diff --git a/.githooks/pre-commit-formatter b/.githooks/pre-commit-formatter new file mode 100644 index 000000000..7d155e35c --- /dev/null +++ b/.githooks/pre-commit-formatter @@ -0,0 +1,16 @@ +#!/bin/bash + +# Get only files that are staged and have changes in the index (excluding unstaged changes) +STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(swift)$') + +if [ -n "$STAGED_FILES" ]; then + echo "Formatting staged files..." + echo "$STAGED_FILES" | while read -r file; do + scripts/AdamantCLI/adamant-cli format staged "$file" + git add "$file" + done +else + echo "No staged files to format." +fi + +exit 0 \ No newline at end of file diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..7d5ecc406 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,37 @@ +# This workflow will build a Swift project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift + +name: run tests + +on: + workflow_call: + push: + branches: [ "develop" ] + pull_request: + branches: [ "develop" ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref_type != 'tag' }} + +jobs: + run_tests: + runs-on: macos-15 + steps: + - name: Set Xcode 16.2.0 + run: sudo xcode-select -s /Applications/Xcode_16.2.0.app/Contents/Developer + - name: Print Current Xcode + run: xcode-select -p + - uses: actions/checkout@v3 + - name: Build adamant-cli + run: make configure + - name: Setup project dependencies + run: ./scripts/AdamantCLI/adamant-cli setup + - name: Pod Install + run: pod install + - name: Generate mocks for tests + run: ./scripts/AdamantCLI/adamant-cli mocks + - name: Run tests + run: xcodebuild test -workspace Adamant.xcworkspace -scheme Adamant.Dev -destination 'platform=iOS Simulator,name=iPhone 16' | xcbeautify + + diff --git a/.gitignore b/.gitignore index bdfc17fd2..9acf0af73 100644 --- a/.gitignore +++ b/.gitignore @@ -92,6 +92,7 @@ xcuserdata/ *.moved-aside *.xccheckout *.xcscmblueprint +.idea/ ### Xcode Patch ### *.xcodeproj/* @@ -109,3 +110,6 @@ runkit ex.rtf to do.rtf tz.rtf profiles +Generated/ +.swiftpm/ +adamant-cli \ No newline at end of file diff --git a/.sourcery.yml b/.sourcery.yml new file mode 100644 index 000000000..ca52e200f --- /dev/null +++ b/.sourcery.yml @@ -0,0 +1,20 @@ +sources: + - ./Adamant/ + - ./CommonKit/ + - ./BitcoinKit/ +templates: + - ./Templates/sourcery/Mock.swifttemplate +output: + path: ./AdamantTests/Stubs/Generated/ +args: + import: + - CommonKit + - CoreData + - XCTest + - Alamofire + - BitcoinKit + - LiskKit + testable: + - Adamant + - SwiftyMocky + - SwiftyMockyXCTest \ No newline at end of file diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 000000000..19e7d1bd7 --- /dev/null +++ b/.swiftformat @@ -0,0 +1,70 @@ +{ + "fileScopedDeclarationPrivacy" : { + "accessLevel" : "private" + }, + "indentConditionalCompilationBlocks" : true, + "indentSwitchCaseLabels" : false, + "indentation" : { + "spaces" : 4 + }, + "lineBreakAroundMultilineExpressionChainComponents" : false, + "lineBreakBeforeControlFlowKeywords" : false, + "lineBreakBeforeEachArgument" : true, + "lineBreakBeforeEachGenericRequirement" : false, + "lineLength" : 160, + "maximumBlankLines" : 1, + "multiElementCollectionTrailingCommas" : false, + "noAssignmentInExpressions" : { + "allowedFunctions" : [ + "XCTAssertNoThrow" + ] + }, + "prioritizeKeepingFunctionOutputTogether" : true, + "respectsExistingLineBreaks" : true, + "rules" : { + "AllPublicDeclarationsHaveDocumentation" : false, + "AlwaysUseLiteralForEmptyCollectionInit" : false, + "AlwaysUseLowerCamelCase" : true, + "AmbiguousTrailingClosureOverload" : true, + "BeginDocumentationCommentWithOneLineSummary" : false, + "DoNotUseSemicolons" : true, + "DontRepeatTypeInStaticProperties" : true, + "FileScopedDeclarationPrivacy" : true, + "FullyIndirectEnum" : true, + "GroupNumericLiterals" : true, + "IdentifiersMustBeASCII" : true, + "NeverForceUnwrap" : false, + "NeverUseForceTry" : false, + "NeverUseImplicitlyUnwrappedOptionals" : false, + "NoAccessLevelOnExtensionDeclaration" : true, + "NoAssignmentInExpressions" : true, + "NoBlockComments" : true, + "NoCasesWithOnlyFallthrough" : true, + "NoEmptyTrailingClosureParentheses" : true, + "NoLabelsInCasePatterns" : true, + "NoLeadingUnderscores" : false, + "NoParensAroundConditions" : true, + "NoPlaygroundLiterals" : true, + "NoVoidReturnOnFunctionSignature" : true, + "OmitExplicitReturns" : false, + "OneCasePerLine" : true, + "OneVariableDeclarationPerLine" : true, + "OnlyOneTrailingClosureArgument" : true, + "OrderedImports" : true, + "ReplaceForEachWithForLoop" : true, + "ReturnVoidInsteadOfEmptyTuple" : true, + "TypeNamesShouldBeCapitalized" : true, + "UseEarlyExits" : false, + "UseExplicitNilCheckInConditions" : true, + "UseLetInEveryBoundCaseVariable" : true, + "UseShorthandTypeNames" : true, + "UseSingleLinePropertyGetter" : true, + "UseSynthesizedInitializer" : true, + "UseTripleSlashForDocumentationComments" : true, + "UseWhereClausesInForLoops" : false, + "ValidateDocumentationComments" : false + }, + "spacesAroundRangeFormationOperators" : false, + "tabWidth" : 8, + "version" : 1 +} diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index 458e56ee5..0540e7588 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -3,435 +3,29 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 77; objects = { /* Begin PBXBuildFile section */ - 2621AB372C60E74A00046D7A /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2621AB362C60E74A00046D7A /* NotificationsView.swift */; }; - 2621AB392C60E7AE00046D7A /* NotificationsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2621AB382C60E7AE00046D7A /* NotificationsViewModel.swift */; }; - 2621AB3B2C613C8100046D7A /* NotificationsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2621AB3A2C613C8100046D7A /* NotificationsFactory.swift */; }; - 2657A0CA2C707D780021E7E6 /* notification.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = E9A174B820587B83003667CD /* notification.mp3 */; }; - 2657A0CB2C707D7B0021E7E6 /* so-proud-notification.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 4198D57A28C8B7DA009337F2 /* so-proud-notification.mp3 */; }; - 2657A0CC2C707D7E0021E7E6 /* relax-message-tone.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 4198D57C28C8B7F9009337F2 /* relax-message-tone.mp3 */; }; - 2657A0CD2C707D800021E7E6 /* short-success.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 4198D57E28C8B834009337F2 /* short-success.mp3 */; }; - 2657A0CE2C707D830021E7E6 /* default.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 4198D58028C8B8D1009337F2 /* default.mp3 */; }; - 265AA1622B74E6B900CF98B0 /* ChatPreservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 265AA1612B74E6B900CF98B0 /* ChatPreservation.swift */; }; - 269B83102C74A2FF002AA1D7 /* note.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B830F2C74A2FF002AA1D7 /* note.mp3 */; }; - 269B83112C74A34F002AA1D7 /* note.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B830F2C74A2FF002AA1D7 /* note.mp3 */; }; - 269B831E2C74B4EC002AA1D7 /* handoff.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83122C74B4EA002AA1D7 /* handoff.mp3 */; }; - 269B831F2C74B4EC002AA1D7 /* handoff.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83122C74B4EA002AA1D7 /* handoff.mp3 */; }; - 269B83202C74B4EC002AA1D7 /* portal.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83132C74B4EA002AA1D7 /* portal.mp3 */; }; - 269B83212C74B4EC002AA1D7 /* portal.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83132C74B4EA002AA1D7 /* portal.mp3 */; }; - 269B83222C74B4EC002AA1D7 /* antic.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83142C74B4EB002AA1D7 /* antic.mp3 */; }; - 269B83232C74B4EC002AA1D7 /* antic.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83142C74B4EB002AA1D7 /* antic.mp3 */; }; - 269B83242C74B4EC002AA1D7 /* droplet.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83152C74B4EB002AA1D7 /* droplet.mp3 */; }; - 269B83252C74B4EC002AA1D7 /* droplet.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83152C74B4EB002AA1D7 /* droplet.mp3 */; }; - 269B83262C74B4EC002AA1D7 /* passage.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83162C74B4EB002AA1D7 /* passage.mp3 */; }; - 269B83272C74B4EC002AA1D7 /* passage.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83162C74B4EB002AA1D7 /* passage.mp3 */; }; - 269B83282C74B4EC002AA1D7 /* chord.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83172C74B4EB002AA1D7 /* chord.mp3 */; }; - 269B83292C74B4EC002AA1D7 /* chord.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83172C74B4EB002AA1D7 /* chord.mp3 */; }; - 269B832A2C74B4EC002AA1D7 /* rattle.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83182C74B4EB002AA1D7 /* rattle.mp3 */; }; - 269B832B2C74B4EC002AA1D7 /* rattle.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83182C74B4EB002AA1D7 /* rattle.mp3 */; }; - 269B832C2C74B4EC002AA1D7 /* rebound.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83192C74B4EB002AA1D7 /* rebound.mp3 */; }; - 269B832D2C74B4EC002AA1D7 /* rebound.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B83192C74B4EB002AA1D7 /* rebound.mp3 */; }; - 269B832E2C74B4EC002AA1D7 /* milestone.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B831A2C74B4EC002AA1D7 /* milestone.mp3 */; }; - 269B832F2C74B4EC002AA1D7 /* milestone.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B831A2C74B4EC002AA1D7 /* milestone.mp3 */; }; - 269B83302C74B4EC002AA1D7 /* cheers.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B831B2C74B4EC002AA1D7 /* cheers.mp3 */; }; - 269B83312C74B4EC002AA1D7 /* cheers.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B831B2C74B4EC002AA1D7 /* cheers.mp3 */; }; - 269B83322C74B4EC002AA1D7 /* slide.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B831C2C74B4EC002AA1D7 /* slide.mp3 */; }; - 269B83332C74B4EC002AA1D7 /* slide.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B831C2C74B4EC002AA1D7 /* slide.mp3 */; }; - 269B83342C74B4EC002AA1D7 /* welcome.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B831D2C74B4EC002AA1D7 /* welcome.mp3 */; }; - 269B83352C74B4EC002AA1D7 /* welcome.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 269B831D2C74B4EC002AA1D7 /* welcome.mp3 */; }; - 269B83372C74D1F9002AA1D7 /* NotificationSoundsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 269B83362C74D1F9002AA1D7 /* NotificationSoundsView.swift */; }; - 269B833A2C74D4AA002AA1D7 /* NotificationSoundsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 269B83392C74D4AA002AA1D7 /* NotificationSoundsViewModel.swift */; }; - 269B833D2C74E661002AA1D7 /* NotificationSoundsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 269B833C2C74E661002AA1D7 /* NotificationSoundsFactory.swift */; }; - 269E13522B594B2D008D1CA7 /* AccountFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 269E13512B594B2D008D1CA7 /* AccountFooterView.swift */; }; - 26A975FF2B7E843E0095C367 /* SelectTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26A975FE2B7E843E0095C367 /* SelectTextView.swift */; }; - 26A976012B7E852E0095C367 /* ChatSelectTextViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26A976002B7E852E0095C367 /* ChatSelectTextViewFactory.swift */; }; 3A075C9E2B98A3B100714E3B /* FilesPickerKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3A075C9D2B98A3B100714E3B /* FilesPickerKit */; }; - 3A20D93B2AE7F316005475A6 /* AdamantTransactionDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A20D93A2AE7F316005475A6 /* AdamantTransactionDetails.swift */; }; - 3A2478AE2BB42967009D89E9 /* ChatDropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2478AD2BB42967009D89E9 /* ChatDropView.swift */; }; - 3A2478B12BB45DF8009D89E9 /* StorageUsageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2478B02BB45DF8009D89E9 /* StorageUsageView.swift */; }; - 3A2478B32BB461A7009D89E9 /* StorageUsageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2478B22BB461A7009D89E9 /* StorageUsageViewModel.swift */; }; - 3A2478B52BB46617009D89E9 /* StorageUsageFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2478B42BB46617009D89E9 /* StorageUsageFactory.swift */; }; - 3A26D9352C3C1BE2003AD832 /* KlyWalletService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A26D9342C3C1BE2003AD832 /* KlyWalletService.swift */; }; - 3A26D9372C3C1C01003AD832 /* KlyWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A26D9362C3C1C01003AD832 /* KlyWallet.swift */; }; - 3A26D9392C3C1C62003AD832 /* KlyWalletFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A26D9382C3C1C62003AD832 /* KlyWalletFactory.swift */; }; - 3A26D93B2C3C1C97003AD832 /* KlyApiCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A26D93A2C3C1C97003AD832 /* KlyApiCore.swift */; }; - 3A26D93D2C3C1CC3003AD832 /* KlyNodeApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A26D93C2C3C1CC3003AD832 /* KlyNodeApiService.swift */; }; - 3A26D93F2C3C1CED003AD832 /* KlyServiceApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A26D93E2C3C1CED003AD832 /* KlyServiceApiService.swift */; }; - 3A26D9412C3C2DC4003AD832 /* KlyWalletService+Send.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A26D9402C3C2DC4003AD832 /* KlyWalletService+Send.swift */; }; - 3A26D9432C3C2E19003AD832 /* KlyWalletService+StatusCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A26D9422C3C2E19003AD832 /* KlyWalletService+StatusCheck.swift */; }; - 3A26D9452C3D336A003AD832 /* KlyWalletService+RichMessageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A26D9442C3D336A003AD832 /* KlyWalletService+RichMessageProvider.swift */; }; - 3A26D9472C3D37B5003AD832 /* KlyWalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A26D9462C3D37B5003AD832 /* KlyWalletViewController.swift */; }; - 3A26D9492C3D3804003AD832 /* KlyTransferViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A26D9482C3D3804003AD832 /* KlyTransferViewController.swift */; }; - 3A26D94B2C3D3838003AD832 /* KlyTransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A26D94A2C3D3838003AD832 /* KlyTransactionsViewController.swift */; }; - 3A26D94D2C3D387B003AD832 /* KlyTransactionDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A26D94C2C3D387B003AD832 /* KlyTransactionDetailsViewController.swift */; }; - 3A26D9502C3D3A5A003AD832 /* KlyWalletService+WalletCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A26D94F2C3D3A5A003AD832 /* KlyWalletService+WalletCore.swift */; }; - 3A26D9522C3E7F1E003AD832 /* klayr_notificationContent.png in Resources */ = {isa = PBXBuildFile; fileRef = 3A26D9512C3E7F1D003AD832 /* klayr_notificationContent.png */; }; - 3A299C692B838AA600B54C61 /* ChatMediaCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A299C682B838AA600B54C61 /* ChatMediaCell.swift */; }; - 3A299C6B2B838F2300B54C61 /* ChatMediaContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A299C6A2B838F2300B54C61 /* ChatMediaContainerView.swift */; }; - 3A299C6D2B838F8F00B54C61 /* ChatMediaContainerView+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A299C6C2B838F8F00B54C61 /* ChatMediaContainerView+Model.swift */; }; - 3A299C712B83975700B54C61 /* ChatMediaContnentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A299C702B83975700B54C61 /* ChatMediaContnentView.swift */; }; - 3A299C732B83975D00B54C61 /* ChatMediaContnentView+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A299C722B83975D00B54C61 /* ChatMediaContnentView+Model.swift */; }; - 3A299C762B84CE4100B54C61 /* FilesToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A299C752B84CE4100B54C61 /* FilesToolbarView.swift */; }; - 3A299C782B84D11100B54C61 /* FilesToolbarCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A299C772B84D11100B54C61 /* FilesToolbarCollectionViewCell.swift */; }; - 3A299C7B2B85EABB00B54C61 /* FileListContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A299C7A2B85EABB00B54C61 /* FileListContentView.swift */; }; - 3A299C7D2B85F98700B54C61 /* ChatFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A299C7C2B85F98700B54C61 /* ChatFile.swift */; }; - 3A2F55F92AC6F308000A3F26 /* CoinTransaction+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2F55F72AC6F308000A3F26 /* CoinTransaction+CoreDataClass.swift */; }; - 3A2F55FA2AC6F308000A3F26 /* CoinTransaction+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2F55F82AC6F308000A3F26 /* CoinTransaction+CoreDataProperties.swift */; }; - 3A2F55FC2AC6F885000A3F26 /* CoinStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2F55FB2AC6F885000A3F26 /* CoinStorage.swift */; }; - 3A2F55FE2AC6F90E000A3F26 /* AdamantCoinStorageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2F55FD2AC6F90E000A3F26 /* AdamantCoinStorageService.swift */; }; - 3A33F9FA2A7A53DA002B8003 /* EmojiUpdateType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A33F9F92A7A53DA002B8003 /* EmojiUpdateType.swift */; }; - 3A4068342ACD7C18007E87BD /* CoinTransaction+TransactionDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4068332ACD7C18007E87BD /* CoinTransaction+TransactionDetails.swift */; }; - 3A41938F2A580C57006A6B22 /* AdamantRichTransactionReactService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A41938E2A580C57006A6B22 /* AdamantRichTransactionReactService.swift */; }; - 3A4193912A580C85006A6B22 /* RichTransactionReactService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4193902A580C85006A6B22 /* RichTransactionReactService.swift */; }; - 3A41939A2A5D554A006A6B22 /* Reaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4193992A5D554A006A6B22 /* Reaction.swift */; }; - 3A53BD462C6B7AF100BB1EE6 /* DownloadPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A53BD452C6B7AF100BB1EE6 /* DownloadPolicy.swift */; }; - 3A5DF1792C4698EC0005369D /* EdgeInsetLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5DF1782C4698EC0005369D /* EdgeInsetLabel.swift */; }; - 3A770E4C2AE14F130008D98F /* SimpleTransactionDetails+Hashable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A770E4B2AE14F130008D98F /* SimpleTransactionDetails+Hashable.swift */; }; - 3A7BD00E2AA9BCE80045AAB0 /* VibroService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7BD00D2AA9BCE80045AAB0 /* VibroService.swift */; }; - 3A7BD0102AA9BD030045AAB0 /* AdamantVibroService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7BD00F2AA9BD030045AAB0 /* AdamantVibroService.swift */; }; - 3A7BD0122AA9BD5A0045AAB0 /* AdamantVibroType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7BD0112AA9BD5A0045AAB0 /* AdamantVibroType.swift */; }; - 3A7FD6F52C076D86002AF7D9 /* FileMessageStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7FD6F42C076D85002AF7D9 /* FileMessageStatus.swift */; }; 3A833C402B99CDA000238F6A /* FilesStorageKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3A833C3F2B99CDA000238F6A /* FilesStorageKit */; }; 3A8875EF27BBF38D00436195 /* Parchment in Frameworks */ = {isa = PBXBuildFile; productRef = 3A8875EE27BBF38D00436195 /* Parchment */; }; - 3A9015A52A614A18002A2464 /* EmojiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9015A42A614A18002A2464 /* EmojiService.swift */; }; - 3A9015A72A614A62002A2464 /* AdamantEmojiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9015A62A614A62002A2464 /* AdamantEmojiService.swift */; }; - 3A9015A92A615893002A2464 /* ChatMessagesListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9015A82A615893002A2464 /* ChatMessagesListViewModel.swift */; }; - 3A9365A92C41332F0073D9A7 /* KLYWalletService+DynamicConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9365A82C41332F0073D9A7 /* KLYWalletService+DynamicConstants.swift */; }; - 3A96E37A2AED27D7001F5A52 /* AdamantPartnerQRService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A96E3792AED27D7001F5A52 /* AdamantPartnerQRService.swift */; }; - 3A96E37C2AED27F8001F5A52 /* PartnerQRService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A96E37B2AED27F8001F5A52 /* PartnerQRService.swift */; }; - 3AA2D5F7280EADE3000ED971 /* SocketService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA2D5F6280EADE3000ED971 /* SocketService.swift */; }; - 3AA2D5FA280EAF5D000ED971 /* AdamantSocketService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA2D5F9280EAF5D000ED971 /* AdamantSocketService.swift */; }; - 3AA388052B67F4DD00125684 /* BtcBlockchainInfoDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA388042B67F4DD00125684 /* BtcBlockchainInfoDTO.swift */; }; - 3AA388072B67F53F00125684 /* BtcNetworkInfoDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA388062B67F53F00125684 /* BtcNetworkInfoDTO.swift */; }; - 3AA3880A2B69173500125684 /* DashNetworkInfoDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA388092B69173500125684 /* DashNetworkInfoDTO.swift */; }; - 3AA50DEF2AEBE65D00C58FC8 /* PartnerQRView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA50DEE2AEBE65D00C58FC8 /* PartnerQRView.swift */; }; - 3AA50DF12AEBE66A00C58FC8 /* PartnerQRViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA50DF02AEBE66A00C58FC8 /* PartnerQRViewModel.swift */; }; - 3AA50DF32AEBE67C00C58FC8 /* PartnerQRFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA50DF22AEBE67C00C58FC8 /* PartnerQRFactory.swift */; }; - 3AA6DF402BA9941E00EA2E16 /* MediaContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA6DF3F2BA9941E00EA2E16 /* MediaContainerView.swift */; }; - 3AA6DF442BA997C000EA2E16 /* FileListContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA6DF432BA997C000EA2E16 /* FileListContainerView.swift */; }; - 3AA6DF462BA9BEB700EA2E16 /* MediaContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA6DF452BA9BEB700EA2E16 /* MediaContentView.swift */; }; 3AC76E3D2AB09118008042C4 /* ElegantEmojiPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 3AC76E3C2AB09118008042C4 /* ElegantEmojiPicker */; }; - 3ACD307E2BBD86B700ABF671 /* FilesStorageProprietiesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACD307D2BBD86B700ABF671 /* FilesStorageProprietiesService.swift */; }; - 3ACD30802BBD86C800ABF671 /* FilesStorageProprietiesProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACD307F2BBD86C800ABF671 /* FilesStorageProprietiesProtocol.swift */; }; - 3AE0A42A2BC6A64900BF7125 /* FilesNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE0A4272BC6A64900BF7125 /* FilesNetworkManager.swift */; }; - 3AE0A42B2BC6A64900BF7125 /* IPFSApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE0A4282BC6A64900BF7125 /* IPFSApiService.swift */; }; - 3AE0A42C2BC6A64900BF7125 /* IPFSApiCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE0A4292BC6A64900BF7125 /* IPFSApiCore.swift */; }; - 3AE0A42E2BC6A96B00BF7125 /* IPFS+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE0A42D2BC6A96A00BF7125 /* IPFS+Constants.swift */; }; - 3AE0A4312BC6A9C900BF7125 /* IPFSDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE0A4302BC6A9C900BF7125 /* IPFSDTO.swift */; }; - 3AE0A4332BC6A9EB00BF7125 /* FileApiServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE0A4322BC6A9EB00BF7125 /* FileApiServiceProtocol.swift */; }; - 3AE0A4352BC6AA1B00BF7125 /* FileManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE0A4342BC6AA1B00BF7125 /* FileManagerError.swift */; }; - 3AE0A4372BC6AA6000BF7125 /* FilesNetworkManagerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE0A4362BC6AA6000BF7125 /* FilesNetworkManagerProtocol.swift */; }; - 3AF08D5F2B4EB3A200EB82B1 /* LanguageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF08D5E2B4EB3A200EB82B1 /* LanguageService.swift */; }; - 3AF08D612B4EB3C400EB82B1 /* LanguageStorageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF08D602B4EB3C400EB82B1 /* LanguageStorageProtocol.swift */; }; - 3AF0A6CA2BBAF5850019FF47 /* ChatFileService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF0A6C92BBAF5850019FF47 /* ChatFileService.swift */; }; - 3AF53F8D2B3DCFA300B30312 /* NodeGroup+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF53F8C2B3DCFA300B30312 /* NodeGroup+Constants.swift */; }; - 3AF53F8F2B3EE0DA00B30312 /* DogeNodeInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF53F8E2B3EE0DA00B30312 /* DogeNodeInfo.swift */; }; - 3AF8D9E92C73ADFA007A7CBC /* IPFSNodeStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF8D9E82C73ADFA007A7CBC /* IPFSNodeStatus.swift */; }; - 3AF9DF0B2BFE306C009A43A8 /* ChatFileProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF9DF0A2BFE306C009A43A8 /* ChatFileProtocol.swift */; }; - 3AF9DF0D2C049161009A43A8 /* CircularProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF9DF0C2C049161009A43A8 /* CircularProgressView.swift */; }; - 3AFE7E412B18D88B00718739 /* WalletService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AFE7E402B18D88B00718739 /* WalletService.swift */; }; - 3AFE7E432B19E4D900718739 /* WalletServiceCompose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AFE7E422B19E4D900718739 /* WalletServiceCompose.swift */; }; - 3AFE7E522B1F6B3400718739 /* WalletServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AFE7E512B1F6B3400718739 /* WalletServiceProtocol.swift */; }; 3C06931576393125C61FB8F6 /* Pods_Adamant.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 33975C0D891698AA7E74EBCC /* Pods_Adamant.framework */; }; - 41047B70294B5EE10039E956 /* VisibleWalletsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41047B6F294B5EE10039E956 /* VisibleWalletsViewController.swift */; }; - 41047B72294B5F210039E956 /* VisibleWalletsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41047B71294B5F210039E956 /* VisibleWalletsTableViewCell.swift */; }; - 41047B74294C61D10039E956 /* VisibleWalletsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41047B73294C61D10039E956 /* VisibleWalletsService.swift */; }; - 41047B76294C62710039E956 /* AdamantVisibleWalletsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41047B75294C62710039E956 /* AdamantVisibleWalletsService.swift */; }; - 411743002A39B1D2008CD98A /* ContributeFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 411742FF2A39B1D2008CD98A /* ContributeFactory.swift */; }; - 411743022A39B208008CD98A /* ContributeState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 411743012A39B208008CD98A /* ContributeState.swift */; }; - 411743042A39B257008CD98A /* ContributeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 411743032A39B257008CD98A /* ContributeViewModel.swift */; }; - 411DB8332A14D01F006AB158 /* ChatKeyboardManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 411DB8322A14D01F006AB158 /* ChatKeyboardManager.swift */; }; - 412C0ED929124A3400DE2C5E /* dash_notificationContent.png in Resources */ = {isa = PBXBuildFile; fileRef = 412C0ED829124A3400DE2C5E /* dash_notificationContent.png */; }; - 41330F7629F1509400CB587C /* AdamantCellAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41330F7529F1509400CB587C /* AdamantCellAnimation.swift */; }; - 4133AED429769EEC00F3D017 /* UpdatingIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4133AED329769EEC00F3D017 /* UpdatingIndicatorView.swift */; }; - 4133AF242A1CE1A3001A0A1E /* UITableView+Adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4133AF232A1CE1A3001A0A1E /* UITableView+Adamant.swift */; }; - 4153045929C09902000E4BEA /* AdamantIncreaseFeeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4153045829C09902000E4BEA /* AdamantIncreaseFeeService.swift */; }; - 4153045B29C09C6C000E4BEA /* IncreaseFeeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4153045A29C09C6C000E4BEA /* IncreaseFeeService.swift */; }; - 4154413B2923AED000824478 /* bitcoin_notificationContent.png in Resources */ = {isa = PBXBuildFile; fileRef = 4154413A2923AED000824478 /* bitcoin_notificationContent.png */; }; - 4154413C2923AED000824478 /* bitcoin_notificationContent.png in Resources */ = {isa = PBXBuildFile; fileRef = 4154413A2923AED000824478 /* bitcoin_notificationContent.png */; }; - 416380E12A51765F00F90E6D /* ChatReactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 416380E02A51765F00F90E6D /* ChatReactionsView.swift */; }; - 4164A9D728F17D4000EEF16D /* ChatTransactionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4164A9D628F17D4000EEF16D /* ChatTransactionService.swift */; }; - 4164A9D928F17DA700EEF16D /* AdamantChatTransactionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4164A9D828F17DA700EEF16D /* AdamantChatTransactionService.swift */; }; 416F5EA4290162EB00EF0400 /* SocketIO in Frameworks */ = {isa = PBXBuildFile; productRef = 416F5EA3290162EB00EF0400 /* SocketIO */; }; 4177E5E12A52DA7100C089FE /* AdvancedContextMenuKit in Frameworks */ = {isa = PBXBuildFile; productRef = 4177E5E02A52DA7100C089FE /* AdvancedContextMenuKit */; }; - 4184F16E2A33023A00D7B8B9 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 4184F16D2A33023A00D7B8B9 /* GoogleService-Info.plist */; }; 4184F1712A33044E00D7B8B9 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = 4184F1702A33044E00D7B8B9 /* FirebaseCrashlytics */; }; - 4184F1732A33102800D7B8B9 /* AdamantCrashlysticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4184F1722A33102800D7B8B9 /* AdamantCrashlysticsService.swift */; }; - 4184F1752A33106200D7B8B9 /* CrashlysticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4184F1742A33106200D7B8B9 /* CrashlysticsService.swift */; }; - 4184F1772A33173100D7B8B9 /* ContributeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4184F1762A33173100D7B8B9 /* ContributeView.swift */; }; - 4186B3302941E642006594A3 /* AdmWalletService+DynamicConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4186B32F2941E642006594A3 /* AdmWalletService+DynamicConstants.swift */; }; - 4186B332294200B4006594A3 /* BtcWalletService+DynamicConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4186B331294200B4006594A3 /* BtcWalletService+DynamicConstants.swift */; }; - 4186B334294200C5006594A3 /* EthWalletService+DynamicConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4186B333294200C5006594A3 /* EthWalletService+DynamicConstants.swift */; }; - 4186B338294200E8006594A3 /* DogeWalletService+DynamicConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4186B337294200E8006594A3 /* DogeWalletService+DynamicConstants.swift */; }; - 4186B33A294200F4006594A3 /* DashWalletService+DynamicConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4186B339294200F4006594A3 /* DashWalletService+DynamicConstants.swift */; }; - 418FDE502A25CA340055E3CD /* ChatMenuManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 418FDE4F2A25CA340055E3CD /* ChatMenuManager.swift */; }; - 4193AE1629FBEFBF002F21BE /* NSAttributedText+Adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4193AE1529FBEFBF002F21BE /* NSAttributedText+Adamant.swift */; }; - 4197B9C92952FAFF004CAF64 /* VisibleWalletsCheckmarkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4197B9C82952FAFF004CAF64 /* VisibleWalletsCheckmarkView.swift */; }; - 4198D57B28C8B7DA009337F2 /* so-proud-notification.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 4198D57A28C8B7DA009337F2 /* so-proud-notification.mp3 */; }; - 4198D57D28C8B7FA009337F2 /* relax-message-tone.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 4198D57C28C8B7F9009337F2 /* relax-message-tone.mp3 */; }; - 4198D57F28C8B834009337F2 /* short-success.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 4198D57E28C8B834009337F2 /* short-success.mp3 */; }; - 4198D58128C8B8D1009337F2 /* default.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = 4198D58028C8B8D1009337F2 /* default.mp3 */; }; - 41A1994229D2D3920031AD75 /* SwipePanGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A1994129D2D3920031AD75 /* SwipePanGestureRecognizer.swift */; }; - 41A1994429D2D3CF0031AD75 /* MessageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A1994329D2D3CF0031AD75 /* MessageModel.swift */; }; - 41A1994629D2FCF80031AD75 /* ReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A1994529D2FCF80031AD75 /* ReplyView.swift */; }; - 41A1995229D42C460031AD75 /* ChatMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A1995129D42C460031AD75 /* ChatMessageCell.swift */; }; - 41A1995429D56E340031AD75 /* ChatMessageReplyCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A1995329D56E340031AD75 /* ChatMessageReplyCell.swift */; }; - 41A1995629D56EAA0031AD75 /* ChatMessageReplyCell+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A1995529D56EAA0031AD75 /* ChatMessageReplyCell+Model.swift */; }; - 41A1995829D5733D0031AD75 /* ChatMessageCell+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A1995729D5733D0031AD75 /* ChatMessageCell+Model.swift */; }; - 41BCB310295C6082004B12AB /* VisibleWalletsResetTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41BCB30F295C6082004B12AB /* VisibleWalletsResetTableViewCell.swift */; }; - 41C1698C29E7F34900FEB3CB /* RichTransactionReplyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41C1698B29E7F34900FEB3CB /* RichTransactionReplyService.swift */; }; - 41C1698E29E7F36900FEB3CB /* AdamantRichTransactionReplyService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41C1698D29E7F36900FEB3CB /* AdamantRichTransactionReplyService.swift */; }; - 41CA598C29A0D84F002BFDE4 /* TaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41CA598B29A0D84F002BFDE4 /* TaskManager.swift */; }; - 41CE153A297FF98200CC9254 /* Web3Swift+Adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41CE1539297FF98200CC9254 /* Web3Swift+Adamant.swift */; }; - 41E3C9CC2A0E20F500AF0985 /* AdamantCoinTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41E3C9CB2A0E20F500AF0985 /* AdamantCoinTools.swift */; }; - 4E9EE86F28CE793D008359F7 /* SafeDecimalRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E9EE86E28CE793D008359F7 /* SafeDecimalRow.swift */; }; - 551F66E628959A5300DE5D69 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551F66E528959A5200DE5D69 /* LoadingView.swift */; }; - 551F66E82895B3DA00DE5D69 /* AdamantHealthCheckServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551F66E72895B3DA00DE5D69 /* AdamantHealthCheckServiceTests.swift */; }; - 5551CC8F28A8B75300B52AD0 /* ApiServiceStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5551CC8E28A8B75300B52AD0 /* ApiServiceStub.swift */; }; 557AC306287B10D8004699D7 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 557AC305287B10D8004699D7 /* SnapKit */; }; - 557AC308287B1365004699D7 /* CheckmarkRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557AC307287B1365004699D7 /* CheckmarkRowView.swift */; }; 55D1D84F287B78F200F94A4E /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 55D1D84E287B78F200F94A4E /* SnapKit */; }; 55D1D851287B78FC00F94A4E /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 55D1D850287B78FC00F94A4E /* SnapKit */; }; - 55D1D855287B890300F94A4E /* AddressGeneratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55D1D854287B890300F94A4E /* AddressGeneratorTests.swift */; }; - 55E69E172868D7920025D82E /* CheckmarkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E69E162868D7920025D82E /* CheckmarkView.swift */; }; - 55FBAAFB28C550920066E629 /* NodesAllowanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55FBAAFA28C550920066E629 /* NodesAllowanceTests.swift */; }; 6403F5DB2272389800D58779 /* (null) in Sources */ = {isa = PBXBuildFile; }; - 6403F5DE22723C6800D58779 /* DashMainnet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6403F5DD22723C6800D58779 /* DashMainnet.swift */; }; - 6403F5E022723F6400D58779 /* DashWalletFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6403F5DF22723F6400D58779 /* DashWalletFactory.swift */; }; - 6403F5E222723F7500D58779 /* DashWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6403F5E122723F7500D58779 /* DashWallet.swift */; }; - 6403F5E422723F8C00D58779 /* DashWalletService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6403F5E322723F8C00D58779 /* DashWalletService.swift */; }; - 6403F5E622723FDA00D58779 /* DashWalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6403F5E522723FDA00D58779 /* DashWalletViewController.swift */; }; - 6406D74A21C7F06000196713 /* SearchResultsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6406D74821C7F06000196713 /* SearchResultsViewController.xib */; }; - 6414C18E217DF43100373FA6 /* String+adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6414C18D217DF43100373FA6 /* String+adamant.swift */; }; - 644793C32166314A00FC4CF5 /* OnboardPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644793C22166314A00FC4CF5 /* OnboardPage.swift */; }; - 6448C291235CA6E100F3F15B /* ERC20WalletService+RichMessageProviderWithStatusCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6449BA64235CA0930033B936 /* ERC20WalletService+RichMessageProviderWithStatusCheck.swift */; }; - 6449BA68235CA0930033B936 /* ERC20WalletService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6449BA5E235CA0930033B936 /* ERC20WalletService.swift */; }; - 6449BA69235CA0930033B936 /* ERC20TransferViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6449BA5F235CA0930033B936 /* ERC20TransferViewController.swift */; }; - 6449BA6A235CA0930033B936 /* ERC20Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6449BA60235CA0930033B936 /* ERC20Wallet.swift */; }; - 6449BA6B235CA0930033B936 /* ERC20TransactionDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6449BA61235CA0930033B936 /* ERC20TransactionDetailsViewController.swift */; }; - 6449BA6C235CA0930033B936 /* ERC20WalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6449BA62235CA0930033B936 /* ERC20WalletViewController.swift */; }; - 6449BA6D235CA0930033B936 /* ERC20TransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6449BA63235CA0930033B936 /* ERC20TransactionsViewController.swift */; }; - 6449BA6F235CA0930033B936 /* ERC20WalletService+Send.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6449BA65235CA0930033B936 /* ERC20WalletService+Send.swift */; }; - 6449BA70235CA0930033B936 /* ERC20WalletFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6449BA66235CA0930033B936 /* ERC20WalletFactory.swift */; }; - 6449BA71235CA0930033B936 /* ERC20WalletService+RichMessageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6449BA67235CA0930033B936 /* ERC20WalletService+RichMessageProvider.swift */; }; - 644EC35220EFA9A300F40C73 /* DelegatesFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EC35120EFA9A300F40C73 /* DelegatesFactory.swift */; }; - 644EC35720EFAAB700F40C73 /* DelegatesListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EC35520EFAAB700F40C73 /* DelegatesListViewController.swift */; }; - 644EC35B20EFB8E900F40C73 /* AdamantDelegateCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EC35920EFB8E900F40C73 /* AdamantDelegateCell.swift */; }; - 644EC35E20F34F1E00F40C73 /* DelegateDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EC35D20F34F1E00F40C73 /* DelegateDetailsViewController.swift */; }; - 6455E9F121075D3600B2E94C /* AddressBookService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6455E9F021075D3600B2E94C /* AddressBookService.swift */; }; - 6455E9F321075D8000B2E94C /* AdamantAddressBookService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6455E9F221075D8000B2E94C /* AdamantAddressBookService.swift */; }; - 6458548C211B3AB1004C5909 /* WelcomeViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6458548A211B3AB1004C5909 /* WelcomeViewController.xib */; }; - 645938942378395E00A2BE7C /* EulaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 645938922378395E00A2BE7C /* EulaViewController.swift */; }; - 645938952378395E00A2BE7C /* EulaViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 645938932378395E00A2BE7C /* EulaViewController.xib */; }; - 645AE06621E67D3300AD3623 /* UITextField+adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 645AE06521E67D3300AD3623 /* UITextField+adamant.swift */; }; - 645FEB34213E72C100D6BA2D /* OnboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 645FEB32213E72C100D6BA2D /* OnboardViewController.swift */; }; - 645FEB35213E72C100D6BA2D /* OnboardViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 645FEB33213E72C100D6BA2D /* OnboardViewController.xib */; }; - 648BCA6D213D384F00875EB5 /* AvatarService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648BCA6C213D384F00875EB5 /* AvatarService.swift */; }; - 648C696F22915A12006645F5 /* DashTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648C696E22915A12006645F5 /* DashTransaction.swift */; }; - 648C697122915CB8006645F5 /* BTCRPCServerResponce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648C697022915CB8006645F5 /* BTCRPCServerResponce.swift */; }; - 648C697322916192006645F5 /* DashTransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648C697222916192006645F5 /* DashTransactionsViewController.swift */; }; - 648CE3A022999C890070A2CC /* BaseBtcTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648CE39F22999C890070A2CC /* BaseBtcTransaction.swift */; }; - 648CE3A222999CE70070A2CC /* BTCRawTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648CE3A122999CE70070A2CC /* BTCRawTransaction.swift */; }; - 648CE3A42299A94D0070A2CC /* DashTransactionDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648CE3A32299A94D0070A2CC /* DashTransactionDetailsViewController.swift */; }; - 648CE3A6229AD1CD0070A2CC /* DashWalletService+Send.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648CE3A5229AD1CD0070A2CC /* DashWalletService+Send.swift */; }; - 648CE3A8229AD1E20070A2CC /* DashWalletService+RichMessageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648CE3A7229AD1E20070A2CC /* DashWalletService+RichMessageProvider.swift */; }; - 648CE3AA229AD1F90070A2CC /* DashWalletService+RichMessageProviderWithStatusCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648CE3A9229AD1F90070A2CC /* DashWalletService+RichMessageProviderWithStatusCheck.swift */; }; - 648CE3AC229AD2190070A2CC /* DashTransferViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648CE3AB229AD2190070A2CC /* DashTransferViewController.swift */; }; - 648DD79E2236A0B500B811FD /* DogeTransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648DD79D2236A0B500B811FD /* DogeTransactionsViewController.swift */; }; - 648DD7A02236A59200B811FD /* DogeTransactionDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648DD79F2236A59200B811FD /* DogeTransactionDetailsViewController.swift */; }; - 648DD7A22237D9A000B811FD /* DogeTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648DD7A12237D9A000B811FD /* DogeTransaction.swift */; }; - 648DD7A42237DB9E00B811FD /* DogeWalletService+Send.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648DD7A32237DB9E00B811FD /* DogeWalletService+Send.swift */; }; - 648DD7A62237DC4000B811FD /* DogeTransferViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648DD7A52237DC4000B811FD /* DogeTransferViewController.swift */; }; - 648DD7A82239147800B811FD /* DogeWalletService+RichMessageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648DD7A72239147800B811FD /* DogeWalletService+RichMessageProvider.swift */; }; - 648DD7AA2239150E00B811FD /* DogeWalletService+RichMessageProviderWithStatusCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = 648DD7A92239150E00B811FD /* DogeWalletService+RichMessageProviderWithStatusCheck.swift */; }; - 649D6BEC21BD5A53009E727B /* UISuffixTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649D6BEB21BD5A53009E727B /* UISuffixTextField.swift */; }; - 649D6BF021BFF481009E727B /* AdamantChatsProvider+search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649D6BEF21BFF481009E727B /* AdamantChatsProvider+search.swift */; }; - 649D6BF221C27D5C009E727B /* SearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649D6BF121C27D5C009E727B /* SearchResultsViewController.swift */; }; - 64A223D620F760BB005157CB /* Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A223D520F760BB005157CB /* Localization.swift */; }; - 64B5736F2209B892005DC968 /* BtcTransactionDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64B5736E2209B892005DC968 /* BtcTransactionDetailsViewController.swift */; }; - 64BD2B7520E2814B00E2CD36 /* EthTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64BD2B7420E2814B00E2CD36 /* EthTransaction.swift */; }; - 64BD2B7720E2820300E2CD36 /* TransactionDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64BD2B7620E2820300E2CD36 /* TransactionDetails.swift */; }; - 64C65F4523893C7600DC0425 /* OnboardOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C65F4423893C7600DC0425 /* OnboardOverlay.swift */; }; - 64D059FF20D3116B003AD655 /* NodesListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D059FE20D3116A003AD655 /* NodesListViewController.swift */; }; - 64E1C82D222E95E2006C4DA7 /* DogeWalletFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E1C82C222E95E2006C4DA7 /* DogeWalletFactory.swift */; }; - 64E1C82F222E95F6006C4DA7 /* DogeWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E1C82E222E95F6006C4DA7 /* DogeWallet.swift */; }; - 64E1C831222E9617006C4DA7 /* DogeWalletService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E1C830222E9617006C4DA7 /* DogeWalletService.swift */; }; - 64E1C833222EA0F0006C4DA7 /* DogeWalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E1C832222EA0F0006C4DA7 /* DogeWalletViewController.swift */; }; - 64EAB37422463E020018D9B2 /* InfoServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EAB37322463E020018D9B2 /* InfoServiceProtocol.swift */; }; - 64F085D920E2D7600006DE68 /* AdmTransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F085D820E2D7600006DE68 /* AdmTransactionsViewController.swift */; }; - 64FA53CD20E1300B006783C9 /* EthTransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FA53CC20E1300A006783C9 /* EthTransactionsViewController.swift */; }; - 64FA53D120E24942006783C9 /* TransactionDetailsViewControllerBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FA53D020E24941006783C9 /* TransactionDetailsViewControllerBase.swift */; }; - 85B405022D3012D5000AB744 /* AccountViewController+Form.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85B405012D3012C7000AB744 /* AccountViewController+Form.swift */; }; - 9300F94629D0149100FEDDB8 /* RichMessageProviderWithStatusCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9300F94529D0149100FEDDB8 /* RichMessageProviderWithStatusCheck.swift */; }; - 9304F8BE292F88F900173F18 /* ANSPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9304F8BD292F88F900173F18 /* ANSPayload.swift */; }; - 9304F8C2292F895C00173F18 /* PushNotificationsTokenService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9304F8C1292F895C00173F18 /* PushNotificationsTokenService.swift */; }; - 9304F8C4292F8A3100173F18 /* AdamantPushNotificationsTokenService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9304F8C3292F8A3100173F18 /* AdamantPushNotificationsTokenService.swift */; }; - 9306E0932CF8C50E00A99BA4 /* PKGeneratorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9306E0912CF8C50E00A99BA4 /* PKGeneratorView.swift */; }; - 9306E0942CF8C50E00A99BA4 /* PKGeneratorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9306E08F2CF8C50E00A99BA4 /* PKGeneratorFactory.swift */; }; - 9306E0952CF8C50E00A99BA4 /* PKGeneratorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9306E0922CF8C50E00A99BA4 /* PKGeneratorViewModel.swift */; }; - 9306E0962CF8C50E00A99BA4 /* PKGeneratorState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9306E0902CF8C50E00A99BA4 /* PKGeneratorState.swift */; }; - 9306E0982CF8C67B00A99BA4 /* AdamantSecureField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9306E0972CF8C67B00A99BA4 /* AdamantSecureField.swift */; }; - 931224A92C7AA0E4009E0ED0 /* InfoServiceApiService+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 931224A82C7AA0E4009E0ED0 /* InfoServiceApiService+Extension.swift */; }; - 931224AB2C7AA212009E0ED0 /* InfoServiceRatesRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 931224AA2C7AA212009E0ED0 /* InfoServiceRatesRequestDTO.swift */; }; - 931224AD2C7AA67B009E0ED0 /* InfoServiceHistoryRequestDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 931224AC2C7AA67B009E0ED0 /* InfoServiceHistoryRequestDTO.swift */; }; - 931224AF2C7AA88E009E0ED0 /* InfoService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 931224AE2C7AA88E009E0ED0 /* InfoService.swift */; }; - 931224B12C7ACFE6009E0ED0 /* InfoServiceAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 931224B02C7ACFE6009E0ED0 /* InfoServiceAssembly.swift */; }; - 931224B32C7AD5DD009E0ED0 /* InfoServiceTicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 931224B22C7AD5DD009E0ED0 /* InfoServiceTicker.swift */; }; - 9322E875297042F000B8357C /* ChatSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9322E874297042F000B8357C /* ChatSender.swift */; }; - 9322E877297042FA00B8357C /* ChatMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9322E876297042FA00B8357C /* ChatMessage.swift */; }; - 9322E87B2970431200B8357C /* ChatMessageFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9322E87A2970431200B8357C /* ChatMessageFactory.swift */; }; - 93294B7D2AAD067000911109 /* AppContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93294B7C2AAD067000911109 /* AppContainer.swift */; }; - 93294B822AAD0BB400911109 /* BtcWalletFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93294B812AAD0BB400911109 /* BtcWalletFactory.swift */; }; - 93294B842AAD0C8F00911109 /* Assembler+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93294B832AAD0C8F00911109 /* Assembler+Extension.swift */; }; - 93294B872AAD0E0A00911109 /* AdmWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93294B852AAD0E0A00911109 /* AdmWallet.swift */; }; - 93294B882AAD0E0A00911109 /* AdmWalletService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93294B862AAD0E0A00911109 /* AdmWalletService.swift */; }; - 93294B8E2AAD2C6B00911109 /* SwiftyOnboardPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93294B8B2AAD2C6B00911109 /* SwiftyOnboardPage.swift */; }; - 93294B8F2AAD2C6B00911109 /* SwiftyOnboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93294B8C2AAD2C6B00911109 /* SwiftyOnboard.swift */; }; - 93294B902AAD2C6B00911109 /* SwiftyOnboardOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93294B8D2AAD2C6B00911109 /* SwiftyOnboardOverlay.swift */; }; - 93294B962AAD320B00911109 /* ScreensFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93294B952AAD320B00911109 /* ScreensFactory.swift */; }; - 93294B982AAD364F00911109 /* AdamantScreensFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93294B972AAD364F00911109 /* AdamantScreensFactory.swift */; }; - 93294B9A2AAD624100911109 /* WalletFactoryCompose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93294B992AAD624100911109 /* WalletFactoryCompose.swift */; }; - 932B34E92974AA4A002A75BA /* ChatPreservationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 932B34E82974AA4A002A75BA /* ChatPreservationProtocol.swift */; }; - 932F77592989F999006D8801 /* ChatCellManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 932F77582989F999006D8801 /* ChatCellManager.swift */; }; - 9332C39D2C76BE7500164B80 /* FileApiServiceResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9332C39C2C76BE7500164B80 /* FileApiServiceResult.swift */; }; - 9332C3A32C76C45A00164B80 /* ApiServiceComposeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9332C3A22C76C45A00164B80 /* ApiServiceComposeProtocol.swift */; }; - 9332C3A52C76C4EC00164B80 /* ApiServiceCompose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9332C3A42C76C4EC00164B80 /* ApiServiceCompose.swift */; }; - 9340078029AC341100A20622 /* ChatAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9340077F29AC341000A20622 /* ChatAction.swift */; }; + 6FB686162D3AAE8800CAB6DD /* AdamantWalletsKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6FB686152D3AAE8800CAB6DD /* AdamantWalletsKit */; }; 9342F6C22A6A35E300A9B39F /* CommonKit in Frameworks */ = {isa = PBXBuildFile; productRef = 9342F6C12A6A35E300A9B39F /* CommonKit */; }; - 9345769528FD0C34004E6C7A /* UIViewController+email.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9345769428FD0C34004E6C7A /* UIViewController+email.swift */; }; - 93496B832A6C85F400DD062F /* AdamantResources+CoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93496B822A6C85F400DD062F /* AdamantResources+CoreData.swift */; }; - 93496BA02A6CAE9300DD062F /* LogoFullHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = 93496B9F2A6CAE9300DD062F /* LogoFullHeader.xib */; }; - 93496BAD2A6CAED100DD062F /* Roboto_700_normal.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 93496BA22A6CAED100DD062F /* Roboto_700_normal.ttf */; }; - 93496BAE2A6CAED100DD062F /* Exo+2_700_normal.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 93496BA32A6CAED100DD062F /* Exo+2_700_normal.ttf */; }; - 93496BAF2A6CAED100DD062F /* Exo+2_100_normal.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 93496BA42A6CAED100DD062F /* Exo+2_100_normal.ttf */; }; - 93496BB02A6CAED100DD062F /* Exo+2_300_normal.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 93496BA52A6CAED100DD062F /* Exo+2_300_normal.ttf */; }; - 93496BB12A6CAED100DD062F /* Exo+2_400_normal.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 93496BA62A6CAED100DD062F /* Exo+2_400_normal.ttf */; }; - 93496BB22A6CAED100DD062F /* Exo+2_400_italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 93496BA72A6CAED100DD062F /* Exo+2_400_italic.ttf */; }; - 93496BB32A6CAED100DD062F /* Exo+2_500_normal.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 93496BA82A6CAED100DD062F /* Exo+2_500_normal.ttf */; }; - 93496BB42A6CAED100DD062F /* Roboto_400_italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 93496BA92A6CAED100DD062F /* Roboto_400_italic.ttf */; }; - 93496BB52A6CAED100DD062F /* Roboto_300_normal.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 93496BAA2A6CAED100DD062F /* Roboto_300_normal.ttf */; }; - 93496BB62A6CAED100DD062F /* Roboto_400_normal.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 93496BAB2A6CAED100DD062F /* Roboto_400_normal.ttf */; }; - 93496BB72A6CAED100DD062F /* Roboto_500_normal.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 93496BAC2A6CAED100DD062F /* Roboto_500_normal.ttf */; }; - 934FD9A42C783D2E00336841 /* InfoServiceStatusDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9A32C783D2E00336841 /* InfoServiceStatusDTO.swift */; }; - 934FD9A62C783DB700336841 /* InfoServiceStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9A52C783DB700336841 /* InfoServiceStatus.swift */; }; - 934FD9A82C783E0C00336841 /* InfoServiceMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9A72C783E0C00336841 /* InfoServiceMapper.swift */; }; - 934FD9AA2C7842C800336841 /* InfoServiceResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9A92C7842C800336841 /* InfoServiceResponseDTO.swift */; }; - 934FD9AC2C78443600336841 /* InfoServiceHistoryItemDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9AB2C78443600336841 /* InfoServiceHistoryItemDTO.swift */; }; - 934FD9AE2C7846BA00336841 /* InfoServiceHistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9AD2C7846BA00336841 /* InfoServiceHistoryItem.swift */; }; - 934FD9B02C78481500336841 /* InfoServiceApiError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9AF2C78481500336841 /* InfoServiceApiError.swift */; }; - 934FD9B22C7849C800336841 /* InfoServiceApiResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9B12C7849C800336841 /* InfoServiceApiResult.swift */; }; - 934FD9B42C78514E00336841 /* InfoServiceApiCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9B32C78514E00336841 /* InfoServiceApiCore.swift */; }; - 934FD9B62C78519600336841 /* InfoServiceApiCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9B52C78519600336841 /* InfoServiceApiCommands.swift */; }; - 934FD9B82C7854AF00336841 /* InfoServiceMapperProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9B72C7854AF00336841 /* InfoServiceMapperProtocol.swift */; }; - 934FD9BA2C78565400336841 /* InfoServiceApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9B92C78565400336841 /* InfoServiceApiService.swift */; }; - 934FD9BC2C78567300336841 /* InfoServiceApiServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934FD9BB2C78567300336841 /* InfoServiceApiServiceProtocol.swift */; }; - 93547BCA29E2262D00B0914B /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93547BC929E2262D00B0914B /* WelcomeViewController.swift */; }; - 9366588D2B0AB6BD00BDB2D3 /* CoinsNodesListState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9366588C2B0AB6BD00BDB2D3 /* CoinsNodesListState.swift */; }; - 9366588F2B0AB97500BDB2D3 /* CoinsNodesListMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9366588E2B0AB97500BDB2D3 /* CoinsNodesListMapper.swift */; }; - 936658932B0AC03700BDB2D3 /* CoinsNodesListStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 936658922B0AC03700BDB2D3 /* CoinsNodesListStrings.swift */; }; - 936658952B0AC15300BDB2D3 /* Node+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 936658942B0AC15300BDB2D3 /* Node+UI.swift */; }; - 936658972B0ACB1500BDB2D3 /* CoinsNodesListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 936658962B0ACB1500BDB2D3 /* CoinsNodesListViewModel.swift */; }; - 9366589D2B0ADBAF00BDB2D3 /* CoinsNodesListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9366589C2B0ADBAF00BDB2D3 /* CoinsNodesListView.swift */; }; - 936658A32B0ADE4400BDB2D3 /* CoinsNodesListView+Row.swift in Sources */ = {isa = PBXBuildFile; fileRef = 936658A22B0ADE4400BDB2D3 /* CoinsNodesListView+Row.swift */; }; - 936658A52B0AE67A00BDB2D3 /* CoinsNodesListFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 936658A42B0AE67A00BDB2D3 /* CoinsNodesListFactory.swift */; }; - 93684A2A29EFA28A00F9EFFE /* FixedTextMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93684A2929EFA28A00F9EFFE /* FixedTextMessageSizeCalculator.swift */; }; - 9371130F2996EDA900F64CF9 /* ChatRefreshMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9371130E2996EDA900F64CF9 /* ChatRefreshMock.swift */; }; - 937173F52C8049E0009D5191 /* InfoService+Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 937173F42C8049E0009D5191 /* InfoService+Constants.swift */; }; - 9371E561295CD53100438F2C /* ChatLocalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9371E560295CD53100438F2C /* ChatLocalization.swift */; }; - 93760BD72C656CF8002507C3 /* DefaultNodesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93760BD62C656CF8002507C3 /* DefaultNodesProvider.swift */; }; - 93760BDF2C65A284002507C3 /* WordList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93760BDE2C65A284002507C3 /* WordList.swift */; }; - 93760BE12C65A2F3002507C3 /* Mnemonic+extended.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93760BE02C65A2F3002507C3 /* Mnemonic+extended.swift */; }; - 93760BE22C65A424002507C3 /* english.txt in Resources */ = {isa = PBXBuildFile; fileRef = 93760BDD2C65A1FA002507C3 /* english.txt */; }; - 937612712CC4C3CA0036EEB4 /* TransactionsStatusActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 937612702CC4C3CA0036EEB4 /* TransactionsStatusActor.swift */; }; - 937736822B0949C500B35C7A /* NodeCell+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 937736812B0949C500B35C7A /* NodeCell+Model.swift */; }; 937751A52A68B3320054BD65 /* CommonKit in Frameworks */ = {isa = PBXBuildFile; productRef = 937751A42A68B3320054BD65 /* CommonKit */; }; 937751A72A68B33A0054BD65 /* CommonKit in Frameworks */ = {isa = PBXBuildFile; productRef = 937751A62A68B33A0054BD65 /* CommonKit */; }; 937751A92A68B3400054BD65 /* CommonKit in Frameworks */ = {isa = PBXBuildFile; productRef = 937751A82A68B3400054BD65 /* CommonKit */; }; - 937751AB2A68BB390054BD65 /* ChatTransactionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 937751AA2A68BB390054BD65 /* ChatTransactionCell.swift */; }; - 937751AD2A68BCE10054BD65 /* MessageCellWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 937751AC2A68BCE10054BD65 /* MessageCellWrapper.swift */; }; - 93775E462A674FA9009061AC /* Markdown+Adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93775E452A674FA9009061AC /* Markdown+Adamant.swift */; }; - 9377FBDF296C2A2F00C9211B /* ChatTransactionContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9377FBDE296C2A2F00C9211B /* ChatTransactionContentView.swift */; }; - 9377FBE2296C2ACA00C9211B /* ChatTransactionContentView+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9377FBE1296C2ACA00C9211B /* ChatTransactionContentView+Model.swift */; }; - 937EDFC02C9CF6B300F219BB /* VersionFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 937EDFBF2C9CF6B300F219BB /* VersionFooterView.swift */; }; - 9380EF632D1119DD006939E1 /* ChatSwipeWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9380EF622D1119DD006939E1 /* ChatSwipeWrapper.swift */; }; - 9380EF662D111BD1006939E1 /* ChatSwipeWrapperModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9380EF652D111BD1006939E1 /* ChatSwipeWrapperModel.swift */; }; - 9380EF682D112BB9006939E1 /* ChatSwipeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9380EF672D112BB9006939E1 /* ChatSwipeManager.swift */; }; - 9382F61329DEC0A3005E6216 /* ChatModelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9382F61229DEC0A3005E6216 /* ChatModelView.swift */; }; 938F7D582955C1DA001915CA /* MessageKit in Frameworks */ = {isa = PBXBuildFile; productRef = 938F7D572955C1DA001915CA /* MessageKit */; }; - 938F7D5B2955C8DA001915CA /* ChatDisplayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 938F7D5A2955C8DA001915CA /* ChatDisplayManager.swift */; }; - 938F7D5D2955C8F9001915CA /* ChatLayoutManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 938F7D5C2955C8F9001915CA /* ChatLayoutManager.swift */; }; - 938F7D5F2955C90D001915CA /* ChatInputBarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 938F7D5E2955C90D001915CA /* ChatInputBarManager.swift */; }; - 938F7D612955C92B001915CA /* ChatDataSourceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 938F7D602955C92B001915CA /* ChatDataSourceManager.swift */; }; - 938F7D642955C94F001915CA /* ChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 938F7D632955C94F001915CA /* ChatViewController.swift */; }; - 938F7D662955C966001915CA /* ChatInputBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 938F7D652955C966001915CA /* ChatInputBar.swift */; }; - 938F7D692955C9EC001915CA /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 938F7D682955C9EC001915CA /* ChatViewModel.swift */; }; - 938F7D722955CE72001915CA /* ChatFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 938F7D712955CE72001915CA /* ChatFactory.swift */; }; - 9390C5032976B42800270CDF /* ChatDialogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9390C5022976B42800270CDF /* ChatDialogManager.swift */; }; - 9390C5052976B53000270CDF /* ChatDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9390C5042976B53000270CDF /* ChatDialog.swift */; }; - 93996A972968209C008D080B /* ChatMessagesCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93996A962968209C008D080B /* ChatMessagesCollection.swift */; }; - 9399F5ED29A85A48006C3E30 /* ChatCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9399F5EC29A85A48006C3E30 /* ChatCacheService.swift */; }; - 939FA3422B0D6F0000710EC6 /* SelfRemovableHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 939FA3412B0D6F0000710EC6 /* SelfRemovableHostingController.swift */; }; - 93A118512993167500E144CC /* ChatMessageBackgroundColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A118502993167500E144CC /* ChatMessageBackgroundColor.swift */; }; - 93A118532993241D00E144CC /* ChatMessagesListFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A118522993241D00E144CC /* ChatMessagesListFactory.swift */; }; - 93A18C862AAEACC100D0AB98 /* AdamantWalletFactoryCompose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A18C852AAEACC100D0AB98 /* AdamantWalletFactoryCompose.swift */; }; - 93A18C892AAEAE7700D0AB98 /* WalletFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A18C882AAEAE7700D0AB98 /* WalletFactory.swift */; }; - 93A2A8602CB4733800DBC75E /* MainThreadAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A2A85F2CB4733800DBC75E /* MainThreadAssembly.swift */; }; - 93A2A8662CB4A1EE00DBC75E /* UnsafeSendableExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A2A8652CB4A1EE00DBC75E /* UnsafeSendableExtensions.swift */; }; - 93A91FD1297972B7001DB1F8 /* ChatScrollButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A91FD0297972B7001DB1F8 /* ChatScrollButton.swift */; }; - 93A91FD329799298001DB1F8 /* ChatStartPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A91FD229799298001DB1F8 /* ChatStartPosition.swift */; }; - 93ADE0712ACA66AF008ED641 /* VibrationSelectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93ADE06E2ACA66AF008ED641 /* VibrationSelectionViewModel.swift */; }; - 93ADE0722ACA66AF008ED641 /* VibrationSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93ADE06F2ACA66AF008ED641 /* VibrationSelectionView.swift */; }; - 93ADE0732ACA66AF008ED641 /* VibrationSelectionFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93ADE0702ACA66AF008ED641 /* VibrationSelectionFactory.swift */; }; - 93B28EC22B076D31007F268B /* DashApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93B28EC12B076D31007F268B /* DashApiService.swift */; }; - 93B28EC52B076E2C007F268B /* DashBlockchainInfoDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93B28EC42B076E2C007F268B /* DashBlockchainInfoDTO.swift */; }; - 93B28EC82B076E68007F268B /* DashResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93B28EC72B076E68007F268B /* DashResponseDTO.swift */; }; - 93B28ECA2B076E88007F268B /* DashErrorDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93B28EC92B076E88007F268B /* DashErrorDTO.swift */; }; - 93BF4A6629E4859900505CD0 /* DelegatesBottomPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93BF4A6529E4859900505CD0 /* DelegatesBottomPanel.swift */; }; - 93BF4A6C29E4B4BF00505CD0 /* DelegatesBottomPanel+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93BF4A6B29E4B4BF00505CD0 /* DelegatesBottomPanel+Model.swift */; }; - 93C794442B07725C00408826 /* DashGetAddressBalanceDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93C794432B07725C00408826 /* DashGetAddressBalanceDTO.swift */; }; - 93C794482B0778C700408826 /* DashGetBlockDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93C794472B0778C700408826 /* DashGetBlockDTO.swift */; }; - 93C7944A2B077A1C00408826 /* DashGetUnspentTransactionsDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93C794492B077A1C00408826 /* DashGetUnspentTransactionsDTO.swift */; }; - 93C7944C2B077B2700408826 /* DashGetAddressTransactionIds.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93C7944B2B077B2700408826 /* DashGetAddressTransactionIds.swift */; }; - 93C7944E2B077C1F00408826 /* DashSendRawTransactionDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93C7944D2B077C1F00408826 /* DashSendRawTransactionDTO.swift */; }; - 93CC8DC7296F00D6003772BF /* ChatTransactionContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CC8DC6296F00D6003772BF /* ChatTransactionContainerView.swift */; }; - 93CC8DC9296F01DE003772BF /* ChatTransactionContainerView+Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CC8DC8296F01DE003772BF /* ChatTransactionContainerView+Model.swift */; }; - 93CC94C12B17EE73004842AC /* EthApiCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CC94C02B17EE73004842AC /* EthApiCore.swift */; }; - 93CCAE792B06D81D00EA5B94 /* DogeApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CCAE782B06D81D00EA5B94 /* DogeApiService.swift */; }; - 93CCAE7B2B06D9B500EA5B94 /* DogeBlocksDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CCAE7A2B06D9B500EA5B94 /* DogeBlocksDTO.swift */; }; - 93CCAE7E2B06DA6C00EA5B94 /* DogeBlockDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CCAE7D2B06DA6C00EA5B94 /* DogeBlockDTO.swift */; }; - 93CCAE802B06E2D100EA5B94 /* ApiServiceError+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CCAE7F2B06E2D100EA5B94 /* ApiServiceError+Extension.swift */; }; - 93E1232F2A6DF8EF004DF33B /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 93E123312A6DF8EF004DF33B /* InfoPlist.strings */; }; - 93E123382A6DFD15004DF33B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 93E1233A2A6DFD15004DF33B /* Localizable.strings */; }; - 93E1233F2A6DFE24004DF33B /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 93E123412A6DFE24004DF33B /* Localizable.stringsdict */; }; - 93E123442A6DFECB004DF33B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 93E1233A2A6DFD15004DF33B /* Localizable.strings */; }; - 93E123452A6DFECB004DF33B /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 93E123412A6DFE24004DF33B /* Localizable.stringsdict */; }; - 93E123462A6DFECB004DF33B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 93E1233A2A6DFD15004DF33B /* Localizable.strings */; }; - 93E123472A6DFECB004DF33B /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 93E123412A6DFE24004DF33B /* Localizable.stringsdict */; }; - 93E123482A6DFECC004DF33B /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 93E1233A2A6DFD15004DF33B /* Localizable.strings */; }; - 93E123492A6DFECC004DF33B /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 93E123412A6DFE24004DF33B /* Localizable.stringsdict */; }; - 93E1234B2A6DFEF7004DF33B /* NotificationStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E1234A2A6DFEF7004DF33B /* NotificationStrings.swift */; }; - 93E1234C2A6DFF62004DF33B /* NotificationStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E1234A2A6DFEF7004DF33B /* NotificationStrings.swift */; }; - 93E1234D2A6DFF62004DF33B /* NotificationStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E1234A2A6DFEF7004DF33B /* NotificationStrings.swift */; }; - 93E1234E2A6DFF62004DF33B /* NotificationStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93E1234A2A6DFEF7004DF33B /* NotificationStrings.swift */; }; - 93ED214A2CC3555500AA1FC8 /* TxStatusServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93ED21492CC3555500AA1FC8 /* TxStatusServiceProtocol.swift */; }; - 93ED214C2CC3561800AA1FC8 /* TransactionsStatusServiceComposeProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93ED214B2CC3561800AA1FC8 /* TransactionsStatusServiceComposeProtocol.swift */; }; - 93ED214F2CC3567600AA1FC8 /* TransactionsStatusServiceCompose.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93ED214E2CC3567600AA1FC8 /* TransactionsStatusServiceCompose.swift */; }; - 93ED21512CC3571200AA1FC8 /* TxStatusService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93ED21502CC3571200AA1FC8 /* TxStatusService.swift */; }; - 93F391502962F5D400BFD6AE /* SpinnerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93F3914F2962F5D400BFD6AE /* SpinnerView.swift */; }; 93FA403629401BFC00D20DB6 /* PopupKit in Frameworks */ = {isa = PBXBuildFile; productRef = 93FA403529401BFC00D20DB6 /* PopupKit */; }; - 93FC169B2B0197FD0062B507 /* BtcApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93FC169A2B0197FD0062B507 /* BtcApiService.swift */; }; - 93FC169D2B019F440062B507 /* EthApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93FC169C2B019F440062B507 /* EthApiService.swift */; }; - 93FC16A12B01DE120062B507 /* ERC20ApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93FC16A02B01DE120062B507 /* ERC20ApiService.swift */; }; - A50A41082822F8CE006BDFE1 /* BtcWalletService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50A41042822F8CE006BDFE1 /* BtcWalletService.swift */; }; - A50A41092822F8CE006BDFE1 /* BtcWalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50A41052822F8CE006BDFE1 /* BtcWalletViewController.swift */; }; - A50A410A2822F8CE006BDFE1 /* BtcWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50A41062822F8CE006BDFE1 /* BtcWallet.swift */; }; - A50A41112822FC35006BDFE1 /* BtcWalletService+RichMessageProviderWithStatusCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50A410D2822FC35006BDFE1 /* BtcWalletService+RichMessageProviderWithStatusCheck.swift */; }; - A50A41122822FC35006BDFE1 /* BtcWalletService+RichMessageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50A410E2822FC35006BDFE1 /* BtcWalletService+RichMessageProvider.swift */; }; - A50A41132822FC35006BDFE1 /* BtcWalletService+Send.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50A410F2822FC35006BDFE1 /* BtcWalletService+Send.swift */; }; - A50A41142822FC35006BDFE1 /* BtcTransferViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50A41102822FC35006BDFE1 /* BtcTransferViewController.swift */; }; A50AEB04262C815200B37C22 /* EFQRCode in Frameworks */ = {isa = PBXBuildFile; productRef = A50AEB03262C815200B37C22 /* EFQRCode */; }; A50AEB0C262C81E300B37C22 /* QRCodeReader in Frameworks */ = {isa = PBXBuildFile; productRef = A50AEB0B262C81E300B37C22 /* QRCodeReader */; }; A5241B70262DEDE1009FA43E /* Clibsodium in Frameworks */ = {isa = PBXBuildFile; productRef = A5241B6F262DEDE1009FA43E /* Clibsodium */; }; @@ -444,210 +38,28 @@ A57282D3262C94DF00C96FA8 /* DateToolsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = A57282D2262C94DF00C96FA8 /* DateToolsSwift */; }; A57282D5262C94E500C96FA8 /* DateToolsSwift in Frameworks */ = {isa = PBXBuildFile; productRef = A57282D4262C94E500C96FA8 /* DateToolsSwift */; }; A5785ADF262C63580001BC66 /* web3swift in Frameworks */ = {isa = PBXBuildFile; productRef = A5785ADE262C63580001BC66 /* web3swift */; }; - A578BDE52623051C00090141 /* DashWalletService+Transactions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A578BDE42623051C00090141 /* DashWalletService+Transactions.swift */; }; A5AC8DFF262E0B030053A7E2 /* SipHash in Frameworks */ = {isa = PBXBuildFile; productRef = A5AC8DFE262E0B030053A7E2 /* SipHash */; }; A5AC8E00262E0B030053A7E2 /* SipHash in Embed Frameworks */ = {isa = PBXBuildFile; productRef = A5AC8DFE262E0B030053A7E2 /* SipHash */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; A5C99E0E262C9E3A00F7B1B7 /* Reachability in Frameworks */ = {isa = PBXBuildFile; productRef = A5C99E0D262C9E3A00F7B1B7 /* Reachability */; }; A5D87BA3262CA01D00DC28F0 /* ProcedureKit in Frameworks */ = {isa = PBXBuildFile; productRef = A5D87BA2262CA01D00DC28F0 /* ProcedureKit */; }; A5DBBABD262C7221004AC028 /* Clibsodium in Frameworks */ = {isa = PBXBuildFile; productRef = A5DBBABC262C7221004AC028 /* Clibsodium */; }; - A5DBBADC262C729B004AC028 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = A5DBBADB262C729B004AC028 /* CryptoSwift */; }; - A5DBBAE3262C72B0004AC028 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = A5DBBAE2262C72B0004AC028 /* CryptoSwift */; }; - A5DBBAE5262C72B7004AC028 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = A5DBBAE4262C72B7004AC028 /* CryptoSwift */; }; - A5DBBAE7262C72BD004AC028 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = A5DBBAE6262C72BD004AC028 /* CryptoSwift */; }; A5DBBAEE262C72EF004AC028 /* BitcoinKit in Frameworks */ = {isa = PBXBuildFile; productRef = A5DBBAED262C72EF004AC028 /* BitcoinKit */; }; A5DBBAF0262C72EF004AC028 /* LiskKit in Frameworks */ = {isa = PBXBuildFile; productRef = A5DBBAEF262C72EF004AC028 /* LiskKit */; }; - A5E04224282A830B0076CD13 /* BtcTransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64B5736C2201E196005DC968 /* BtcTransactionsViewController.swift */; }; - A5E04227282A8BDC0076CD13 /* BtcBalanceResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E04226282A8BDC0076CD13 /* BtcBalanceResponse.swift */; }; - A5E04229282A998C0076CD13 /* BtcTransactionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E04228282A998C0076CD13 /* BtcTransactionResponse.swift */; }; - A5E0422B282AB18B0076CD13 /* BtcUnspentTransactionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E0422A282AB18B0076CD13 /* BtcUnspentTransactionResponse.swift */; }; A5F0A04B262C9CA90009672A /* Swinject in Frameworks */ = {isa = PBXBuildFile; productRef = A5F0A04A262C9CA90009672A /* Swinject */; }; A5F92994262C855B00C3E60A /* MarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = A5F92993262C855B00C3E60A /* MarkdownKit */; }; A5F929AF262C857D00C3E60A /* MarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = A5F929AE262C857D00C3E60A /* MarkdownKit */; }; A5F929B6262C858700C3E60A /* MarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = A5F929B5262C858700C3E60A /* MarkdownKit */; }; A5F929B8262C858F00C3E60A /* MarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = A5F929B7262C858F00C3E60A /* MarkdownKit */; }; - E90055F520EBF5DA00D0CB2D /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055F420EBF5DA00D0CB2D /* AboutViewController.swift */; }; - E90055F720EC200900D0CB2D /* SecurityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055F620EC200900D0CB2D /* SecurityViewController.swift */; }; - E90055F920ECD86800D0CB2D /* SecurityViewController+StayIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055F820ECD86800D0CB2D /* SecurityViewController+StayIn.swift */; }; - E90055FB20ECE78A00D0CB2D /* SecurityViewController+notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90055FA20ECE78A00D0CB2D /* SecurityViewController+notifications.swift */; }; - E905D39F204C281400DDB504 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E905D39E204C281400DDB504 /* LoginViewController.swift */; }; - E9061B97207501E40011F104 /* AdamantUserInfoKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9061B96207501E40011F104 /* AdamantUserInfoKey.swift */; }; - E907350E2256779C00BF02CC /* DogeMainnet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E907350D2256779C00BF02CC /* DogeMainnet.swift */; }; + AA8FFFCA2D4E6435001D8576 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = AA8FFFC92D4E6435001D8576 /* CryptoSwift */; }; + B18F4D712D986CD300F6917F /* SwiftyMockyXCTest in Frameworks */ = {isa = PBXBuildFile; productRef = B18F4D702D986CD300F6917F /* SwiftyMockyXCTest */; }; + B1C2BA722D90BF34001FE840 /* SwiftyMocky in Frameworks */ = {isa = PBXBuildFile; productRef = B1C2BA712D90BF34001FE840 /* SwiftyMocky */; }; E9079A7C229DEF9C0022CA0D /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E957E110229B04280019732A /* UserNotifications.framework */; }; E9079A7D229DEF9C0022CA0D /* UserNotificationsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E957E112229B04280019732A /* UserNotificationsUI.framework */; }; - E9079A80229DEF9C0022CA0D /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9079A7F229DEF9C0022CA0D /* NotificationViewController.swift */; }; - E9079A83229DEF9C0022CA0D /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E9079A81229DEF9C0022CA0D /* MainInterface.storyboard */; }; E9079A87229DEF9C0022CA0D /* MessageNotificationContentExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = E9079A7B229DEF9C0022CA0D /* MessageNotificationContentExtension.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - E908471B2196FE590095825D /* Adamant.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = E90847192196FE590095825D /* Adamant.xcdatamodeld */; }; - E908472A2196FEA80095825D /* RichMessageTransaction+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E908471C2196FEA80095825D /* RichMessageTransaction+CoreDataClass.swift */; }; - E908472B2196FEA80095825D /* RichMessageTransaction+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = E908471D2196FEA80095825D /* RichMessageTransaction+CoreDataProperties.swift */; }; - E908472C2196FEA80095825D /* CoreDataAccount+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E908471E2196FEA80095825D /* CoreDataAccount+CoreDataClass.swift */; }; - E908472D2196FEA80095825D /* CoreDataAccount+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = E908471F2196FEA80095825D /* CoreDataAccount+CoreDataProperties.swift */; }; - E908472E2196FEA80095825D /* BaseTransaction+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90847202196FEA80095825D /* BaseTransaction+CoreDataClass.swift */; }; - E908472F2196FEA80095825D /* BaseTransaction+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90847212196FEA80095825D /* BaseTransaction+CoreDataProperties.swift */; }; - E90847302196FEA80095825D /* ChatTransaction+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90847222196FEA80095825D /* ChatTransaction+CoreDataClass.swift */; }; - E90847312196FEA80095825D /* ChatTransaction+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90847232196FEA80095825D /* ChatTransaction+CoreDataProperties.swift */; }; - E90847322196FEA80095825D /* TransferTransaction+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90847242196FEA80095825D /* TransferTransaction+CoreDataClass.swift */; }; - E90847332196FEA80095825D /* TransferTransaction+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90847252196FEA80095825D /* TransferTransaction+CoreDataProperties.swift */; }; - E90847342196FEA80095825D /* MessageTransaction+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90847262196FEA80095825D /* MessageTransaction+CoreDataClass.swift */; }; - E90847352196FEA80095825D /* MessageTransaction+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90847272196FEA80095825D /* MessageTransaction+CoreDataProperties.swift */; }; - E90847362196FEA80095825D /* Chatroom+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90847282196FEA80095825D /* Chatroom+CoreDataClass.swift */; }; - E90847372196FEA80095825D /* Chatroom+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90847292196FEA80095825D /* Chatroom+CoreDataProperties.swift */; }; - E90847392196FEF50095825D /* BaseTransaction+TransactionDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90847382196FEF50095825D /* BaseTransaction+TransactionDetails.swift */; }; - E908473B219707200095825D /* AccountViewController+StayIn.swift in Sources */ = {isa = PBXBuildFile; fileRef = E908473A219707200095825D /* AccountViewController+StayIn.swift */; }; - E90A4943204C5ED6009F6A65 /* EurekaPassphraseRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90A4942204C5ED6009F6A65 /* EurekaPassphraseRow.swift */; }; - E90A4945204C6204009F6A65 /* PassphraseCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = E90A4944204C5F60009F6A65 /* PassphraseCell.xib */; }; - E90A494B204D9EB8009F6A65 /* AdamantAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90A494A204D9EB8009F6A65 /* AdamantAuthentication.swift */; }; - E90A494D204DA932009F6A65 /* LocalAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = E90A494C204DA932009F6A65 /* LocalAuthentication.swift */; }; - E90EA5C321BA8BF400A2CE25 /* DelegateDetailsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E90EA5C221BA8BF400A2CE25 /* DelegateDetailsViewController.xib */; }; - E913C8F21FFFA51D001A83F7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E913C8F11FFFA51D001A83F7 /* AppDelegate.swift */; }; - E913C8F91FFFA51D001A83F7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E913C8F81FFFA51D001A83F7 /* Assets.xcassets */; }; - E9147B5F20500E9300145913 /* MyLittlePinpad+adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9147B5E20500E9300145913 /* MyLittlePinpad+adamant.swift */; }; - E9147B612050599000145913 /* LoginViewController+QR.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9147B602050599000145913 /* LoginViewController+QR.swift */; }; - E9147B6320505C7500145913 /* QRCodeReader+adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9147B6220505C7500145913 /* QRCodeReader+adamant.swift */; }; - E9147B6F205088DE00145913 /* LoginViewController+Pinpad.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9147B6E205088DE00145913 /* LoginViewController+Pinpad.swift */; }; - E91E5BF220DAF05500B06B3C /* NodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E91E5BF120DAF05500B06B3C /* NodeCell.swift */; }; - E9204B5220C9762400F3B9AB /* MessageStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9204B5120C9762300F3B9AB /* MessageStatus.swift */; }; - E921534E20EE1E8700C0843F /* EurekaAlertLabelRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E921534C20EE1E8700C0843F /* EurekaAlertLabelRow.swift */; }; - E921534F20EE1E8700C0843F /* AlertLabelCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = E921534D20EE1E8700C0843F /* AlertLabelCell.xib */; }; - E9215973206119FB0000CA5C /* ReachabilityMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9215972206119FB0000CA5C /* ReachabilityMonitor.swift */; }; - E921597520611A6A0000CA5C /* AdamantReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = E921597420611A6A0000CA5C /* AdamantReachability.swift */; }; - E921597B206503000000CA5C /* ButtonsStripeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E921597A206503000000CA5C /* ButtonsStripeView.swift */; }; - E921597D2065031D0000CA5C /* ButtonsStripe.xib in Resources */ = {isa = PBXBuildFile; fileRef = E921597C2065031D0000CA5C /* ButtonsStripe.xib */; }; - E923222621135F9000A7E5AF /* EthAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = E923222521135F9000A7E5AF /* EthAccount.swift */; }; - E9240BF5215D686500187B09 /* AdmWalletService+RichMessageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9240BF4215D686500187B09 /* AdmWalletService+RichMessageProvider.swift */; }; - E9256F5F2034C21100DE86E9 /* String+localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9256F5E2034C21100DE86E9 /* String+localized.swift */; }; - E9256F762039A9A200DE86E9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E9256F752039A9A200DE86E9 /* LaunchScreen.storyboard */; }; - E926E02E213EAABF005E536B /* TransferViewControllerBase+Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = E926E02D213EAABF005E536B /* TransferViewControllerBase+Alert.swift */; }; - E926E032213EC43B005E536B /* FullscreenAlertView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E926E031213EC43B005E536B /* FullscreenAlertView.swift */; }; - E9332B8921F1FA4400D56E72 /* OnboardFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9332B8821F1FA4400D56E72 /* OnboardFactory.swift */; }; - E933475B225539390083839E /* DogeGetTransactionsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E933475A225539390083839E /* DogeGetTransactionsResponse.swift */; }; - E9393FAA2055D03300EE6F30 /* AdamantMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9393FA92055D03300EE6F30 /* AdamantMessage.swift */; }; - E93B0D742028B21400126346 /* ChatsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E93B0D732028B21400126346 /* ChatsProvider.swift */; }; - E93B0D762028B28E00126346 /* AdamantChatsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E93B0D752028B28E00126346 /* AdamantChatsProvider.swift */; }; - E93D7ABE2052CEE1005D19DC /* NotificationsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E93D7ABD2052CEE1005D19DC /* NotificationsService.swift */; }; - E93D7AC02052CF63005D19DC /* AdamantNotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E93D7ABF2052CF63005D19DC /* AdamantNotificationService.swift */; }; - E93EB09F20DA3FA4001F9601 /* NodesEditorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E93EB09E20DA3FA4001F9601 /* NodesEditorFactory.swift */; }; - E940086B2114A70600CD2D67 /* LskAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = E940086A2114A70600CD2D67 /* LskAccount.swift */; }; - E940086E2114AA2E00CD2D67 /* WalletCoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E940086D2114AA2E00CD2D67 /* WalletCoreProtocol.swift */; }; - E94008722114EACF00CD2D67 /* WalletAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = E94008712114EACF00CD2D67 /* WalletAccount.swift */; }; - E940087B2114ED0600CD2D67 /* EthWalletService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E940087A2114ED0600CD2D67 /* EthWalletService.swift */; }; - E940087D2114EDEE00CD2D67 /* EthWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E940087C2114EDEE00CD2D67 /* EthWallet.swift */; }; - E94008872114F05B00CD2D67 /* AddressValidationResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = E94008862114F05B00CD2D67 /* AddressValidationResult.swift */; }; - E940088B2114F63000CD2D67 /* NSRegularExpression+adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = E940088A2114F63000CD2D67 /* NSRegularExpression+adamant.swift */; }; - E940088F2119A9E800CD2D67 /* BigInt+Decimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = E940088E2119A9E800CD2D67 /* BigInt+Decimal.swift */; }; - E941CCDB20E786D800C96220 /* AccountHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = E941CCDA20E786D700C96220 /* AccountHeader.xib */; }; - E941CCDE20E7B70200C96220 /* WalletCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E941CCDC20E7B70200C96220 /* WalletCollectionViewCell.swift */; }; - E941CCDF20E7B70200C96220 /* WalletCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = E941CCDD20E7B70200C96220 /* WalletCollectionViewCell.xib */; }; - E9484B79227C617E008E10F0 /* BalanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9484B77227C617D008E10F0 /* BalanceTableViewCell.swift */; }; - E9484B7A227CA93B008E10F0 /* BalanceTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = E9484B78227C617E008E10F0 /* BalanceTableViewCell.xib */; }; - E9484B7D2285BAD9008E10F0 /* PrivateKeyGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9484B7C2285BAD8008E10F0 /* PrivateKeyGenerator.swift */; }; - E94883E7203F07CD00F6E1B0 /* PassphraseValidation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E94883E6203F07CD00F6E1B0 /* PassphraseValidation.swift */; }; - E948E03B20235E2300975D6B /* SettingsFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E948E03A20235E2300975D6B /* SettingsFactory.swift */; }; - E94E7B01205D3F090042B639 /* ChatListViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E94E7B00205D3F090042B639 /* ChatListViewController.xib */; }; - E94E7B08205D4CB80042B639 /* ShareQRFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E94E7B07205D4CB80042B639 /* ShareQRFactory.swift */; }; - E94E7B0C205D5E4A0042B639 /* TransactionsListViewControllerBase.xib in Resources */ = {isa = PBXBuildFile; fileRef = E94E7B0B205D5E4A0042B639 /* TransactionsListViewControllerBase.xib */; }; - E9502740202E257E002C1098 /* RepeaterService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E950273F202E257E002C1098 /* RepeaterService.swift */; }; - E950652120404BF0008352E5 /* AdamantUriBuilding.swift in Sources */ = {isa = PBXBuildFile; fileRef = E950652020404BF0008352E5 /* AdamantUriBuilding.swift */; }; - E950652320404C84008352E5 /* AdamantUriTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = E950652220404C84008352E5 /* AdamantUriTools.swift */; }; - E957E107229AF7CB0019732A /* adamant_notificationContent.png in Resources */ = {isa = PBXBuildFile; fileRef = E957E103229AF7CA0019732A /* adamant_notificationContent.png */; }; - E957E108229AF7CB0019732A /* doge_notificationContent.png in Resources */ = {isa = PBXBuildFile; fileRef = E957E104229AF7CA0019732A /* doge_notificationContent.png */; }; - E957E109229AF7CB0019732A /* lisk_notificationContent.png in Resources */ = {isa = PBXBuildFile; fileRef = E957E105229AF7CA0019732A /* lisk_notificationContent.png */; }; - E957E10A229AF7CB0019732A /* ethereum_notificationContent.png in Resources */ = {isa = PBXBuildFile; fileRef = E957E106229AF7CB0019732A /* ethereum_notificationContent.png */; }; E957E12E229B10F80019732A /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E957E110229B04280019732A /* UserNotifications.framework */; }; E957E12F229B10F80019732A /* UserNotificationsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E957E112229B04280019732A /* UserNotificationsUI.framework */; }; - E957E132229B10F80019732A /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E957E131229B10F80019732A /* NotificationViewController.swift */; }; - E957E135229B10F80019732A /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E957E133229B10F80019732A /* MainInterface.storyboard */; }; E957E139229B10F80019732A /* TransferNotificationContentExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = E957E12D229B10F80019732A /* TransferNotificationContentExtension.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - E95F85712007D98D0070534A /* CurrencyFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85702007D98D0070534A /* CurrencyFormatterTests.swift */; }; - E95F85752007E4790070534A /* HexAndBytesUtilitiesTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85742007E4790070534A /* HexAndBytesUtilitiesTest.swift */; }; - E95F85802008C8D70070534A /* ChatListFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F857E2008C8D60070534A /* ChatListFactory.swift */; }; - E95F85852008CB3A0070534A /* ChatListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85842008CB3A0070534A /* ChatListViewController.swift */; }; - E95F85B7200A4D8F0070534A /* TestTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85B6200A4D8F0070534A /* TestTools.swift */; }; - E95F85BC200A4E670070534A /* ParsingModelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85BB200A4E670070534A /* ParsingModelsTests.swift */; }; - E95F85C0200A51BB0070534A /* Account.json in Resources */ = {isa = PBXBuildFile; fileRef = E95F85BF200A51BB0070534A /* Account.json */; }; - E95F85C7200A9B070070534A /* ChatTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85C5200A9B070070534A /* ChatTableViewCell.swift */; }; - E95F85C8200A9B070070534A /* ChatTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = E95F85C6200A9B070070534A /* ChatTableViewCell.xib */; }; - E96BBE3121F70F5E009AA738 /* ReadonlyTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E96BBE3021F70F5E009AA738 /* ReadonlyTextView.swift */; }; - E96BBE3321F71290009AA738 /* BuyAndSellViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E96BBE3221F71290009AA738 /* BuyAndSellViewController.swift */; }; - E96D64BE2295C06400CA5587 /* JSAdamantCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9220E0221983155009C9642 /* JSAdamantCore.swift */; }; - E96D64BF2295C06400CA5587 /* adamant-core.js in Resources */ = {isa = PBXBuildFile; fileRef = E9220E0121983155009C9642 /* adamant-core.js */; }; - E96D64C02295C06400CA5587 /* JSModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F856A200789450070534A /* JSModels.swift */; }; - E96D64C12295C06400CA5587 /* JSAdamantCoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85762007E8EC0070534A /* JSAdamantCoreTests.swift */; }; - E96D64C22295C06400CA5587 /* NativeCoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9220E07219879B9009C9642 /* NativeCoreTests.swift */; }; - E96D64C82295C44400CA5587 /* Data+utilites.swift in Sources */ = {isa = PBXBuildFile; fileRef = E96D64C72295C44400CA5587 /* Data+utilites.swift */; }; - E96D64CF2295C82B00CA5587 /* Chat.json in Resources */ = {isa = PBXBuildFile; fileRef = E95F85C3200A540B0070534A /* Chat.json */; }; - E96D64D02295C82B00CA5587 /* NormalizedTransaction.json in Resources */ = {isa = PBXBuildFile; fileRef = E95F85C1200A53E90070534A /* NormalizedTransaction.json */; }; - E96D64D12295C82B00CA5587 /* TransactionChat.json in Resources */ = {isa = PBXBuildFile; fileRef = E95F85BD200A503A0070534A /* TransactionChat.json */; }; - E96D64D22295C82B00CA5587 /* TransactionSend.json in Resources */ = {isa = PBXBuildFile; fileRef = E95F85B9200A4DC90070534A /* TransactionSend.json */; }; - E96D64DE2295CD4700CA5587 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E96D64DD2295CD4700CA5587 /* NotificationService.swift */; }; E96D64E22295CD4700CA5587 /* NotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = E96D64DB2295CD4700CA5587 /* NotificationServiceExtension.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - E96E86B821679C120061F80A /* EthTransactionDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E96E86B721679C120061F80A /* EthTransactionDetailsViewController.swift */; }; - E971591A21681D6900A5F904 /* TransactionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E971591921681D6900A5F904 /* TransactionStatus.swift */; }; - E971591C2168209800A5F904 /* EthWalletService+RichMessageProviderWithStatusCheck.swift in Sources */ = {isa = PBXBuildFile; fileRef = E971591B2168209800A5F904 /* EthWalletService+RichMessageProviderWithStatusCheck.swift */; }; - E9722066201F42BB004F2AAD /* CoreDataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9722065201F42BB004F2AAD /* CoreDataStack.swift */; }; - E9722068201F42CC004F2AAD /* InMemoryCoreDataStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9722067201F42CC004F2AAD /* InMemoryCoreDataStack.swift */; }; - E972206B201F44CA004F2AAD /* TransfersProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E972206A201F44CA004F2AAD /* TransfersProvider.swift */; }; - E9771DA722997F310099AAC7 /* ServerResponseWithTimestamp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9771DA622997F310099AAC7 /* ServerResponseWithTimestamp.swift */; }; - E983AE2120E655C500497E1A /* AccountHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E983AE2020E655C500497E1A /* AccountHeaderView.swift */; }; - E983AE2A20E65F3200497E1A /* AccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E983AE2820E65F3200497E1A /* AccountViewController.swift */; }; - E983AE2D20E6720D00497E1A /* AccountFooter.xib in Resources */ = {isa = PBXBuildFile; fileRef = E983AE2C20E6720D00497E1A /* AccountFooter.xib */; }; - E987024920C2B1F700E393F4 /* AdamantChatsProvider+fakeMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = E987024820C2B1F700E393F4 /* AdamantChatsProvider+fakeMessages.swift */; }; - E98FC34420F920BD00032D65 /* UIFont+adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98FC34320F920BD00032D65 /* UIFont+adamant.swift */; }; - E993301E212EF39700CD5200 /* EthTransferViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E993301D212EF39700CD5200 /* EthTransferViewController.swift */; }; - E993302021354B1800CD5200 /* AdmWalletFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E993301F21354B1800CD5200 /* AdmWalletFactory.swift */; }; - E993302221354BC300CD5200 /* EthWalletFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E993302121354BC300CD5200 /* EthWalletFactory.swift */; }; - E99330262136B0E500CD5200 /* TransferViewControllerBase+QR.swift in Sources */ = {isa = PBXBuildFile; fileRef = E99330252136B0E500CD5200 /* TransferViewControllerBase+QR.swift */; }; - E9942B80203C058C00C163AF /* QRGeneratorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9942B7F203C058C00C163AF /* QRGeneratorViewController.swift */; }; - E9942B84203CBFCE00C163AF /* AdamantQRTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9942B83203CBFCE00C163AF /* AdamantQRTools.swift */; }; - E9942B87203D9E5100C163AF /* EurekaQRRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9942B86203D9E5100C163AF /* EurekaQRRow.swift */; }; - E9942B89203D9ECA00C163AF /* QrCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = E9942B88203D9ECA00C163AF /* QrCell.xib */; }; - E9960B3321F5154300C840A8 /* BaseAccount+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9960B2F21F5154300C840A8 /* BaseAccount+CoreDataClass.swift */; }; - E9960B3421F5154300C840A8 /* BaseAccount+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9960B3021F5154300C840A8 /* BaseAccount+CoreDataProperties.swift */; }; - E9960B3521F5154300C840A8 /* DummyAccount+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9960B3121F5154300C840A8 /* DummyAccount+CoreDataClass.swift */; }; - E9960B3621F5154300C840A8 /* DummyAccount+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9960B3221F5154300C840A8 /* DummyAccount+CoreDataProperties.swift */; }; - E99818942120892F0018C84C /* WalletViewControllerBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = E99818932120892F0018C84C /* WalletViewControllerBase.swift */; }; - E9981896212095CA0018C84C /* EthWalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9981895212095CA0018C84C /* EthWalletViewController.swift */; }; - E9981898212096ED0018C84C /* WalletViewControllerBase.xib in Resources */ = {isa = PBXBuildFile; fileRef = E9981897212096ED0018C84C /* WalletViewControllerBase.xib */; }; - E9A03FD220DBC0F2007653A1 /* NodeEditorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A03FD120DBC0F2007653A1 /* NodeEditorViewController.swift */; }; - E9A03FD420DBC824007653A1 /* NodeVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A03FD320DBC824007653A1 /* NodeVersion.swift */; }; - E9A174B32057EC47003667CD /* BackgroundFetchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A174B22057EC47003667CD /* BackgroundFetchService.swift */; }; - E9A174B52057EDCE003667CD /* AdamantTransfersProvider+backgroundFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A174B42057EDCE003667CD /* AdamantTransfersProvider+backgroundFetch.swift */; }; - E9A174B72057F1B3003667CD /* AdamantChatsProvider+backgroundFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A174B62057F1B3003667CD /* AdamantChatsProvider+backgroundFetch.swift */; }; - E9A174B920587B84003667CD /* notification.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = E9A174B820587B83003667CD /* notification.mp3 */; }; - E9AA8BF82129F13000F9249F /* ComplexTransferViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9AA8BF72129F13000F9249F /* ComplexTransferViewController.swift */; }; - E9AA8BFA212C166600F9249F /* EthWalletService+Send.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9AA8BF9212C166600F9249F /* EthWalletService+Send.swift */; }; - E9AA8C02212C5BF500F9249F /* AdmWalletService+Send.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9AA8C01212C5BF500F9249F /* AdmWalletService+Send.swift */; }; - E9B1AA572121ACC000080A2A /* AdmWalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9B1AA562121ACBF00080A2A /* AdmWalletViewController.swift */; }; - E9B1AA5B21283E0F00080A2A /* AdmTransferViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9B1AA5A21283E0F00080A2A /* AdmTransferViewController.swift */; }; - E9B3D39A201F90570019EB36 /* AccountsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9B3D399201F90570019EB36 /* AccountsProvider.swift */; }; - E9B3D39E201F99F40019EB36 /* DataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9B3D39D201F99F40019EB36 /* DataProvider.swift */; }; - E9B3D3A1201FA26B0019EB36 /* AdamantAccountsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9B3D3A0201FA26B0019EB36 /* AdamantAccountsProvider.swift */; }; - E9B3D3A9202082450019EB36 /* AdamantTransfersProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9B3D3A8202082450019EB36 /* AdamantTransfersProvider.swift */; }; - E9B4E1A8210F079E007E77FC /* DoubleDetailsTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9B4E1A7210F079E007E77FC /* DoubleDetailsTableViewCell.swift */; }; - E9B4E1AA210F1803007E77FC /* DoubleDetailsTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = E9B4E1A9210F08BE007E77FC /* DoubleDetailsTableViewCell.xib */; }; - E9C51ECF200E2D1100385EB7 /* FeeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9C51ECE200E2D1100385EB7 /* FeeTests.swift */; }; - E9C51EF12013F18000385EB7 /* NewChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9C51EF02013F18000385EB7 /* NewChatViewController.swift */; }; - E9D1BE1C211DABE100E86B72 /* WalletPagingItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D1BE1B211DABE100E86B72 /* WalletPagingItem.swift */; }; - E9DFB71C21624C9200CF8C7C /* AdmTransactionDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9DFB71B21624C9200CF8C7C /* AdmTransactionDetailsViewController.swift */; }; - E9E7CD8B20026B0600DFC4DB /* AccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CD8A20026B0600DFC4DB /* AccountService.swift */; }; - E9E7CD8D20026B6600DFC4DB /* DialogService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CD8C20026B6600DFC4DB /* DialogService.swift */; }; - E9E7CD8F20026CD300DFC4DB /* AdamantDialogService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CD8E20026CD300DFC4DB /* AdamantDialogService.swift */; }; - E9E7CD9120026FA100DFC4DB /* AppAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CD9020026FA100DFC4DB /* AppAssembly.swift */; }; - E9E7CD932002740500DFC4DB /* AdamantAccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CD922002740500DFC4DB /* AdamantAccountService.swift */; }; - E9E7CDB12002B97B00DFC4DB /* AccountFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDB02002B97B00DFC4DB /* AccountFactory.swift */; }; - E9E7CDB32002B9FB00DFC4DB /* LoginFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDB22002B9FB00DFC4DB /* LoginFactory.swift */; }; - E9E7CDB72003994E00DFC4DB /* AdamantUtilities+extended.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDB62003994E00DFC4DB /* AdamantUtilities+extended.swift */; }; - E9E7CDBE2003AEFB00DFC4DB /* CellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDBD2003AEFB00DFC4DB /* CellFactory.swift */; }; - E9E7CDC02003AF6D00DFC4DB /* AdamantCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDBF2003AF6D00DFC4DB /* AdamantCellFactory.swift */; }; - E9E7CDC22003F5A400DFC4DB /* TransactionsListViewControllerBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDC12003F5A400DFC4DB /* TransactionsListViewControllerBase.swift */; }; - E9E7CDC72003F6D200DFC4DB /* TransactionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDC52003F6D200DFC4DB /* TransactionTableViewCell.swift */; }; - E9EC342120052ABB00C0E546 /* TransferViewControllerBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9EC342020052ABB00C0E546 /* TransferViewControllerBase.swift */; }; - E9EC344720066D4A00C0E546 /* AddressValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9EC344620066D4A00C0E546 /* AddressValidationTests.swift */; }; - E9FAE5DA203DBFEF008D3A6B /* Comparable+clamped.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FAE5D9203DBFEF008D3A6B /* Comparable+clamped.swift */; }; - E9FAE5E2203ED1AE008D3A6B /* ShareQrViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FAE5E0203ED1AE008D3A6B /* ShareQrViewController.swift */; }; - E9FAE5E3203ED1AE008D3A6B /* ShareQrViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E9FAE5E1203ED1AE008D3A6B /* ShareQrViewController.xib */; }; - E9FCA1E6218334C00005E83D /* SimpleTransactionDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FCA1E5218334C00005E83D /* SimpleTransactionDetails.swift */; }; - E9FEECA62143C300007DD7C8 /* EthWalletService+RichMessageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FEECA52143C300007DD7C8 /* EthWalletService+RichMessageProvider.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -709,2136 +121,284 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 2621AB362C60E74A00046D7A /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; }; - 2621AB382C60E7AE00046D7A /* NotificationsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsViewModel.swift; sourceTree = ""; }; - 2621AB3A2C613C8100046D7A /* NotificationsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsFactory.swift; sourceTree = ""; }; - 265AA1612B74E6B900CF98B0 /* ChatPreservation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreservation.swift; sourceTree = ""; }; - 269B830F2C74A2FF002AA1D7 /* note.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = note.mp3; sourceTree = ""; }; - 269B83122C74B4EA002AA1D7 /* handoff.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = handoff.mp3; sourceTree = ""; }; - 269B83132C74B4EA002AA1D7 /* portal.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = portal.mp3; sourceTree = ""; }; - 269B83142C74B4EB002AA1D7 /* antic.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = antic.mp3; sourceTree = ""; }; - 269B83152C74B4EB002AA1D7 /* droplet.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = droplet.mp3; sourceTree = ""; }; - 269B83162C74B4EB002AA1D7 /* passage.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = passage.mp3; sourceTree = ""; }; - 269B83172C74B4EB002AA1D7 /* chord.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = chord.mp3; sourceTree = ""; }; - 269B83182C74B4EB002AA1D7 /* rattle.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = rattle.mp3; sourceTree = ""; }; - 269B83192C74B4EB002AA1D7 /* rebound.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = rebound.mp3; sourceTree = ""; }; - 269B831A2C74B4EC002AA1D7 /* milestone.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = milestone.mp3; sourceTree = ""; }; - 269B831B2C74B4EC002AA1D7 /* cheers.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = cheers.mp3; sourceTree = ""; }; - 269B831C2C74B4EC002AA1D7 /* slide.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = slide.mp3; sourceTree = ""; }; - 269B831D2C74B4EC002AA1D7 /* welcome.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = welcome.mp3; sourceTree = ""; }; - 269B83362C74D1F9002AA1D7 /* NotificationSoundsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSoundsView.swift; sourceTree = ""; }; - 269B83392C74D4AA002AA1D7 /* NotificationSoundsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSoundsViewModel.swift; sourceTree = ""; }; - 269B833C2C74E661002AA1D7 /* NotificationSoundsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSoundsFactory.swift; sourceTree = ""; }; - 269E13512B594B2D008D1CA7 /* AccountFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFooterView.swift; sourceTree = ""; }; - 26A975FE2B7E843E0095C367 /* SelectTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectTextView.swift; sourceTree = ""; }; - 26A976002B7E852E0095C367 /* ChatSelectTextViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatSelectTextViewFactory.swift; sourceTree = ""; }; 33975C0D891698AA7E74EBCC /* Pods_Adamant.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Adamant.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 36AB8CE9537B3B873972548B /* Pods_AdmCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AdmCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 3A20D93A2AE7F316005475A6 /* AdamantTransactionDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantTransactionDetails.swift; sourceTree = ""; }; - 3A2478AD2BB42967009D89E9 /* ChatDropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDropView.swift; sourceTree = ""; }; - 3A2478B02BB45DF8009D89E9 /* StorageUsageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageUsageView.swift; sourceTree = ""; }; - 3A2478B22BB461A7009D89E9 /* StorageUsageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageUsageViewModel.swift; sourceTree = ""; }; - 3A2478B42BB46617009D89E9 /* StorageUsageFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageUsageFactory.swift; sourceTree = ""; }; - 3A26D9342C3C1BE2003AD832 /* KlyWalletService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KlyWalletService.swift; sourceTree = ""; }; - 3A26D9362C3C1C01003AD832 /* KlyWallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KlyWallet.swift; sourceTree = ""; }; - 3A26D9382C3C1C62003AD832 /* KlyWalletFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KlyWalletFactory.swift; sourceTree = ""; }; - 3A26D93A2C3C1C97003AD832 /* KlyApiCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KlyApiCore.swift; sourceTree = ""; }; - 3A26D93C2C3C1CC3003AD832 /* KlyNodeApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KlyNodeApiService.swift; sourceTree = ""; }; - 3A26D93E2C3C1CED003AD832 /* KlyServiceApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KlyServiceApiService.swift; sourceTree = ""; }; - 3A26D9402C3C2DC4003AD832 /* KlyWalletService+Send.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KlyWalletService+Send.swift"; sourceTree = ""; }; - 3A26D9422C3C2E19003AD832 /* KlyWalletService+StatusCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KlyWalletService+StatusCheck.swift"; sourceTree = ""; }; - 3A26D9442C3D336A003AD832 /* KlyWalletService+RichMessageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KlyWalletService+RichMessageProvider.swift"; sourceTree = ""; }; - 3A26D9462C3D37B5003AD832 /* KlyWalletViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KlyWalletViewController.swift; sourceTree = ""; }; - 3A26D9482C3D3804003AD832 /* KlyTransferViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KlyTransferViewController.swift; sourceTree = ""; }; - 3A26D94A2C3D3838003AD832 /* KlyTransactionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KlyTransactionsViewController.swift; sourceTree = ""; }; - 3A26D94C2C3D387B003AD832 /* KlyTransactionDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KlyTransactionDetailsViewController.swift; sourceTree = ""; }; - 3A26D94F2C3D3A5A003AD832 /* KlyWalletService+WalletCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KlyWalletService+WalletCore.swift"; sourceTree = ""; }; - 3A26D9512C3E7F1D003AD832 /* klayr_notificationContent.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = klayr_notificationContent.png; sourceTree = ""; }; - 3A299C682B838AA600B54C61 /* ChatMediaCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMediaCell.swift; sourceTree = ""; }; - 3A299C6A2B838F2300B54C61 /* ChatMediaContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMediaContainerView.swift; sourceTree = ""; }; - 3A299C6C2B838F8F00B54C61 /* ChatMediaContainerView+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatMediaContainerView+Model.swift"; sourceTree = ""; }; - 3A299C702B83975700B54C61 /* ChatMediaContnentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMediaContnentView.swift; sourceTree = ""; }; - 3A299C722B83975D00B54C61 /* ChatMediaContnentView+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatMediaContnentView+Model.swift"; sourceTree = ""; }; - 3A299C752B84CE4100B54C61 /* FilesToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesToolbarView.swift; sourceTree = ""; }; - 3A299C772B84D11100B54C61 /* FilesToolbarCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesToolbarCollectionViewCell.swift; sourceTree = ""; }; - 3A299C7A2B85EABB00B54C61 /* FileListContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileListContentView.swift; sourceTree = ""; }; - 3A299C7C2B85F98700B54C61 /* ChatFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatFile.swift; sourceTree = ""; }; - 3A2F55F72AC6F308000A3F26 /* CoinTransaction+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoinTransaction+CoreDataClass.swift"; sourceTree = ""; }; - 3A2F55F82AC6F308000A3F26 /* CoinTransaction+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoinTransaction+CoreDataProperties.swift"; sourceTree = ""; }; - 3A2F55FB2AC6F885000A3F26 /* CoinStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinStorage.swift; sourceTree = ""; }; - 3A2F55FD2AC6F90E000A3F26 /* AdamantCoinStorageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantCoinStorageService.swift; sourceTree = ""; }; - 3A33F9F92A7A53DA002B8003 /* EmojiUpdateType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiUpdateType.swift; sourceTree = ""; }; - 3A4068332ACD7C18007E87BD /* CoinTransaction+TransactionDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoinTransaction+TransactionDetails.swift"; sourceTree = ""; }; - 3A41938E2A580C57006A6B22 /* AdamantRichTransactionReactService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantRichTransactionReactService.swift; sourceTree = ""; }; - 3A4193902A580C85006A6B22 /* RichTransactionReactService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RichTransactionReactService.swift; sourceTree = ""; }; - 3A4193992A5D554A006A6B22 /* Reaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reaction.swift; sourceTree = ""; }; - 3A53BD452C6B7AF100BB1EE6 /* DownloadPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadPolicy.swift; sourceTree = ""; }; - 3A5DF1782C4698EC0005369D /* EdgeInsetLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EdgeInsetLabel.swift; sourceTree = ""; }; - 3A770E4B2AE14F130008D98F /* SimpleTransactionDetails+Hashable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SimpleTransactionDetails+Hashable.swift"; sourceTree = ""; }; - 3A7BD00D2AA9BCE80045AAB0 /* VibroService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VibroService.swift; sourceTree = ""; }; - 3A7BD00F2AA9BD030045AAB0 /* AdamantVibroService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantVibroService.swift; sourceTree = ""; }; - 3A7BD0112AA9BD5A0045AAB0 /* AdamantVibroType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantVibroType.swift; sourceTree = ""; }; - 3A7FD6F42C076D85002AF7D9 /* FileMessageStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileMessageStatus.swift; sourceTree = ""; }; - 3A9015A42A614A18002A2464 /* EmojiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmojiService.swift; sourceTree = ""; }; - 3A9015A62A614A62002A2464 /* AdamantEmojiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantEmojiService.swift; sourceTree = ""; }; - 3A9015A82A615893002A2464 /* ChatMessagesListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessagesListViewModel.swift; sourceTree = ""; }; - 3A9365A82C41332F0073D9A7 /* KLYWalletService+DynamicConstants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "KLYWalletService+DynamicConstants.swift"; sourceTree = ""; }; - 3A96E3792AED27D7001F5A52 /* AdamantPartnerQRService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantPartnerQRService.swift; sourceTree = ""; }; - 3A96E37B2AED27F8001F5A52 /* PartnerQRService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartnerQRService.swift; sourceTree = ""; }; - 3AA2D5F6280EADE3000ED971 /* SocketService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketService.swift; sourceTree = ""; }; - 3AA2D5F9280EAF5D000ED971 /* AdamantSocketService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantSocketService.swift; sourceTree = ""; }; - 3AA388042B67F4DD00125684 /* BtcBlockchainInfoDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcBlockchainInfoDTO.swift; sourceTree = ""; }; - 3AA388062B67F53F00125684 /* BtcNetworkInfoDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcNetworkInfoDTO.swift; sourceTree = ""; }; - 3AA388092B69173500125684 /* DashNetworkInfoDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashNetworkInfoDTO.swift; sourceTree = ""; }; - 3AA50DEE2AEBE65D00C58FC8 /* PartnerQRView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartnerQRView.swift; sourceTree = ""; }; - 3AA50DF02AEBE66A00C58FC8 /* PartnerQRViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartnerQRViewModel.swift; sourceTree = ""; }; - 3AA50DF22AEBE67C00C58FC8 /* PartnerQRFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PartnerQRFactory.swift; sourceTree = ""; }; - 3AA6DF3F2BA9941E00EA2E16 /* MediaContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaContainerView.swift; sourceTree = ""; }; - 3AA6DF432BA997C000EA2E16 /* FileListContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileListContainerView.swift; sourceTree = ""; }; - 3AA6DF452BA9BEB700EA2E16 /* MediaContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaContentView.swift; sourceTree = ""; }; - 3ACD307D2BBD86B700ABF671 /* FilesStorageProprietiesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesStorageProprietiesService.swift; sourceTree = ""; }; - 3ACD307F2BBD86C800ABF671 /* FilesStorageProprietiesProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesStorageProprietiesProtocol.swift; sourceTree = ""; }; - 3AE0A4272BC6A64900BF7125 /* FilesNetworkManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FilesNetworkManager.swift; sourceTree = ""; }; - 3AE0A4282BC6A64900BF7125 /* IPFSApiService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IPFSApiService.swift; sourceTree = ""; }; - 3AE0A4292BC6A64900BF7125 /* IPFSApiCore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IPFSApiCore.swift; sourceTree = ""; }; - 3AE0A42D2BC6A96A00BF7125 /* IPFS+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IPFS+Constants.swift"; sourceTree = ""; }; - 3AE0A4302BC6A9C900BF7125 /* IPFSDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPFSDTO.swift; sourceTree = ""; }; - 3AE0A4322BC6A9EB00BF7125 /* FileApiServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileApiServiceProtocol.swift; sourceTree = ""; }; - 3AE0A4342BC6AA1B00BF7125 /* FileManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerError.swift; sourceTree = ""; }; - 3AE0A4362BC6AA6000BF7125 /* FilesNetworkManagerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesNetworkManagerProtocol.swift; sourceTree = ""; }; - 3AF08D5B2B4E7FFC00EB82B1 /* zh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh; path = zh.lproj/InfoPlist.strings; sourceTree = ""; }; - 3AF08D5C2B4E7FFC00EB82B1 /* zh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = zh; path = zh.lproj/Localizable.stringsdict; sourceTree = ""; }; - 3AF08D5D2B4E7FFC00EB82B1 /* zh */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = zh; path = zh.lproj/Localizable.strings; sourceTree = ""; }; - 3AF08D5E2B4EB3A200EB82B1 /* LanguageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageService.swift; sourceTree = ""; }; - 3AF08D602B4EB3C400EB82B1 /* LanguageStorageProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageStorageProtocol.swift; sourceTree = ""; }; - 3AF0A6C92BBAF5850019FF47 /* ChatFileService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatFileService.swift; sourceTree = ""; }; - 3AF53F8C2B3DCFA300B30312 /* NodeGroup+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NodeGroup+Constants.swift"; sourceTree = ""; }; - 3AF53F8E2B3EE0DA00B30312 /* DogeNodeInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeNodeInfo.swift; sourceTree = ""; }; - 3AF8D9E82C73ADFA007A7CBC /* IPFSNodeStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPFSNodeStatus.swift; sourceTree = ""; }; - 3AF9DF0A2BFE306C009A43A8 /* ChatFileProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatFileProtocol.swift; sourceTree = ""; }; - 3AF9DF0C2C049161009A43A8 /* CircularProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = ""; }; - 3AFE7E402B18D88B00718739 /* WalletService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletService.swift; sourceTree = ""; }; - 3AFE7E422B19E4D900718739 /* WalletServiceCompose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletServiceCompose.swift; sourceTree = ""; }; - 3AFE7E512B1F6B3400718739 /* WalletServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletServiceProtocol.swift; sourceTree = ""; }; - 41047B6F294B5EE10039E956 /* VisibleWalletsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibleWalletsViewController.swift; sourceTree = ""; }; - 41047B71294B5F210039E956 /* VisibleWalletsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibleWalletsTableViewCell.swift; sourceTree = ""; }; - 41047B73294C61D10039E956 /* VisibleWalletsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibleWalletsService.swift; sourceTree = ""; }; - 41047B75294C62710039E956 /* AdamantVisibleWalletsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantVisibleWalletsService.swift; sourceTree = ""; }; - 411742FF2A39B1D2008CD98A /* ContributeFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContributeFactory.swift; sourceTree = ""; }; - 411743012A39B208008CD98A /* ContributeState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContributeState.swift; sourceTree = ""; }; - 411743032A39B257008CD98A /* ContributeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContributeViewModel.swift; sourceTree = ""; }; - 411DB8322A14D01F006AB158 /* ChatKeyboardManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatKeyboardManager.swift; sourceTree = ""; }; - 412C0ED829124A3400DE2C5E /* dash_notificationContent.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = dash_notificationContent.png; sourceTree = ""; }; - 41330F7529F1509400CB587C /* AdamantCellAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantCellAnimation.swift; sourceTree = ""; }; - 4133AED329769EEC00F3D017 /* UpdatingIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatingIndicatorView.swift; sourceTree = ""; }; - 4133AF232A1CE1A3001A0A1E /* UITableView+Adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+Adamant.swift"; sourceTree = ""; }; - 4153045829C09902000E4BEA /* AdamantIncreaseFeeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantIncreaseFeeService.swift; sourceTree = ""; }; - 4153045A29C09C6C000E4BEA /* IncreaseFeeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncreaseFeeService.swift; sourceTree = ""; }; - 4154413A2923AED000824478 /* bitcoin_notificationContent.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = bitcoin_notificationContent.png; sourceTree = ""; }; - 416380E02A51765F00F90E6D /* ChatReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatReactionsView.swift; sourceTree = ""; }; - 4164A9D628F17D4000EEF16D /* ChatTransactionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTransactionService.swift; sourceTree = ""; }; - 4164A9D828F17DA700EEF16D /* AdamantChatTransactionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantChatTransactionService.swift; sourceTree = ""; }; - 4184F16D2A33023A00D7B8B9 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; - 4184F1722A33102800D7B8B9 /* AdamantCrashlysticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantCrashlysticsService.swift; sourceTree = ""; }; - 4184F1742A33106200D7B8B9 /* CrashlysticsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashlysticsService.swift; sourceTree = ""; }; - 4184F1762A33173100D7B8B9 /* ContributeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContributeView.swift; sourceTree = ""; }; - 4186B32F2941E642006594A3 /* AdmWalletService+DynamicConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdmWalletService+DynamicConstants.swift"; sourceTree = ""; }; - 4186B331294200B4006594A3 /* BtcWalletService+DynamicConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BtcWalletService+DynamicConstants.swift"; sourceTree = ""; }; - 4186B333294200C5006594A3 /* EthWalletService+DynamicConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EthWalletService+DynamicConstants.swift"; sourceTree = ""; }; - 4186B337294200E8006594A3 /* DogeWalletService+DynamicConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DogeWalletService+DynamicConstants.swift"; sourceTree = ""; }; - 4186B339294200F4006594A3 /* DashWalletService+DynamicConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DashWalletService+DynamicConstants.swift"; sourceTree = ""; }; - 418FDE4F2A25CA340055E3CD /* ChatMenuManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMenuManager.swift; sourceTree = ""; }; - 4193AE1529FBEFBF002F21BE /* NSAttributedText+Adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedText+Adamant.swift"; sourceTree = ""; }; - 4197B9C82952FAFF004CAF64 /* VisibleWalletsCheckmarkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibleWalletsCheckmarkView.swift; sourceTree = ""; }; - 4198D57A28C8B7DA009337F2 /* so-proud-notification.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "so-proud-notification.mp3"; sourceTree = ""; }; - 4198D57C28C8B7F9009337F2 /* relax-message-tone.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "relax-message-tone.mp3"; sourceTree = ""; }; - 4198D57E28C8B834009337F2 /* short-success.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = "short-success.mp3"; sourceTree = ""; }; - 4198D58028C8B8D1009337F2 /* default.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = default.mp3; sourceTree = ""; }; - 41A1994129D2D3920031AD75 /* SwipePanGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipePanGestureRecognizer.swift; sourceTree = ""; }; - 41A1994329D2D3CF0031AD75 /* MessageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageModel.swift; sourceTree = ""; }; - 41A1994529D2FCF80031AD75 /* ReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyView.swift; sourceTree = ""; }; - 41A1995129D42C460031AD75 /* ChatMessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageCell.swift; sourceTree = ""; }; - 41A1995329D56E340031AD75 /* ChatMessageReplyCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageReplyCell.swift; sourceTree = ""; }; - 41A1995529D56EAA0031AD75 /* ChatMessageReplyCell+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatMessageReplyCell+Model.swift"; sourceTree = ""; }; - 41A1995729D5733D0031AD75 /* ChatMessageCell+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatMessageCell+Model.swift"; sourceTree = ""; }; - 41BCB30F295C6082004B12AB /* VisibleWalletsResetTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibleWalletsResetTableViewCell.swift; sourceTree = ""; }; - 41C1698B29E7F34900FEB3CB /* RichTransactionReplyService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RichTransactionReplyService.swift; sourceTree = ""; }; - 41C1698D29E7F36900FEB3CB /* AdamantRichTransactionReplyService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantRichTransactionReplyService.swift; sourceTree = ""; }; - 41CA598B29A0D84F002BFDE4 /* TaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskManager.swift; sourceTree = ""; }; - 41CE1539297FF98200CC9254 /* Web3Swift+Adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Web3Swift+Adamant.swift"; sourceTree = ""; }; - 41E3C9CB2A0E20F500AF0985 /* AdamantCoinTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantCoinTools.swift; sourceTree = ""; }; 4A4D67BD3DC89C07D1351248 /* Pods-AdmCore.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AdmCore.release.xcconfig"; path = "Target Support Files/Pods-AdmCore/Pods-AdmCore.release.xcconfig"; sourceTree = ""; }; - 4E9EE86E28CE793D008359F7 /* SafeDecimalRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeDecimalRow.swift; sourceTree = ""; }; - 551F66E528959A5200DE5D69 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; - 551F66E72895B3DA00DE5D69 /* AdamantHealthCheckServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantHealthCheckServiceTests.swift; sourceTree = ""; }; - 5551CC8E28A8B75300B52AD0 /* ApiServiceStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiServiceStub.swift; sourceTree = ""; }; - 557AC307287B1365004699D7 /* CheckmarkRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckmarkRowView.swift; sourceTree = ""; }; - 55D1D854287B890300F94A4E /* AddressGeneratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressGeneratorTests.swift; sourceTree = ""; }; - 55E69E162868D7920025D82E /* CheckmarkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckmarkView.swift; sourceTree = ""; }; - 55FBAAFA28C550920066E629 /* NodesAllowanceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodesAllowanceTests.swift; sourceTree = ""; }; - 6403F5DD22723C6800D58779 /* DashMainnet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashMainnet.swift; sourceTree = ""; }; - 6403F5DF22723F6400D58779 /* DashWalletFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashWalletFactory.swift; sourceTree = ""; }; - 6403F5E122723F7500D58779 /* DashWallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashWallet.swift; sourceTree = ""; }; - 6403F5E322723F8C00D58779 /* DashWalletService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashWalletService.swift; sourceTree = ""; }; - 6403F5E522723FDA00D58779 /* DashWalletViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashWalletViewController.swift; sourceTree = ""; }; - 6406D74821C7F06000196713 /* SearchResultsViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = SearchResultsViewController.xib; sourceTree = ""; }; - 6414C18D217DF43100373FA6 /* String+adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+adamant.swift"; sourceTree = ""; }; - 644793C22166314A00FC4CF5 /* OnboardPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardPage.swift; sourceTree = ""; }; - 6449BA5E235CA0930033B936 /* ERC20WalletService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ERC20WalletService.swift; sourceTree = ""; }; - 6449BA5F235CA0930033B936 /* ERC20TransferViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ERC20TransferViewController.swift; sourceTree = ""; }; - 6449BA60235CA0930033B936 /* ERC20Wallet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ERC20Wallet.swift; sourceTree = ""; }; - 6449BA61235CA0930033B936 /* ERC20TransactionDetailsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ERC20TransactionDetailsViewController.swift; sourceTree = ""; }; - 6449BA62235CA0930033B936 /* ERC20WalletViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ERC20WalletViewController.swift; sourceTree = ""; }; - 6449BA63235CA0930033B936 /* ERC20TransactionsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ERC20TransactionsViewController.swift; sourceTree = ""; }; - 6449BA64235CA0930033B936 /* ERC20WalletService+RichMessageProviderWithStatusCheck.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ERC20WalletService+RichMessageProviderWithStatusCheck.swift"; sourceTree = ""; }; - 6449BA65235CA0930033B936 /* ERC20WalletService+Send.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ERC20WalletService+Send.swift"; sourceTree = ""; }; - 6449BA66235CA0930033B936 /* ERC20WalletFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ERC20WalletFactory.swift; sourceTree = ""; }; - 6449BA67235CA0930033B936 /* ERC20WalletService+RichMessageProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ERC20WalletService+RichMessageProvider.swift"; sourceTree = ""; }; - 644EC35120EFA9A300F40C73 /* DelegatesFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatesFactory.swift; sourceTree = ""; }; - 644EC35520EFAAB700F40C73 /* DelegatesListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatesListViewController.swift; sourceTree = ""; }; - 644EC35920EFB8E900F40C73 /* AdamantDelegateCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantDelegateCell.swift; sourceTree = ""; }; - 644EC35D20F34F1E00F40C73 /* DelegateDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegateDetailsViewController.swift; sourceTree = ""; }; - 6455E9F021075D3600B2E94C /* AddressBookService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressBookService.swift; sourceTree = ""; }; - 6455E9F221075D8000B2E94C /* AdamantAddressBookService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantAddressBookService.swift; sourceTree = ""; }; - 6458548A211B3AB1004C5909 /* WelcomeViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = WelcomeViewController.xib; sourceTree = ""; }; - 645938922378395E00A2BE7C /* EulaViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EulaViewController.swift; sourceTree = ""; }; - 645938932378395E00A2BE7C /* EulaViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = EulaViewController.xib; sourceTree = ""; }; - 645AE06521E67D3300AD3623 /* UITextField+adamant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITextField+adamant.swift"; sourceTree = ""; }; - 645FEB32213E72C100D6BA2D /* OnboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardViewController.swift; sourceTree = ""; }; - 645FEB33213E72C100D6BA2D /* OnboardViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = OnboardViewController.xib; sourceTree = ""; }; - 648BCA6C213D384F00875EB5 /* AvatarService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AvatarService.swift; sourceTree = ""; }; - 648C696E22915A12006645F5 /* DashTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashTransaction.swift; sourceTree = ""; }; - 648C697022915CB8006645F5 /* BTCRPCServerResponce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTCRPCServerResponce.swift; sourceTree = ""; }; - 648C697222916192006645F5 /* DashTransactionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashTransactionsViewController.swift; sourceTree = ""; }; - 648CE39F22999C890070A2CC /* BaseBtcTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseBtcTransaction.swift; sourceTree = ""; }; - 648CE3A122999CE70070A2CC /* BTCRawTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BTCRawTransaction.swift; sourceTree = ""; }; - 648CE3A32299A94D0070A2CC /* DashTransactionDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashTransactionDetailsViewController.swift; sourceTree = ""; }; - 648CE3A5229AD1CD0070A2CC /* DashWalletService+Send.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DashWalletService+Send.swift"; sourceTree = ""; }; - 648CE3A7229AD1E20070A2CC /* DashWalletService+RichMessageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DashWalletService+RichMessageProvider.swift"; sourceTree = ""; }; - 648CE3A9229AD1F90070A2CC /* DashWalletService+RichMessageProviderWithStatusCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DashWalletService+RichMessageProviderWithStatusCheck.swift"; sourceTree = ""; }; - 648CE3AB229AD2190070A2CC /* DashTransferViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashTransferViewController.swift; sourceTree = ""; }; - 648DD79D2236A0B500B811FD /* DogeTransactionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeTransactionsViewController.swift; sourceTree = ""; }; - 648DD79F2236A59200B811FD /* DogeTransactionDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeTransactionDetailsViewController.swift; sourceTree = ""; }; - 648DD7A12237D9A000B811FD /* DogeTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeTransaction.swift; sourceTree = ""; }; - 648DD7A32237DB9E00B811FD /* DogeWalletService+Send.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DogeWalletService+Send.swift"; sourceTree = ""; }; - 648DD7A52237DC4000B811FD /* DogeTransferViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeTransferViewController.swift; sourceTree = ""; }; - 648DD7A72239147800B811FD /* DogeWalletService+RichMessageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DogeWalletService+RichMessageProvider.swift"; sourceTree = ""; }; - 648DD7A92239150E00B811FD /* DogeWalletService+RichMessageProviderWithStatusCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DogeWalletService+RichMessageProviderWithStatusCheck.swift"; sourceTree = ""; }; - 649D6BEB21BD5A53009E727B /* UISuffixTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UISuffixTextField.swift; sourceTree = ""; }; - 649D6BEF21BFF481009E727B /* AdamantChatsProvider+search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantChatsProvider+search.swift"; sourceTree = ""; }; - 649D6BF121C27D5C009E727B /* SearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsViewController.swift; sourceTree = ""; }; - 64A223D520F760BB005157CB /* Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Localization.swift; sourceTree = ""; }; - 64B5736C2201E196005DC968 /* BtcTransactionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcTransactionsViewController.swift; sourceTree = ""; }; - 64B5736E2209B892005DC968 /* BtcTransactionDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcTransactionDetailsViewController.swift; sourceTree = ""; }; - 64BD2B7420E2814B00E2CD36 /* EthTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthTransaction.swift; sourceTree = ""; }; - 64BD2B7620E2820300E2CD36 /* TransactionDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDetails.swift; sourceTree = ""; }; - 64C65F4423893C7600DC0425 /* OnboardOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardOverlay.swift; sourceTree = ""; }; - 64D059FE20D3116A003AD655 /* NodesListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodesListViewController.swift; sourceTree = ""; }; - 64E1C82C222E95E2006C4DA7 /* DogeWalletFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeWalletFactory.swift; sourceTree = ""; }; - 64E1C82E222E95F6006C4DA7 /* DogeWallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeWallet.swift; sourceTree = ""; }; - 64E1C830222E9617006C4DA7 /* DogeWalletService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeWalletService.swift; sourceTree = ""; }; - 64E1C832222EA0F0006C4DA7 /* DogeWalletViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeWalletViewController.swift; sourceTree = ""; }; - 64EAB37322463E020018D9B2 /* InfoServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceProtocol.swift; sourceTree = ""; }; - 64F085D820E2D7600006DE68 /* AdmTransactionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdmTransactionsViewController.swift; sourceTree = ""; }; - 64FA53CC20E1300A006783C9 /* EthTransactionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthTransactionsViewController.swift; sourceTree = ""; }; - 64FA53D020E24941006783C9 /* TransactionDetailsViewControllerBase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionDetailsViewControllerBase.swift; sourceTree = ""; }; 74D5744703A7ECC98E244B14 /* Pods-AdmCore.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AdmCore.debug.xcconfig"; path = "Target Support Files/Pods-AdmCore/Pods-AdmCore.debug.xcconfig"; sourceTree = ""; }; - 85B405012D3012C7000AB744 /* AccountViewController+Form.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountViewController+Form.swift"; sourceTree = ""; }; - 9300F94529D0149100FEDDB8 /* RichMessageProviderWithStatusCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RichMessageProviderWithStatusCheck.swift; sourceTree = ""; }; - 9304F8BD292F88F900173F18 /* ANSPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ANSPayload.swift; sourceTree = ""; }; - 9304F8C1292F895C00173F18 /* PushNotificationsTokenService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationsTokenService.swift; sourceTree = ""; }; - 9304F8C3292F8A3100173F18 /* AdamantPushNotificationsTokenService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantPushNotificationsTokenService.swift; sourceTree = ""; }; - 9306E08F2CF8C50E00A99BA4 /* PKGeneratorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PKGeneratorFactory.swift; sourceTree = ""; }; - 9306E0902CF8C50E00A99BA4 /* PKGeneratorState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PKGeneratorState.swift; sourceTree = ""; }; - 9306E0912CF8C50E00A99BA4 /* PKGeneratorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PKGeneratorView.swift; sourceTree = ""; }; - 9306E0922CF8C50E00A99BA4 /* PKGeneratorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PKGeneratorViewModel.swift; sourceTree = ""; }; - 9306E0972CF8C67B00A99BA4 /* AdamantSecureField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantSecureField.swift; sourceTree = ""; }; - 931224A82C7AA0E4009E0ED0 /* InfoServiceApiService+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InfoServiceApiService+Extension.swift"; sourceTree = ""; }; - 931224AA2C7AA212009E0ED0 /* InfoServiceRatesRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceRatesRequestDTO.swift; sourceTree = ""; }; - 931224AC2C7AA67B009E0ED0 /* InfoServiceHistoryRequestDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceHistoryRequestDTO.swift; sourceTree = ""; }; - 931224AE2C7AA88E009E0ED0 /* InfoService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoService.swift; sourceTree = ""; }; - 931224B02C7ACFE6009E0ED0 /* InfoServiceAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceAssembly.swift; sourceTree = ""; }; - 931224B22C7AD5DD009E0ED0 /* InfoServiceTicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceTicker.swift; sourceTree = ""; }; - 9322E874297042F000B8357C /* ChatSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatSender.swift; sourceTree = ""; }; - 9322E876297042FA00B8357C /* ChatMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessage.swift; sourceTree = ""; }; - 9322E87A2970431200B8357C /* ChatMessageFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageFactory.swift; sourceTree = ""; }; - 93294B7C2AAD067000911109 /* AppContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContainer.swift; sourceTree = ""; }; - 93294B812AAD0BB400911109 /* BtcWalletFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcWalletFactory.swift; sourceTree = ""; }; - 93294B832AAD0C8F00911109 /* Assembler+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Assembler+Extension.swift"; sourceTree = ""; }; - 93294B852AAD0E0A00911109 /* AdmWallet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdmWallet.swift; sourceTree = ""; }; - 93294B862AAD0E0A00911109 /* AdmWalletService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdmWalletService.swift; sourceTree = ""; }; - 93294B8B2AAD2C6B00911109 /* SwiftyOnboardPage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyOnboardPage.swift; sourceTree = ""; }; - 93294B8C2AAD2C6B00911109 /* SwiftyOnboard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyOnboard.swift; sourceTree = ""; }; - 93294B8D2AAD2C6B00911109 /* SwiftyOnboardOverlay.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyOnboardOverlay.swift; sourceTree = ""; }; - 93294B952AAD320B00911109 /* ScreensFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreensFactory.swift; sourceTree = ""; }; - 93294B972AAD364F00911109 /* AdamantScreensFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantScreensFactory.swift; sourceTree = ""; }; - 93294B992AAD624100911109 /* WalletFactoryCompose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletFactoryCompose.swift; sourceTree = ""; }; - 932B34E82974AA4A002A75BA /* ChatPreservationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreservationProtocol.swift; sourceTree = ""; }; - 932F77582989F999006D8801 /* ChatCellManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatCellManager.swift; sourceTree = ""; }; - 9332C39C2C76BE7500164B80 /* FileApiServiceResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileApiServiceResult.swift; sourceTree = ""; }; - 9332C3A22C76C45A00164B80 /* ApiServiceComposeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiServiceComposeProtocol.swift; sourceTree = ""; }; - 9332C3A42C76C4EC00164B80 /* ApiServiceCompose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiServiceCompose.swift; sourceTree = ""; }; - 9340077F29AC341000A20622 /* ChatAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatAction.swift; sourceTree = ""; }; - 9345769428FD0C34004E6C7A /* UIViewController+email.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+email.swift"; sourceTree = ""; }; - 93496B822A6C85F400DD062F /* AdamantResources+CoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantResources+CoreData.swift"; sourceTree = ""; }; - 93496B9F2A6CAE9300DD062F /* LogoFullHeader.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = LogoFullHeader.xib; sourceTree = ""; }; - 93496BA22A6CAED100DD062F /* Roboto_700_normal.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Roboto_700_normal.ttf; sourceTree = ""; }; - 93496BA32A6CAED100DD062F /* Exo+2_700_normal.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Exo+2_700_normal.ttf"; sourceTree = ""; }; - 93496BA42A6CAED100DD062F /* Exo+2_100_normal.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Exo+2_100_normal.ttf"; sourceTree = ""; }; - 93496BA52A6CAED100DD062F /* Exo+2_300_normal.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Exo+2_300_normal.ttf"; sourceTree = ""; }; - 93496BA62A6CAED100DD062F /* Exo+2_400_normal.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Exo+2_400_normal.ttf"; sourceTree = ""; }; - 93496BA72A6CAED100DD062F /* Exo+2_400_italic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Exo+2_400_italic.ttf"; sourceTree = ""; }; - 93496BA82A6CAED100DD062F /* Exo+2_500_normal.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Exo+2_500_normal.ttf"; sourceTree = ""; }; - 93496BA92A6CAED100DD062F /* Roboto_400_italic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Roboto_400_italic.ttf; sourceTree = ""; }; - 93496BAA2A6CAED100DD062F /* Roboto_300_normal.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Roboto_300_normal.ttf; sourceTree = ""; }; - 93496BAB2A6CAED100DD062F /* Roboto_400_normal.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Roboto_400_normal.ttf; sourceTree = ""; }; - 93496BAC2A6CAED100DD062F /* Roboto_500_normal.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = Roboto_500_normal.ttf; sourceTree = ""; }; - 934FD9A32C783D2E00336841 /* InfoServiceStatusDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceStatusDTO.swift; sourceTree = ""; }; - 934FD9A52C783DB700336841 /* InfoServiceStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceStatus.swift; sourceTree = ""; }; - 934FD9A72C783E0C00336841 /* InfoServiceMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceMapper.swift; sourceTree = ""; }; - 934FD9A92C7842C800336841 /* InfoServiceResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceResponseDTO.swift; sourceTree = ""; }; - 934FD9AB2C78443600336841 /* InfoServiceHistoryItemDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceHistoryItemDTO.swift; sourceTree = ""; }; - 934FD9AD2C7846BA00336841 /* InfoServiceHistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceHistoryItem.swift; sourceTree = ""; }; - 934FD9AF2C78481500336841 /* InfoServiceApiError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceApiError.swift; sourceTree = ""; }; - 934FD9B12C7849C800336841 /* InfoServiceApiResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceApiResult.swift; sourceTree = ""; }; - 934FD9B32C78514E00336841 /* InfoServiceApiCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceApiCore.swift; sourceTree = ""; }; - 934FD9B52C78519600336841 /* InfoServiceApiCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceApiCommands.swift; sourceTree = ""; }; - 934FD9B72C7854AF00336841 /* InfoServiceMapperProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceMapperProtocol.swift; sourceTree = ""; }; - 934FD9B92C78565400336841 /* InfoServiceApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceApiService.swift; sourceTree = ""; }; - 934FD9BB2C78567300336841 /* InfoServiceApiServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoServiceApiServiceProtocol.swift; sourceTree = ""; }; - 93547BC929E2262D00B0914B /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = ""; }; - 9366588C2B0AB6BD00BDB2D3 /* CoinsNodesListState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinsNodesListState.swift; sourceTree = ""; }; - 9366588E2B0AB97500BDB2D3 /* CoinsNodesListMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinsNodesListMapper.swift; sourceTree = ""; }; - 936658922B0AC03700BDB2D3 /* CoinsNodesListStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinsNodesListStrings.swift; sourceTree = ""; }; - 936658942B0AC15300BDB2D3 /* Node+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Node+UI.swift"; sourceTree = ""; }; - 936658962B0ACB1500BDB2D3 /* CoinsNodesListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinsNodesListViewModel.swift; sourceTree = ""; }; - 9366589C2B0ADBAF00BDB2D3 /* CoinsNodesListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinsNodesListView.swift; sourceTree = ""; }; - 936658A22B0ADE4400BDB2D3 /* CoinsNodesListView+Row.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoinsNodesListView+Row.swift"; sourceTree = ""; }; - 936658A42B0AE67A00BDB2D3 /* CoinsNodesListFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinsNodesListFactory.swift; sourceTree = ""; }; - 93684A2929EFA28A00F9EFFE /* FixedTextMessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FixedTextMessageSizeCalculator.swift; sourceTree = ""; }; - 9371130E2996EDA900F64CF9 /* ChatRefreshMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatRefreshMock.swift; sourceTree = ""; }; - 937173F42C8049E0009D5191 /* InfoService+Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "InfoService+Constants.swift"; sourceTree = ""; }; - 9371E560295CD53100438F2C /* ChatLocalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatLocalization.swift; sourceTree = ""; }; - 93760BD62C656CF8002507C3 /* DefaultNodesProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultNodesProvider.swift; sourceTree = ""; }; - 93760BDD2C65A1FA002507C3 /* english.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = english.txt; sourceTree = ""; }; - 93760BDE2C65A284002507C3 /* WordList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WordList.swift; sourceTree = ""; }; - 93760BE02C65A2F3002507C3 /* Mnemonic+extended.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Mnemonic+extended.swift"; sourceTree = ""; }; - 937612702CC4C3CA0036EEB4 /* TransactionsStatusActor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionsStatusActor.swift; sourceTree = ""; }; - 937736812B0949C500B35C7A /* NodeCell+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NodeCell+Model.swift"; sourceTree = ""; }; - 937751AA2A68BB390054BD65 /* ChatTransactionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTransactionCell.swift; sourceTree = ""; }; - 937751AC2A68BCE10054BD65 /* MessageCellWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCellWrapper.swift; sourceTree = ""; }; - 93775E452A674FA9009061AC /* Markdown+Adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Markdown+Adamant.swift"; sourceTree = ""; }; - 9377FBDE296C2A2F00C9211B /* ChatTransactionContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTransactionContentView.swift; sourceTree = ""; }; - 9377FBE1296C2ACA00C9211B /* ChatTransactionContentView+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatTransactionContentView+Model.swift"; sourceTree = ""; }; - 937EDFBF2C9CF6B300F219BB /* VersionFooterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionFooterView.swift; sourceTree = ""; }; - 9380EF622D1119DD006939E1 /* ChatSwipeWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatSwipeWrapper.swift; sourceTree = ""; }; - 9380EF652D111BD1006939E1 /* ChatSwipeWrapperModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatSwipeWrapperModel.swift; sourceTree = ""; }; - 9380EF672D112BB9006939E1 /* ChatSwipeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatSwipeManager.swift; sourceTree = ""; }; - 9382F61229DEC0A3005E6216 /* ChatModelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatModelView.swift; sourceTree = ""; }; - 938F7D5A2955C8DA001915CA /* ChatDisplayManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDisplayManager.swift; sourceTree = ""; }; - 938F7D5C2955C8F9001915CA /* ChatLayoutManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatLayoutManager.swift; sourceTree = ""; }; - 938F7D5E2955C90D001915CA /* ChatInputBarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInputBarManager.swift; sourceTree = ""; }; - 938F7D602955C92B001915CA /* ChatDataSourceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDataSourceManager.swift; sourceTree = ""; }; - 938F7D632955C94F001915CA /* ChatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewController.swift; sourceTree = ""; }; - 938F7D652955C966001915CA /* ChatInputBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatInputBar.swift; sourceTree = ""; }; - 938F7D682955C9EC001915CA /* ChatViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewModel.swift; sourceTree = ""; }; - 938F7D712955CE72001915CA /* ChatFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatFactory.swift; sourceTree = ""; }; - 9390C5022976B42800270CDF /* ChatDialogManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDialogManager.swift; sourceTree = ""; }; - 9390C5042976B53000270CDF /* ChatDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatDialog.swift; sourceTree = ""; }; - 93996A962968209C008D080B /* ChatMessagesCollection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessagesCollection.swift; sourceTree = ""; }; - 9399F5EC29A85A48006C3E30 /* ChatCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatCacheService.swift; sourceTree = ""; }; - 939FA3412B0D6F0000710EC6 /* SelfRemovableHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelfRemovableHostingController.swift; sourceTree = ""; }; - 93A118502993167500E144CC /* ChatMessageBackgroundColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessageBackgroundColor.swift; sourceTree = ""; }; - 93A118522993241D00E144CC /* ChatMessagesListFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatMessagesListFactory.swift; sourceTree = ""; }; - 93A18C852AAEACC100D0AB98 /* AdamantWalletFactoryCompose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantWalletFactoryCompose.swift; sourceTree = ""; }; - 93A18C882AAEAE7700D0AB98 /* WalletFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletFactory.swift; sourceTree = ""; }; - 93A2A85F2CB4733800DBC75E /* MainThreadAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainThreadAssembly.swift; sourceTree = ""; }; - 93A2A8652CB4A1EE00DBC75E /* UnsafeSendableExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsafeSendableExtensions.swift; sourceTree = ""; }; - 93A91FD0297972B7001DB1F8 /* ChatScrollButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatScrollButton.swift; sourceTree = ""; }; - 93A91FD229799298001DB1F8 /* ChatStartPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatStartPosition.swift; sourceTree = ""; }; - 93ADE06E2ACA66AF008ED641 /* VibrationSelectionViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VibrationSelectionViewModel.swift; sourceTree = ""; }; - 93ADE06F2ACA66AF008ED641 /* VibrationSelectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VibrationSelectionView.swift; sourceTree = ""; }; - 93ADE0702ACA66AF008ED641 /* VibrationSelectionFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VibrationSelectionFactory.swift; sourceTree = ""; }; - 93B28EC12B076D31007F268B /* DashApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashApiService.swift; sourceTree = ""; }; - 93B28EC42B076E2C007F268B /* DashBlockchainInfoDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashBlockchainInfoDTO.swift; sourceTree = ""; }; - 93B28EC72B076E68007F268B /* DashResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashResponseDTO.swift; sourceTree = ""; }; - 93B28EC92B076E88007F268B /* DashErrorDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashErrorDTO.swift; sourceTree = ""; }; - 93BF4A6529E4859900505CD0 /* DelegatesBottomPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegatesBottomPanel.swift; sourceTree = ""; }; - 93BF4A6B29E4B4BF00505CD0 /* DelegatesBottomPanel+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DelegatesBottomPanel+Model.swift"; sourceTree = ""; }; - 93C794432B07725C00408826 /* DashGetAddressBalanceDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashGetAddressBalanceDTO.swift; sourceTree = ""; }; - 93C794472B0778C700408826 /* DashGetBlockDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashGetBlockDTO.swift; sourceTree = ""; }; - 93C794492B077A1C00408826 /* DashGetUnspentTransactionsDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashGetUnspentTransactionsDTO.swift; sourceTree = ""; }; - 93C7944B2B077B2700408826 /* DashGetAddressTransactionIds.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashGetAddressTransactionIds.swift; sourceTree = ""; }; - 93C7944D2B077C1F00408826 /* DashSendRawTransactionDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashSendRawTransactionDTO.swift; sourceTree = ""; }; - 93CC8DC6296F00D6003772BF /* ChatTransactionContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTransactionContainerView.swift; sourceTree = ""; }; - 93CC8DC8296F01DE003772BF /* ChatTransactionContainerView+Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatTransactionContainerView+Model.swift"; sourceTree = ""; }; - 93CC94C02B17EE73004842AC /* EthApiCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthApiCore.swift; sourceTree = ""; }; - 93CCAE782B06D81D00EA5B94 /* DogeApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeApiService.swift; sourceTree = ""; }; - 93CCAE7A2B06D9B500EA5B94 /* DogeBlocksDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeBlocksDTO.swift; sourceTree = ""; }; - 93CCAE7D2B06DA6C00EA5B94 /* DogeBlockDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeBlockDTO.swift; sourceTree = ""; }; - 93CCAE7F2B06E2D100EA5B94 /* ApiServiceError+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ApiServiceError+Extension.swift"; sourceTree = ""; }; - 93E123302A6DF8EF004DF33B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; - 93E123322A6DF8F1004DF33B /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; }; - 93E123332A6DF8F2004DF33B /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; - 93E123392A6DFD15004DF33B /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; - 93E1233B2A6DFD18004DF33B /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; - 93E1233C2A6DFD19004DF33B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - 93E123402A6DFE24004DF33B /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; - 93E123422A6DFE27004DF33B /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = ""; }; - 93E123432A6DFE2E004DF33B /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = ""; }; - 93E1234A2A6DFEF7004DF33B /* NotificationStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationStrings.swift; sourceTree = ""; }; - 93ED21492CC3555500AA1FC8 /* TxStatusServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TxStatusServiceProtocol.swift; sourceTree = ""; }; - 93ED214B2CC3561800AA1FC8 /* TransactionsStatusServiceComposeProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionsStatusServiceComposeProtocol.swift; sourceTree = ""; }; - 93ED214E2CC3567600AA1FC8 /* TransactionsStatusServiceCompose.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionsStatusServiceCompose.swift; sourceTree = ""; }; - 93ED21502CC3571200AA1FC8 /* TxStatusService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TxStatusService.swift; sourceTree = ""; }; - 93F3914F2962F5D400BFD6AE /* SpinnerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpinnerView.swift; sourceTree = ""; }; - 93FC169A2B0197FD0062B507 /* BtcApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcApiService.swift; sourceTree = ""; }; - 93FC169C2B019F440062B507 /* EthApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthApiService.swift; sourceTree = ""; }; - 93FC16A02B01DE120062B507 /* ERC20ApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ERC20ApiService.swift; sourceTree = ""; }; - A50A41042822F8CE006BDFE1 /* BtcWalletService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BtcWalletService.swift; sourceTree = ""; }; - A50A41052822F8CE006BDFE1 /* BtcWalletViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BtcWalletViewController.swift; sourceTree = ""; }; - A50A41062822F8CE006BDFE1 /* BtcWallet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BtcWallet.swift; sourceTree = ""; }; - A50A410D2822FC35006BDFE1 /* BtcWalletService+RichMessageProviderWithStatusCheck.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BtcWalletService+RichMessageProviderWithStatusCheck.swift"; sourceTree = ""; }; - A50A410E2822FC35006BDFE1 /* BtcWalletService+RichMessageProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BtcWalletService+RichMessageProvider.swift"; sourceTree = ""; }; - A50A410F2822FC35006BDFE1 /* BtcWalletService+Send.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BtcWalletService+Send.swift"; sourceTree = ""; }; - A50A41102822FC35006BDFE1 /* BtcTransferViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BtcTransferViewController.swift; sourceTree = ""; }; - A578BDE42623051C00090141 /* DashWalletService+Transactions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DashWalletService+Transactions.swift"; sourceTree = ""; }; - A5E04226282A8BDC0076CD13 /* BtcBalanceResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcBalanceResponse.swift; sourceTree = ""; }; - A5E04228282A998C0076CD13 /* BtcTransactionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcTransactionResponse.swift; sourceTree = ""; }; - A5E0422A282AB18B0076CD13 /* BtcUnspentTransactionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BtcUnspentTransactionResponse.swift; sourceTree = ""; }; AD258997F050B24C0051CC8D /* Pods-Adamant.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Adamant.release.xcconfig"; path = "Target Support Files/Pods-Adamant/Pods-Adamant.release.xcconfig"; sourceTree = ""; }; ADDFD2FA17E41CCBD11A1733 /* Pods-Adamant.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Adamant.debug.xcconfig"; path = "Target Support Files/Pods-Adamant/Pods-Adamant.debug.xcconfig"; sourceTree = ""; }; - E90055F420EBF5DA00D0CB2D /* AboutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; - E90055F620EC200900D0CB2D /* SecurityViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityViewController.swift; sourceTree = ""; }; - E90055F820ECD86800D0CB2D /* SecurityViewController+StayIn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SecurityViewController+StayIn.swift"; sourceTree = ""; }; - E90055FA20ECE78A00D0CB2D /* SecurityViewController+notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SecurityViewController+notifications.swift"; sourceTree = ""; }; - E905D39E204C281400DDB504 /* LoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; - E9061B96207501E40011F104 /* AdamantUserInfoKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantUserInfoKey.swift; sourceTree = ""; }; - E9061B982077AF8E0011F104 /* Debug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Debug.entitlements; sourceTree = ""; }; - E907350D2256779C00BF02CC /* DogeMainnet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeMainnet.swift; sourceTree = ""; }; E9079A7B229DEF9C0022CA0D /* MessageNotificationContentExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MessageNotificationContentExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - E9079A7F229DEF9C0022CA0D /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = ""; }; - E9079A82229DEF9C0022CA0D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; - E9079A84229DEF9C0022CA0D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - E9079A8B229DEFE40022CA0D /* Release.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - E908471A2196FE590095825D /* Adamant.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Adamant.xcdatamodel; sourceTree = ""; }; - E908471C2196FEA80095825D /* RichMessageTransaction+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RichMessageTransaction+CoreDataClass.swift"; sourceTree = ""; }; - E908471D2196FEA80095825D /* RichMessageTransaction+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RichMessageTransaction+CoreDataProperties.swift"; sourceTree = ""; }; - E908471E2196FEA80095825D /* CoreDataAccount+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoreDataAccount+CoreDataClass.swift"; sourceTree = ""; }; - E908471F2196FEA80095825D /* CoreDataAccount+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoreDataAccount+CoreDataProperties.swift"; sourceTree = ""; }; - E90847202196FEA80095825D /* BaseTransaction+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseTransaction+CoreDataClass.swift"; sourceTree = ""; }; - E90847212196FEA80095825D /* BaseTransaction+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseTransaction+CoreDataProperties.swift"; sourceTree = ""; }; - E90847222196FEA80095825D /* ChatTransaction+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatTransaction+CoreDataClass.swift"; sourceTree = ""; }; - E90847232196FEA80095825D /* ChatTransaction+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ChatTransaction+CoreDataProperties.swift"; sourceTree = ""; }; - E90847242196FEA80095825D /* TransferTransaction+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TransferTransaction+CoreDataClass.swift"; sourceTree = ""; }; - E90847252196FEA80095825D /* TransferTransaction+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TransferTransaction+CoreDataProperties.swift"; sourceTree = ""; }; - E90847262196FEA80095825D /* MessageTransaction+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageTransaction+CoreDataClass.swift"; sourceTree = ""; }; - E90847272196FEA80095825D /* MessageTransaction+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageTransaction+CoreDataProperties.swift"; sourceTree = ""; }; - E90847282196FEA80095825D /* Chatroom+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Chatroom+CoreDataClass.swift"; sourceTree = ""; }; - E90847292196FEA80095825D /* Chatroom+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Chatroom+CoreDataProperties.swift"; sourceTree = ""; }; - E90847382196FEF50095825D /* BaseTransaction+TransactionDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseTransaction+TransactionDetails.swift"; sourceTree = ""; }; - E908473A219707200095825D /* AccountViewController+StayIn.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountViewController+StayIn.swift"; sourceTree = ""; }; - E90A4942204C5ED6009F6A65 /* EurekaPassphraseRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EurekaPassphraseRow.swift; sourceTree = ""; }; - E90A4944204C5F60009F6A65 /* PassphraseCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PassphraseCell.xib; sourceTree = ""; }; - E90A494A204D9EB8009F6A65 /* AdamantAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantAuthentication.swift; sourceTree = ""; }; - E90A494C204DA932009F6A65 /* LocalAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthentication.swift; sourceTree = ""; }; - E90EA5C221BA8BF400A2CE25 /* DelegateDetailsViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = DelegateDetailsViewController.xib; sourceTree = ""; }; E913C8EE1FFFA51D001A83F7 /* Adamant.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Adamant.app; sourceTree = BUILT_PRODUCTS_DIR; }; - E913C8F11FFFA51D001A83F7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - E913C8F81FFFA51D001A83F7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - E913C8FD1FFFA51E001A83F7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - E9147B5E20500E9300145913 /* MyLittlePinpad+adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MyLittlePinpad+adamant.swift"; sourceTree = ""; }; - E9147B602050599000145913 /* LoginViewController+QR.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoginViewController+QR.swift"; sourceTree = ""; }; - E9147B6220505C7500145913 /* QRCodeReader+adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "QRCodeReader+adamant.swift"; sourceTree = ""; }; - E9147B6E205088DE00145913 /* LoginViewController+Pinpad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoginViewController+Pinpad.swift"; sourceTree = ""; }; - E91E5BF120DAF05500B06B3C /* NodeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeCell.swift; sourceTree = ""; }; - E9204B5120C9762300F3B9AB /* MessageStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageStatus.swift; sourceTree = ""; }; - E921534C20EE1E8700C0843F /* EurekaAlertLabelRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EurekaAlertLabelRow.swift; sourceTree = ""; }; - E921534D20EE1E8700C0843F /* AlertLabelCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AlertLabelCell.xib; sourceTree = ""; }; - E9215972206119FB0000CA5C /* ReachabilityMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReachabilityMonitor.swift; sourceTree = ""; }; - E921597420611A6A0000CA5C /* AdamantReachability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantReachability.swift; sourceTree = ""; }; - E921597A206503000000CA5C /* ButtonsStripeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonsStripeView.swift; sourceTree = ""; }; - E921597C2065031D0000CA5C /* ButtonsStripe.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ButtonsStripe.xib; sourceTree = ""; }; - E9220E0121983155009C9642 /* adamant-core.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "adamant-core.js"; sourceTree = ""; }; - E9220E0221983155009C9642 /* JSAdamantCore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSAdamantCore.swift; sourceTree = ""; }; - E9220E07219879B9009C9642 /* NativeCoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeCoreTests.swift; sourceTree = ""; }; - E923222521135F9000A7E5AF /* EthAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthAccount.swift; sourceTree = ""; }; - E9240BF4215D686500187B09 /* AdmWalletService+RichMessageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdmWalletService+RichMessageProvider.swift"; sourceTree = ""; }; - E9256F5E2034C21100DE86E9 /* String+localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+localized.swift"; sourceTree = ""; }; - E9256F752039A9A200DE86E9 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; - E926E02D213EAABF005E536B /* TransferViewControllerBase+Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TransferViewControllerBase+Alert.swift"; sourceTree = ""; }; - E926E031213EC43B005E536B /* FullscreenAlertView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullscreenAlertView.swift; sourceTree = ""; }; - E9332B8821F1FA4400D56E72 /* OnboardFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardFactory.swift; sourceTree = ""; }; - E933475A225539390083839E /* DogeGetTransactionsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DogeGetTransactionsResponse.swift; sourceTree = ""; }; - E9393FA92055D03300EE6F30 /* AdamantMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantMessage.swift; sourceTree = ""; }; - E93B0D732028B21400126346 /* ChatsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatsProvider.swift; sourceTree = ""; }; - E93B0D752028B28E00126346 /* AdamantChatsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantChatsProvider.swift; sourceTree = ""; }; - E93D7ABD2052CEE1005D19DC /* NotificationsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsService.swift; sourceTree = ""; }; - E93D7ABF2052CF63005D19DC /* AdamantNotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantNotificationService.swift; sourceTree = ""; }; - E93EB09E20DA3FA4001F9601 /* NodesEditorFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodesEditorFactory.swift; sourceTree = ""; }; - E940086A2114A70600CD2D67 /* LskAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskAccount.swift; sourceTree = ""; }; - E940086D2114AA2E00CD2D67 /* WalletCoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletCoreProtocol.swift; sourceTree = ""; }; - E94008712114EACF00CD2D67 /* WalletAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletAccount.swift; sourceTree = ""; }; - E940087A2114ED0600CD2D67 /* EthWalletService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthWalletService.swift; sourceTree = ""; }; - E940087C2114EDEE00CD2D67 /* EthWallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthWallet.swift; sourceTree = ""; }; - E94008862114F05B00CD2D67 /* AddressValidationResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressValidationResult.swift; sourceTree = ""; }; - E940088A2114F63000CD2D67 /* NSRegularExpression+adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSRegularExpression+adamant.swift"; sourceTree = ""; }; - E940088E2119A9E800CD2D67 /* BigInt+Decimal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BigInt+Decimal.swift"; sourceTree = ""; }; - E941CCDA20E786D700C96220 /* AccountHeader.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountHeader.xib; sourceTree = ""; }; - E941CCDC20E7B70200C96220 /* WalletCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletCollectionViewCell.swift; sourceTree = ""; }; - E941CCDD20E7B70200C96220 /* WalletCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = WalletCollectionViewCell.xib; sourceTree = ""; }; - E9484B77227C617D008E10F0 /* BalanceTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceTableViewCell.swift; sourceTree = ""; }; - E9484B78227C617E008E10F0 /* BalanceTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BalanceTableViewCell.xib; sourceTree = ""; }; - E9484B7C2285BAD8008E10F0 /* PrivateKeyGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateKeyGenerator.swift; sourceTree = ""; }; - E94883E6203F07CD00F6E1B0 /* PassphraseValidation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassphraseValidation.swift; sourceTree = ""; }; - E948E03A20235E2300975D6B /* SettingsFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsFactory.swift; sourceTree = ""; }; - E94E7B00205D3F090042B639 /* ChatListViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ChatListViewController.xib; sourceTree = ""; }; - E94E7B07205D4CB80042B639 /* ShareQRFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareQRFactory.swift; sourceTree = ""; }; - E94E7B0B205D5E4A0042B639 /* TransactionsListViewControllerBase.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TransactionsListViewControllerBase.xib; sourceTree = ""; }; - E950273F202E257E002C1098 /* RepeaterService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepeaterService.swift; sourceTree = ""; }; - E950652020404BF0008352E5 /* AdamantUriBuilding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantUriBuilding.swift; sourceTree = ""; }; - E950652220404C84008352E5 /* AdamantUriTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantUriTools.swift; sourceTree = ""; }; - E957E103229AF7CA0019732A /* adamant_notificationContent.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = adamant_notificationContent.png; sourceTree = ""; }; - E957E104229AF7CA0019732A /* doge_notificationContent.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = doge_notificationContent.png; sourceTree = ""; }; - E957E105229AF7CA0019732A /* lisk_notificationContent.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = lisk_notificationContent.png; sourceTree = ""; }; - E957E106229AF7CB0019732A /* ethereum_notificationContent.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = ethereum_notificationContent.png; sourceTree = ""; }; E957E110229B04280019732A /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; E957E112229B04280019732A /* UserNotificationsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotificationsUI.framework; path = System/Library/Frameworks/UserNotificationsUI.framework; sourceTree = SDKROOT; }; E957E12D229B10F80019732A /* TransferNotificationContentExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = TransferNotificationContentExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - E957E131229B10F80019732A /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = ""; }; - E957E134229B10F80019732A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; - E957E136229B10F80019732A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - E957E13D229B118E0019732A /* Release.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - E95F856A200789450070534A /* JSModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSModels.swift; sourceTree = ""; }; - E95F85702007D98D0070534A /* CurrencyFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrencyFormatterTests.swift; sourceTree = ""; }; - E95F85742007E4790070534A /* HexAndBytesUtilitiesTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HexAndBytesUtilitiesTest.swift; sourceTree = ""; }; - E95F85762007E8EC0070534A /* JSAdamantCoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSAdamantCoreTests.swift; sourceTree = ""; }; - E95F857E2008C8D60070534A /* ChatListFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListFactory.swift; sourceTree = ""; }; - E95F85842008CB3A0070534A /* ChatListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatListViewController.swift; sourceTree = ""; }; - E95F85B6200A4D8F0070534A /* TestTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTools.swift; sourceTree = ""; }; - E95F85B9200A4DC90070534A /* TransactionSend.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = TransactionSend.json; sourceTree = ""; }; - E95F85BB200A4E670070534A /* ParsingModelsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParsingModelsTests.swift; sourceTree = ""; }; - E95F85BD200A503A0070534A /* TransactionChat.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = TransactionChat.json; sourceTree = ""; }; - E95F85BF200A51BB0070534A /* Account.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Account.json; sourceTree = ""; }; - E95F85C1200A53E90070534A /* NormalizedTransaction.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = NormalizedTransaction.json; sourceTree = ""; }; - E95F85C3200A540B0070534A /* Chat.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Chat.json; sourceTree = ""; }; - E95F85C5200A9B070070534A /* ChatTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatTableViewCell.swift; sourceTree = ""; }; - E95F85C6200A9B070070534A /* ChatTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ChatTableViewCell.xib; sourceTree = ""; }; - E96BBE3021F70F5E009AA738 /* ReadonlyTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadonlyTextView.swift; sourceTree = ""; }; - E96BBE3221F71290009AA738 /* BuyAndSellViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuyAndSellViewController.swift; sourceTree = ""; }; - E96D64C72295C44400CA5587 /* Data+utilites.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+utilites.swift"; sourceTree = ""; }; E96D64DB2295CD4700CA5587 /* NotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - E96D64DD2295CD4700CA5587 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; - E96D64DF2295CD4700CA5587 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - E96E86B721679C120061F80A /* EthTransactionDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthTransactionDetailsViewController.swift; sourceTree = ""; }; - E971591921681D6900A5F904 /* TransactionStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionStatus.swift; sourceTree = ""; }; - E971591B2168209800A5F904 /* EthWalletService+RichMessageProviderWithStatusCheck.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EthWalletService+RichMessageProviderWithStatusCheck.swift"; sourceTree = ""; }; - E9722065201F42BB004F2AAD /* CoreDataStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataStack.swift; sourceTree = ""; }; - E9722067201F42CC004F2AAD /* InMemoryCoreDataStack.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InMemoryCoreDataStack.swift; sourceTree = ""; }; - E972206A201F44CA004F2AAD /* TransfersProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransfersProvider.swift; sourceTree = ""; }; - E9771D7D22995C870099AAC7 /* Debug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Debug.entitlements; sourceTree = ""; }; - E9771DA622997F310099AAC7 /* ServerResponseWithTimestamp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerResponseWithTimestamp.swift; sourceTree = ""; }; - E983AE2020E655C500497E1A /* AccountHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountHeaderView.swift; sourceTree = ""; }; - E983AE2820E65F3200497E1A /* AccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountViewController.swift; sourceTree = ""; }; - E983AE2C20E6720D00497E1A /* AccountFooter.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AccountFooter.xib; sourceTree = ""; }; - E987024820C2B1F700E393F4 /* AdamantChatsProvider+fakeMessages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantChatsProvider+fakeMessages.swift"; sourceTree = ""; }; - E98FC34320F920BD00032D65 /* UIFont+adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+adamant.swift"; sourceTree = ""; }; - E993301D212EF39700CD5200 /* EthTransferViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthTransferViewController.swift; sourceTree = ""; }; - E993301F21354B1800CD5200 /* AdmWalletFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdmWalletFactory.swift; sourceTree = ""; }; - E993302121354BC300CD5200 /* EthWalletFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthWalletFactory.swift; sourceTree = ""; }; - E99330252136B0E500CD5200 /* TransferViewControllerBase+QR.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TransferViewControllerBase+QR.swift"; sourceTree = ""; }; - E9942B7F203C058C00C163AF /* QRGeneratorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRGeneratorViewController.swift; sourceTree = ""; }; - E9942B83203CBFCE00C163AF /* AdamantQRTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantQRTools.swift; sourceTree = ""; }; - E9942B86203D9E5100C163AF /* EurekaQRRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EurekaQRRow.swift; sourceTree = ""; }; - E9942B88203D9ECA00C163AF /* QrCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = QrCell.xib; sourceTree = ""; }; - E9960B2F21F5154300C840A8 /* BaseAccount+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseAccount+CoreDataClass.swift"; sourceTree = ""; }; - E9960B3021F5154300C840A8 /* BaseAccount+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseAccount+CoreDataProperties.swift"; sourceTree = ""; }; - E9960B3121F5154300C840A8 /* DummyAccount+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DummyAccount+CoreDataClass.swift"; sourceTree = ""; }; - E9960B3221F5154300C840A8 /* DummyAccount+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DummyAccount+CoreDataProperties.swift"; sourceTree = ""; }; - E99818932120892F0018C84C /* WalletViewControllerBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletViewControllerBase.swift; sourceTree = ""; }; - E9981895212095CA0018C84C /* EthWalletViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthWalletViewController.swift; sourceTree = ""; }; - E9981897212096ED0018C84C /* WalletViewControllerBase.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = WalletViewControllerBase.xib; sourceTree = ""; }; - E9A03FD120DBC0F2007653A1 /* NodeEditorViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeEditorViewController.swift; sourceTree = ""; }; - E9A03FD320DBC824007653A1 /* NodeVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeVersion.swift; sourceTree = ""; }; - E9A174B22057EC47003667CD /* BackgroundFetchService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundFetchService.swift; sourceTree = ""; }; - E9A174B42057EDCE003667CD /* AdamantTransfersProvider+backgroundFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantTransfersProvider+backgroundFetch.swift"; sourceTree = ""; }; - E9A174B62057F1B3003667CD /* AdamantChatsProvider+backgroundFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantChatsProvider+backgroundFetch.swift"; sourceTree = ""; }; - E9A174B820587B83003667CD /* notification.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = notification.mp3; sourceTree = ""; }; - E9AA8BF72129F13000F9249F /* ComplexTransferViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplexTransferViewController.swift; sourceTree = ""; }; - E9AA8BF9212C166600F9249F /* EthWalletService+Send.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EthWalletService+Send.swift"; sourceTree = ""; }; - E9AA8C01212C5BF500F9249F /* AdmWalletService+Send.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdmWalletService+Send.swift"; sourceTree = ""; }; - E9B1AA562121ACBF00080A2A /* AdmWalletViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdmWalletViewController.swift; sourceTree = ""; }; - E9B1AA5A21283E0F00080A2A /* AdmTransferViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdmTransferViewController.swift; sourceTree = ""; }; - E9B3D399201F90570019EB36 /* AccountsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsProvider.swift; sourceTree = ""; }; - E9B3D39D201F99F40019EB36 /* DataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProvider.swift; sourceTree = ""; }; - E9B3D3A0201FA26B0019EB36 /* AdamantAccountsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantAccountsProvider.swift; sourceTree = ""; }; - E9B3D3A8202082450019EB36 /* AdamantTransfersProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantTransfersProvider.swift; sourceTree = ""; }; - E9B4E1A7210F079E007E77FC /* DoubleDetailsTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoubleDetailsTableViewCell.swift; sourceTree = ""; }; - E9B4E1A9210F08BE007E77FC /* DoubleDetailsTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DoubleDetailsTableViewCell.xib; sourceTree = ""; }; - E9B994BF22BFD678004CD645 /* Debug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Debug.entitlements; sourceTree = ""; }; - E9B994C022BFD6F9004CD645 /* Debug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Debug.entitlements; sourceTree = ""; }; - E9B994C122BFD723004CD645 /* Release.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - E9B994C222BFD73F004CD645 /* Release.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - E9C51ECE200E2D1100385EB7 /* FeeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeeTests.swift; sourceTree = ""; }; - E9C51EF02013F18000385EB7 /* NewChatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatViewController.swift; sourceTree = ""; }; - E9D1BE1B211DABE100E86B72 /* WalletPagingItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletPagingItem.swift; sourceTree = ""; }; - E9DFB71B21624C9200CF8C7C /* AdmTransactionDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdmTransactionDetailsViewController.swift; sourceTree = ""; }; - E9E7CD8A20026B0600DFC4DB /* AccountService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountService.swift; sourceTree = ""; }; - E9E7CD8C20026B6600DFC4DB /* DialogService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialogService.swift; sourceTree = ""; }; - E9E7CD8E20026CD300DFC4DB /* AdamantDialogService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdamantDialogService.swift; sourceTree = ""; }; - E9E7CD9020026FA100DFC4DB /* AppAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppAssembly.swift; sourceTree = ""; }; - E9E7CD922002740500DFC4DB /* AdamantAccountService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantAccountService.swift; sourceTree = ""; }; - E9E7CDB02002B97B00DFC4DB /* AccountFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountFactory.swift; sourceTree = ""; }; - E9E7CDB22002B9FB00DFC4DB /* LoginFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginFactory.swift; sourceTree = ""; }; - E9E7CDB62003994E00DFC4DB /* AdamantUtilities+extended.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantUtilities+extended.swift"; sourceTree = ""; }; - E9E7CDBD2003AEFB00DFC4DB /* CellFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CellFactory.swift; sourceTree = ""; }; - E9E7CDBF2003AF6D00DFC4DB /* AdamantCellFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdamantCellFactory.swift; sourceTree = ""; }; - E9E7CDC12003F5A400DFC4DB /* TransactionsListViewControllerBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionsListViewControllerBase.swift; sourceTree = ""; }; - E9E7CDC52003F6D200DFC4DB /* TransactionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionTableViewCell.swift; sourceTree = ""; }; - E9EC342020052ABB00C0E546 /* TransferViewControllerBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferViewControllerBase.swift; sourceTree = ""; }; E9EC344420066D4A00C0E546 /* AdamantTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AdamantTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - E9EC344620066D4A00C0E546 /* AddressValidationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressValidationTests.swift; sourceTree = ""; }; - E9EC344820066D4A00C0E546 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - E9FAE5D9203DBFEF008D3A6B /* Comparable+clamped.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Comparable+clamped.swift"; sourceTree = ""; }; - E9FAE5E0203ED1AE008D3A6B /* ShareQrViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareQrViewController.swift; sourceTree = ""; }; - E9FAE5E1203ED1AE008D3A6B /* ShareQrViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ShareQrViewController.xib; sourceTree = ""; }; - E9FCA1E5218334C00005E83D /* SimpleTransactionDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleTransactionDetails.swift; sourceTree = ""; }; - E9FEECA52143C300007DD7C8 /* EthWalletService+RichMessageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EthWalletService+RichMessageProvider.swift"; sourceTree = ""; }; /* End PBXFileReference section */ -/* Begin PBXFrameworksBuildPhase section */ - E9079A78229DEF9C0022CA0D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - E9079A7D229DEF9C0022CA0D /* UserNotificationsUI.framework in Frameworks */, - E9079A7C229DEF9C0022CA0D /* UserNotifications.framework in Frameworks */, - A5DBBAE7262C72BD004AC028 /* CryptoSwift in Frameworks */, - A5F929B8262C858F00C3E60A /* MarkdownKit in Frameworks */, - A5241B77262DEDEF009FA43E /* Clibsodium in Frameworks */, - 937751A92A68B3400054BD65 /* CommonKit in Frameworks */, - 55D1D851287B78FC00F94A4E /* SnapKit in Frameworks */, - A57282D5262C94E500C96FA8 /* DateToolsSwift in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - E913C8EB1FFFA51D001A83F7 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - A5AC8DFF262E0B030053A7E2 /* SipHash in Frameworks */, - 3A8875EF27BBF38D00436195 /* Parchment in Frameworks */, - 3C06931576393125C61FB8F6 /* Pods_Adamant.framework in Frameworks */, - A50AEB0C262C81E300B37C22 /* QRCodeReader in Frameworks */, - 416F5EA4290162EB00EF0400 /* SocketIO in Frameworks */, - A5F92994262C855B00C3E60A /* MarkdownKit in Frameworks */, - 3A833C402B99CDA000238F6A /* FilesStorageKit in Frameworks */, - A50AEB04262C815200B37C22 /* EFQRCode in Frameworks */, - A544F0D4262C9878001F1A6D /* Eureka in Frameworks */, - 3AC76E3D2AB09118008042C4 /* ElegantEmojiPicker in Frameworks */, - A5785ADF262C63580001BC66 /* web3swift in Frameworks */, - 557AC306287B10D8004699D7 /* SnapKit in Frameworks */, - 9342F6C22A6A35E300A9B39F /* CommonKit in Frameworks */, - A5F0A04B262C9CA90009672A /* Swinject in Frameworks */, - A5DBBABD262C7221004AC028 /* Clibsodium in Frameworks */, - 938F7D582955C1DA001915CA /* MessageKit in Frameworks */, - A530B0D82842110D003F0210 /* (null) in Frameworks */, - A5DBBADC262C729B004AC028 /* CryptoSwift in Frameworks */, - 4177E5E12A52DA7100C089FE /* AdvancedContextMenuKit in Frameworks */, - A5D87BA3262CA01D00DC28F0 /* ProcedureKit in Frameworks */, - A5C99E0E262C9E3A00F7B1B7 /* Reachability in Frameworks */, - A5DBBAF0262C72EF004AC028 /* LiskKit in Frameworks */, - 93FA403629401BFC00D20DB6 /* PopupKit in Frameworks */, - 4184F1712A33044E00D7B8B9 /* FirebaseCrashlytics in Frameworks */, - A5DBBAEE262C72EF004AC028 /* BitcoinKit in Frameworks */, - A57282CA262C94CD00C96FA8 /* DateToolsSwift in Frameworks */, - 3A075C9E2B98A3B100714E3B /* FilesPickerKit in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - E957E12A229B10F80019732A /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - E957E12F229B10F80019732A /* UserNotificationsUI.framework in Frameworks */, - E957E12E229B10F80019732A /* UserNotifications.framework in Frameworks */, - A5DBBAE5262C72B7004AC028 /* CryptoSwift in Frameworks */, - A5F929B6262C858700C3E60A /* MarkdownKit in Frameworks */, - A5241B70262DEDE1009FA43E /* Clibsodium in Frameworks */, - 937751A72A68B33A0054BD65 /* CommonKit in Frameworks */, - 55D1D84F287B78F200F94A4E /* SnapKit in Frameworks */, - A57282D3262C94DF00C96FA8 /* DateToolsSwift in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - E96D64D82295CD4700CA5587 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 937751A52A68B3320054BD65 /* CommonKit in Frameworks */, - A57282D1262C94DA00C96FA8 /* DateToolsSwift in Frameworks */, - A5F929AF262C857D00C3E60A /* MarkdownKit in Frameworks */, - A5DBBAE3262C72B0004AC028 /* CryptoSwift in Frameworks */, - A5241B7E262DEDFE009FA43E /* Clibsodium in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - E9EC344120066D4A00C0E546 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 0C42C8036B708BFC84C34963 /* Pods */ = { - isa = PBXGroup; - children = ( - ADDFD2FA17E41CCBD11A1733 /* Pods-Adamant.debug.xcconfig */, - AD258997F050B24C0051CC8D /* Pods-Adamant.release.xcconfig */, - 74D5744703A7ECC98E244B14 /* Pods-AdmCore.debug.xcconfig */, - 4A4D67BD3DC89C07D1351248 /* Pods-AdmCore.release.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; - 2621AB352C60E52900046D7A /* Notifications */ = { - isa = PBXGroup; - children = ( - 26F7EF3E2C9118E700E16A94 /* NotificationSounds */, - 2621AB362C60E74A00046D7A /* NotificationsView.swift */, - 2621AB382C60E7AE00046D7A /* NotificationsViewModel.swift */, - 2621AB3A2C613C8100046D7A /* NotificationsFactory.swift */, - ); - path = Notifications; - sourceTree = ""; - }; - 26F7EF3E2C9118E700E16A94 /* NotificationSounds */ = { - isa = PBXGroup; - children = ( - 269B83362C74D1F9002AA1D7 /* NotificationSoundsView.swift */, - 269B83392C74D4AA002AA1D7 /* NotificationSoundsViewModel.swift */, - 269B833C2C74E661002AA1D7 /* NotificationSoundsFactory.swift */, - ); - path = NotificationSounds; - sourceTree = ""; - }; - 3A20D9392AE7F305005475A6 /* Models */ = { - isa = PBXGroup; - children = ( - 64BD2B7620E2820300E2CD36 /* TransactionDetails.swift */, - 3A20D93A2AE7F316005475A6 /* AdamantTransactionDetails.swift */, - ); - path = Models; - sourceTree = ""; - }; - 3A2478AF2BB45DE2009D89E9 /* StorageUsage */ = { - isa = PBXGroup; - children = ( - 3A2478B42BB46617009D89E9 /* StorageUsageFactory.swift */, - 3A2478B02BB45DF8009D89E9 /* StorageUsageView.swift */, - 3A2478B22BB461A7009D89E9 /* StorageUsageViewModel.swift */, - ); - path = StorageUsage; - sourceTree = ""; - }; - 3A26D9312C3C1B55003AD832 /* Klayr */ = { - isa = PBXGroup; - children = ( - 3A26D94E2C3D3983003AD832 /* WalletService */, - 3A26D9362C3C1C01003AD832 /* KlyWallet.swift */, - 3A26D9382C3C1C62003AD832 /* KlyWalletFactory.swift */, - 3A9365A82C41332F0073D9A7 /* KLYWalletService+DynamicConstants.swift */, - 3A26D93A2C3C1C97003AD832 /* KlyApiCore.swift */, - 3A26D93C2C3C1CC3003AD832 /* KlyNodeApiService.swift */, - 3A26D93E2C3C1CED003AD832 /* KlyServiceApiService.swift */, - 3A26D9462C3D37B5003AD832 /* KlyWalletViewController.swift */, - 3A26D9482C3D3804003AD832 /* KlyTransferViewController.swift */, - 3A26D94A2C3D3838003AD832 /* KlyTransactionsViewController.swift */, - 3A26D94C2C3D387B003AD832 /* KlyTransactionDetailsViewController.swift */, - ); - path = Klayr; - sourceTree = ""; - }; - 3A26D94E2C3D3983003AD832 /* WalletService */ = { - isa = PBXGroup; - children = ( - 3A26D9342C3C1BE2003AD832 /* KlyWalletService.swift */, - 3A26D94F2C3D3A5A003AD832 /* KlyWalletService+WalletCore.swift */, - 3A26D9402C3C2DC4003AD832 /* KlyWalletService+Send.swift */, - 3A26D9422C3C2E19003AD832 /* KlyWalletService+StatusCheck.swift */, - 3A26D9442C3D336A003AD832 /* KlyWalletService+RichMessageProvider.swift */, - ); - path = WalletService; - sourceTree = ""; - }; - 3A299C672B838A7800B54C61 /* ChatMedia */ = { - isa = PBXGroup; - children = ( - 3A299C682B838AA600B54C61 /* ChatMediaCell.swift */, - 3A299C6F2B83901600B54C61 /* Container */, - 3A299C6E2B83901000B54C61 /* Content */, - ); - path = ChatMedia; - sourceTree = ""; - }; - 3A299C6E2B83901000B54C61 /* Content */ = { - isa = PBXGroup; - children = ( - 3A299C792B85EAA900B54C61 /* Views */, - 3A299C702B83975700B54C61 /* ChatMediaContnentView.swift */, - 3A299C722B83975D00B54C61 /* ChatMediaContnentView+Model.swift */, - ); - path = Content; - sourceTree = ""; - }; - 3A299C6F2B83901600B54C61 /* Container */ = { - isa = PBXGroup; - children = ( - 3A299C6A2B838F2300B54C61 /* ChatMediaContainerView.swift */, - 3A299C6C2B838F8F00B54C61 /* ChatMediaContainerView+Model.swift */, - ); - path = Container; - sourceTree = ""; - }; - 3A299C742B84CE1400B54C61 /* FilesToolBarView */ = { - isa = PBXGroup; - children = ( - 3A299C752B84CE4100B54C61 /* FilesToolbarView.swift */, - 3A299C772B84D11100B54C61 /* FilesToolbarCollectionViewCell.swift */, - ); - path = FilesToolBarView; - sourceTree = ""; - }; - 3A299C792B85EAA900B54C61 /* Views */ = { - isa = PBXGroup; - children = ( - 3AA6DF422BA9943500EA2E16 /* MediaContainerView */, - 3AA6DF412BA9942300EA2E16 /* ChatFileContainerView */, - ); - path = Views; - sourceTree = ""; - }; - 3A41938D2A580C3B006A6B22 /* RichTransactionReactService */ = { - isa = PBXGroup; - children = ( - 3A41938E2A580C57006A6B22 /* AdamantRichTransactionReactService.swift */, - ); - path = RichTransactionReactService; - sourceTree = ""; - }; - 3A770E4A2AE14EFD0008D98F /* Mappers */ = { - isa = PBXGroup; - children = ( - 3A770E4B2AE14F130008D98F /* SimpleTransactionDetails+Hashable.swift */, - ); - path = Mappers; - sourceTree = ""; - }; - 3AA2D5F8280EAF49000ED971 /* SocketService */ = { - isa = PBXGroup; - children = ( - 3AA2D5F9280EAF5D000ED971 /* AdamantSocketService.swift */, - ); - path = SocketService; - sourceTree = ""; - }; - 3AA50DED2AEBE61C00C58FC8 /* PartnerQR */ = { - isa = PBXGroup; - children = ( - 3AA50DF22AEBE67C00C58FC8 /* PartnerQRFactory.swift */, - 3AA50DEE2AEBE65D00C58FC8 /* PartnerQRView.swift */, - 3AA50DF02AEBE66A00C58FC8 /* PartnerQRViewModel.swift */, - ); - path = PartnerQR; - sourceTree = ""; - }; - 3AA6DF412BA9942300EA2E16 /* ChatFileContainerView */ = { - isa = PBXGroup; - children = ( - 3AA6DF432BA997C000EA2E16 /* FileListContainerView.swift */, - 3A299C7A2B85EABB00B54C61 /* FileListContentView.swift */, - ); - path = ChatFileContainerView; - sourceTree = ""; - }; - 3AA6DF422BA9943500EA2E16 /* MediaContainerView */ = { - isa = PBXGroup; - children = ( - 3AA6DF3F2BA9941E00EA2E16 /* MediaContainerView.swift */, - 3AA6DF452BA9BEB700EA2E16 /* MediaContentView.swift */, - ); - path = MediaContainerView; - sourceTree = ""; - }; - 3AE0A4262BC6A64900BF7125 /* FilesNetworkManager */ = { - isa = PBXGroup; - children = ( - 3AE0A42F2BC6A9BC00BF7125 /* Models */, - 3AE0A4272BC6A64900BF7125 /* FilesNetworkManager.swift */, - 3AE0A4282BC6A64900BF7125 /* IPFSApiService.swift */, - 3AE0A4292BC6A64900BF7125 /* IPFSApiCore.swift */, - 3AE0A42D2BC6A96A00BF7125 /* IPFS+Constants.swift */, - ); - path = FilesNetworkManager; - sourceTree = ""; - }; - 3AE0A42F2BC6A9BC00BF7125 /* Models */ = { - isa = PBXGroup; - children = ( - 3AE0A4302BC6A9C900BF7125 /* IPFSDTO.swift */, - 3AE0A4342BC6AA1B00BF7125 /* FileManagerError.swift */, - 9332C39C2C76BE7500164B80 /* FileApiServiceResult.swift */, - ); - path = Models; - sourceTree = ""; - }; - 3AFE7E502B1F6AFE00718739 /* WalletsService */ = { - isa = PBXGroup; - children = ( - 3AFE7E422B19E4D900718739 /* WalletServiceCompose.swift */, - E940086D2114AA2E00CD2D67 /* WalletCoreProtocol.swift */, - 3AFE7E512B1F6B3400718739 /* WalletServiceProtocol.swift */, - 3AFE7E402B18D88B00718739 /* WalletService.swift */, - ); - path = WalletsService; - sourceTree = ""; - }; - 411742FE2A39B1B1008CD98A /* Contribute */ = { - isa = PBXGroup; - children = ( - 411742FF2A39B1D2008CD98A /* ContributeFactory.swift */, - 411743012A39B208008CD98A /* ContributeState.swift */, - 411743032A39B257008CD98A /* ContributeViewModel.swift */, - 4184F1762A33173100D7B8B9 /* ContributeView.swift */, - ); - path = Contribute; - sourceTree = ""; - }; - 413AD21A29CDDD750025F255 /* ChatReply */ = { - isa = PBXGroup; - children = ( - 41A1995329D56E340031AD75 /* ChatMessageReplyCell.swift */, - 41A1995529D56EAA0031AD75 /* ChatMessageReplyCell+Model.swift */, - ); - path = ChatReply; - sourceTree = ""; - }; - 418FDE552A28BBB70055E3CD /* Helpers */ = { - isa = PBXGroup; - children = ( - 41330F7529F1509400CB587C /* AdamantCellAnimation.swift */, - 416380E02A51765F00F90E6D /* ChatReactionsView.swift */, - 3AF9DF0C2C049161009A43A8 /* CircularProgressView.swift */, - 3A2478AD2BB42967009D89E9 /* ChatDropView.swift */, - 3A299C7C2B85F98700B54C61 /* ChatFile.swift */, - 3A7FD6F42C076D85002AF7D9 /* FileMessageStatus.swift */, - ); - path = Helpers; - sourceTree = ""; - }; - 4197B9C72952FAA2004CAF64 /* VisibleWallets */ = { - isa = PBXGroup; - children = ( - 41047B6F294B5EE10039E956 /* VisibleWalletsViewController.swift */, - 41047B71294B5F210039E956 /* VisibleWalletsTableViewCell.swift */, - 4197B9C82952FAFF004CAF64 /* VisibleWalletsCheckmarkView.swift */, - 41BCB30F295C6082004B12AB /* VisibleWalletsResetTableViewCell.swift */, - ); - path = VisibleWallets; - sourceTree = ""; - }; - 41A1995029D42C160031AD75 /* ChatBaseMessage */ = { - isa = PBXGroup; - children = ( - 41A1995129D42C460031AD75 /* ChatMessageCell.swift */, - 41A1995729D5733D0031AD75 /* ChatMessageCell+Model.swift */, - ); - path = ChatBaseMessage; - sourceTree = ""; - }; - 41C1698A29E7F2EE00FEB3CB /* RichTransactionReplyService */ = { - isa = PBXGroup; - children = ( - 41C1698D29E7F36900FEB3CB /* AdamantRichTransactionReplyService.swift */, - ); - path = RichTransactionReplyService; - sourceTree = ""; - }; - 5551CC8D28A8B72D00B52AD0 /* Stubs */ = { - isa = PBXGroup; - children = ( - 5551CC8E28A8B75300B52AD0 /* ApiServiceStub.swift */, - ); - path = Stubs; - sourceTree = ""; - }; - 6403F5DC22723C2800D58779 /* Dash */ = { - isa = PBXGroup; - children = ( - 93B28EC32B076DFD007F268B /* DTO */, - 6403F5DD22723C6800D58779 /* DashMainnet.swift */, - 6403F5DF22723F6400D58779 /* DashWalletFactory.swift */, - 6403F5E122723F7500D58779 /* DashWallet.swift */, - 6403F5E322723F8C00D58779 /* DashWalletService.swift */, - 93B28EC12B076D31007F268B /* DashApiService.swift */, - 4186B339294200F4006594A3 /* DashWalletService+DynamicConstants.swift */, - A578BDE42623051C00090141 /* DashWalletService+Transactions.swift */, - 648CE3A5229AD1CD0070A2CC /* DashWalletService+Send.swift */, - 648CE3A7229AD1E20070A2CC /* DashWalletService+RichMessageProvider.swift */, - 648CE3A9229AD1F90070A2CC /* DashWalletService+RichMessageProviderWithStatusCheck.swift */, - 6403F5E522723FDA00D58779 /* DashWalletViewController.swift */, - 648CE3AB229AD2190070A2CC /* DashTransferViewController.swift */, - 648C697222916192006645F5 /* DashTransactionsViewController.swift */, - 648CE3A32299A94D0070A2CC /* DashTransactionDetailsViewController.swift */, - ); - path = Dash; - sourceTree = ""; - }; - 6449BA5D235CA0930033B936 /* ERC20 */ = { - isa = PBXGroup; - children = ( - 6449BA66235CA0930033B936 /* ERC20WalletFactory.swift */, - 6449BA60235CA0930033B936 /* ERC20Wallet.swift */, - 6449BA5E235CA0930033B936 /* ERC20WalletService.swift */, - 93FC16A02B01DE120062B507 /* ERC20ApiService.swift */, - 6449BA65235CA0930033B936 /* ERC20WalletService+Send.swift */, - 6449BA67235CA0930033B936 /* ERC20WalletService+RichMessageProvider.swift */, - 6449BA64235CA0930033B936 /* ERC20WalletService+RichMessageProviderWithStatusCheck.swift */, - 6449BA62235CA0930033B936 /* ERC20WalletViewController.swift */, - 6449BA5F235CA0930033B936 /* ERC20TransferViewController.swift */, - 6449BA61235CA0930033B936 /* ERC20TransactionDetailsViewController.swift */, - 6449BA63235CA0930033B936 /* ERC20TransactionsViewController.swift */, - ); - path = ERC20; - sourceTree = ""; - }; - 644EC35020EFA96700F40C73 /* Delegates */ = { - isa = PBXGroup; - children = ( - 93BF4A6A29E4B4B600505CD0 /* DelegatesBottomPanel */, - 644EC35120EFA9A300F40C73 /* DelegatesFactory.swift */, - 644EC35520EFAAB700F40C73 /* DelegatesListViewController.swift */, - 644EC35920EFB8E900F40C73 /* AdamantDelegateCell.swift */, - 644EC35D20F34F1E00F40C73 /* DelegateDetailsViewController.swift */, - E90EA5C221BA8BF400A2CE25 /* DelegateDetailsViewController.xib */, - ); - path = Delegates; - sourceTree = ""; - }; - 64E1C82B222E958C006C4DA7 /* Doge */ = { - isa = PBXGroup; - children = ( - 93CCAE7C2B06D9B900EA5B94 /* DTO */, - E907350D2256779C00BF02CC /* DogeMainnet.swift */, - 64E1C82C222E95E2006C4DA7 /* DogeWalletFactory.swift */, - 64E1C82E222E95F6006C4DA7 /* DogeWallet.swift */, - 64E1C830222E9617006C4DA7 /* DogeWalletService.swift */, - 93CCAE782B06D81D00EA5B94 /* DogeApiService.swift */, - 4186B337294200E8006594A3 /* DogeWalletService+DynamicConstants.swift */, - 648DD7A32237DB9E00B811FD /* DogeWalletService+Send.swift */, - 648DD7A72239147800B811FD /* DogeWalletService+RichMessageProvider.swift */, - 648DD7A92239150E00B811FD /* DogeWalletService+RichMessageProviderWithStatusCheck.swift */, - 64E1C832222EA0F0006C4DA7 /* DogeWalletViewController.swift */, - 648DD7A52237DC4000B811FD /* DogeTransferViewController.swift */, - 648DD79D2236A0B500B811FD /* DogeTransactionsViewController.swift */, - 648DD79F2236A59200B811FD /* DogeTransactionDetailsViewController.swift */, - ); - path = Doge; - sourceTree = ""; - }; - 85B405002D3012BD000AB744 /* AccountViewController */ = { - isa = PBXGroup; - children = ( - 85B405012D3012C7000AB744 /* AccountViewController+Form.swift */, - E983AE2820E65F3200497E1A /* AccountViewController.swift */, - E908473A219707200095825D /* AccountViewController+StayIn.swift */, - ); - path = AccountViewController; - sourceTree = ""; - }; - 9306E08E2CF8C4EF00A99BA4 /* PKGenerator */ = { - isa = PBXGroup; - children = ( - 9306E08F2CF8C50E00A99BA4 /* PKGeneratorFactory.swift */, - 9306E0902CF8C50E00A99BA4 /* PKGeneratorState.swift */, - 9306E0912CF8C50E00A99BA4 /* PKGeneratorView.swift */, - 9306E0922CF8C50E00A99BA4 /* PKGeneratorViewModel.swift */, - ); - path = PKGenerator; - sourceTree = ""; - }; - 931224A72C7A9F9C009E0ED0 /* ApiService */ = { - isa = PBXGroup; - children = ( - 934FD9B92C78565400336841 /* InfoServiceApiService.swift */, - 931224A82C7AA0E4009E0ED0 /* InfoServiceApiService+Extension.swift */, - ); - path = ApiService; - sourceTree = ""; - }; - 9322E87C2970435C00B8357C /* Models */ = { - isa = PBXGroup; - children = ( - 9322E874297042F000B8357C /* ChatSender.swift */, - 9322E876297042FA00B8357C /* ChatMessage.swift */, - 9390C5042976B53000270CDF /* ChatDialog.swift */, - 93A118502993167500E144CC /* ChatMessageBackgroundColor.swift */, - 93A91FD229799298001DB1F8 /* ChatStartPosition.swift */, - 41A1994329D2D3CF0031AD75 /* MessageModel.swift */, - ); - path = Models; - sourceTree = ""; - }; - 93294B7A2AAD054C00911109 /* App */ = { - isa = PBXGroup; - children = ( - 93294B7B2AAD060600911109 /* DI */, - E913C8F11FFFA51D001A83F7 /* AppDelegate.swift */, - ); - path = App; - sourceTree = ""; - }; - 93294B7B2AAD060600911109 /* DI */ = { - isa = PBXGroup; - children = ( - E9E7CD9020026FA100DFC4DB /* AppAssembly.swift */, - 93294B7C2AAD067000911109 /* AppContainer.swift */, - ); - path = DI; - sourceTree = ""; - }; - 93294B892AAD2BD900911109 /* ShareQR */ = { - isa = PBXGroup; - children = ( - E94E7B07205D4CB80042B639 /* ShareQRFactory.swift */, - E9FAE5E0203ED1AE008D3A6B /* ShareQrViewController.swift */, - E9FAE5E1203ED1AE008D3A6B /* ShareQrViewController.xib */, - ); - path = ShareQR; - sourceTree = ""; - }; - 93294B8A2AAD2C6B00911109 /* SwiftyOnboard */ = { - isa = PBXGroup; - children = ( - 93294B8B2AAD2C6B00911109 /* SwiftyOnboardPage.swift */, - 93294B8C2AAD2C6B00911109 /* SwiftyOnboard.swift */, - 93294B8D2AAD2C6B00911109 /* SwiftyOnboardOverlay.swift */, - ); - path = SwiftyOnboard; - sourceTree = ""; - }; - 93294B912AAD2CA500911109 /* Welcome */ = { - isa = PBXGroup; - children = ( - 6458548A211B3AB1004C5909 /* WelcomeViewController.xib */, - 93547BC929E2262D00B0914B /* WelcomeViewController.swift */, - ); - path = Welcome; - sourceTree = ""; - }; - 93294B942AAD31F200911109 /* ScreensFactory */ = { - isa = PBXGroup; - children = ( - 93294B952AAD320B00911109 /* ScreensFactory.swift */, - 93294B972AAD364F00911109 /* AdamantScreensFactory.swift */, - ); - path = ScreensFactory; - sourceTree = ""; - }; - 932BD15929D2F74500AA1947 /* RichMessageProviderWithStatusCheck */ = { - isa = PBXGroup; - children = ( - 9300F94529D0149100FEDDB8 /* RichMessageProviderWithStatusCheck.swift */, - ); - path = RichMessageProviderWithStatusCheck; - sourceTree = ""; - }; - 93496BA12A6CAED100DD062F /* Fonts */ = { - isa = PBXGroup; - children = ( - 93496BA22A6CAED100DD062F /* Roboto_700_normal.ttf */, - 93496BA32A6CAED100DD062F /* Exo+2_700_normal.ttf */, - 93496BA42A6CAED100DD062F /* Exo+2_100_normal.ttf */, - 93496BA52A6CAED100DD062F /* Exo+2_300_normal.ttf */, - 93496BA62A6CAED100DD062F /* Exo+2_400_normal.ttf */, - 93496BA72A6CAED100DD062F /* Exo+2_400_italic.ttf */, - 93496BA82A6CAED100DD062F /* Exo+2_500_normal.ttf */, - 93496BA92A6CAED100DD062F /* Roboto_400_italic.ttf */, - 93496BAA2A6CAED100DD062F /* Roboto_300_normal.ttf */, - 93496BAB2A6CAED100DD062F /* Roboto_400_normal.ttf */, - 93496BAC2A6CAED100DD062F /* Roboto_500_normal.ttf */, - ); - path = Fonts; - sourceTree = ""; - }; - 934FD99E2C783C9500336841 /* InfoService */ = { - isa = PBXGroup; - children = ( - 934FD99F2C783C9E00336841 /* Models */, - 934FD9A12C783CAC00336841 /* Protocols */, - 934FD9A02C783CA700336841 /* Services */, - 931224B02C7ACFE6009E0ED0 /* InfoServiceAssembly.swift */, - 937173F42C8049E0009D5191 /* InfoService+Constants.swift */, - ); - path = InfoService; - sourceTree = ""; - }; - 934FD99F2C783C9E00336841 /* Models */ = { - isa = PBXGroup; - children = ( - 934FD9A22C783D1E00336841 /* DTO */, - 934FD9A52C783DB700336841 /* InfoServiceStatus.swift */, - 934FD9AD2C7846BA00336841 /* InfoServiceHistoryItem.swift */, - 934FD9AF2C78481500336841 /* InfoServiceApiError.swift */, - 934FD9B12C7849C800336841 /* InfoServiceApiResult.swift */, - 934FD9B52C78519600336841 /* InfoServiceApiCommands.swift */, - 931224B22C7AD5DD009E0ED0 /* InfoServiceTicker.swift */, - ); - path = Models; - sourceTree = ""; - }; - 934FD9A02C783CA700336841 /* Services */ = { - isa = PBXGroup; - children = ( - 931224A72C7A9F9C009E0ED0 /* ApiService */, - 934FD9A72C783E0C00336841 /* InfoServiceMapper.swift */, - 934FD9B32C78514E00336841 /* InfoServiceApiCore.swift */, - 931224AE2C7AA88E009E0ED0 /* InfoService.swift */, - ); - path = Services; - sourceTree = ""; - }; - 934FD9A12C783CAC00336841 /* Protocols */ = { - isa = PBXGroup; - children = ( - 64EAB37322463E020018D9B2 /* InfoServiceProtocol.swift */, - 934FD9B72C7854AF00336841 /* InfoServiceMapperProtocol.swift */, - 934FD9BB2C78567300336841 /* InfoServiceApiServiceProtocol.swift */, - ); - path = Protocols; - sourceTree = ""; - }; - 934FD9A22C783D1E00336841 /* DTO */ = { - isa = PBXGroup; - children = ( - 934FD9A32C783D2E00336841 /* InfoServiceStatusDTO.swift */, - 934FD9A92C7842C800336841 /* InfoServiceResponseDTO.swift */, - 934FD9AB2C78443600336841 /* InfoServiceHistoryItemDTO.swift */, - 931224AA2C7AA212009E0ED0 /* InfoServiceRatesRequestDTO.swift */, - 931224AC2C7AA67B009E0ED0 /* InfoServiceHistoryRequestDTO.swift */, - ); - path = DTO; - sourceTree = ""; - }; - 9366588B2B0AB68300BDB2D3 /* CoinsNodesList */ = { - isa = PBXGroup; - children = ( - 936658A12B0ADE3100BDB2D3 /* View */, - 936658A02B0ADE2300BDB2D3 /* ViewModel */, - 936658A42B0AE67A00BDB2D3 /* CoinsNodesListFactory.swift */, - ); - path = CoinsNodesList; - sourceTree = ""; - }; - 936658A02B0ADE2300BDB2D3 /* ViewModel */ = { - isa = PBXGroup; - children = ( - 936658922B0AC03700BDB2D3 /* CoinsNodesListStrings.swift */, - 9366588C2B0AB6BD00BDB2D3 /* CoinsNodesListState.swift */, - 9366588E2B0AB97500BDB2D3 /* CoinsNodesListMapper.swift */, - 936658962B0ACB1500BDB2D3 /* CoinsNodesListViewModel.swift */, - ); - path = ViewModel; - sourceTree = ""; - }; - 936658A12B0ADE3100BDB2D3 /* View */ = { - isa = PBXGroup; - children = ( - 9366589C2B0ADBAF00BDB2D3 /* CoinsNodesListView.swift */, - 936658A22B0ADE4400BDB2D3 /* CoinsNodesListView+Row.swift */, - ); - path = View; - sourceTree = ""; - }; - 93760BDC2C65A1FA002507C3 /* Mnemonic */ = { - isa = PBXGroup; - children = ( - 93760BDD2C65A1FA002507C3 /* english.txt */, - ); - path = Mnemonic; - sourceTree = ""; - }; - 937736832B0949C700B35C7A /* NodeCell */ = { - isa = PBXGroup; - children = ( - E91E5BF120DAF05500B06B3C /* NodeCell.swift */, - 937736812B0949C500B35C7A /* NodeCell+Model.swift */, - ); - path = NodeCell; - sourceTree = ""; - }; - 9377FBE0296C2AB700C9211B /* ChatTransaction */ = { - isa = PBXGroup; - children = ( - 937751AA2A68BB390054BD65 /* ChatTransactionCell.swift */, - 93CC8DC5296F00C4003772BF /* Container */, - 93CC8DC4296F00B7003772BF /* Content */, - ); - path = ChatTransaction; - sourceTree = ""; - }; - 9380EF642D111BBF006939E1 /* ChatSwipeWrapper */ = { - isa = PBXGroup; - children = ( - 9380EF622D1119DD006939E1 /* ChatSwipeWrapper.swift */, - 9380EF652D111BD1006939E1 /* ChatSwipeWrapperModel.swift */, - ); - path = ChatSwipeWrapper; - sourceTree = ""; - }; - 938F7D552955C05D001915CA /* Chat */ = { - isa = PBXGroup; - children = ( - 938F7D672955C992001915CA /* ViewModel */, - 938F7D702955CBAF001915CA /* View */, - 938F7D712955CE72001915CA /* ChatFactory.swift */, - 9371E560295CD53100438F2C /* ChatLocalization.swift */, - ); - path = Chat; - sourceTree = ""; - }; - 938F7D592955C8CB001915CA /* Managers */ = { - isa = PBXGroup; - children = ( - 938F7D5A2955C8DA001915CA /* ChatDisplayManager.swift */, - 938F7D5C2955C8F9001915CA /* ChatLayoutManager.swift */, - 938F7D5E2955C90D001915CA /* ChatInputBarManager.swift */, - 938F7D602955C92B001915CA /* ChatDataSourceManager.swift */, - 9390C5022976B42800270CDF /* ChatDialogManager.swift */, - 932F77582989F999006D8801 /* ChatCellManager.swift */, - 9380EF672D112BB9006939E1 /* ChatSwipeManager.swift */, - 411DB8322A14D01F006AB158 /* ChatKeyboardManager.swift */, - 418FDE4F2A25CA340055E3CD /* ChatMenuManager.swift */, - 9340077F29AC341000A20622 /* ChatAction.swift */, - 93684A2929EFA28A00F9EFFE /* FixedTextMessageSizeCalculator.swift */, - ); - path = Managers; - sourceTree = ""; - }; - 938F7D672955C992001915CA /* ViewModel */ = { - isa = PBXGroup; - children = ( - 9322E87C2970435C00B8357C /* Models */, - 938F7D682955C9EC001915CA /* ChatViewModel.swift */, - 932B34E82974AA4A002A75BA /* ChatPreservationProtocol.swift */, - 93A118522993241D00E144CC /* ChatMessagesListFactory.swift */, - 9322E87A2970431200B8357C /* ChatMessageFactory.swift */, - 9399F5EC29A85A48006C3E30 /* ChatCacheService.swift */, - 3AF0A6C92BBAF5850019FF47 /* ChatFileService.swift */, - 3A9015A82A615893002A2464 /* ChatMessagesListViewModel.swift */, - ); - path = ViewModel; - sourceTree = ""; - }; - 938F7D702955CBAF001915CA /* View */ = { - isa = PBXGroup; - children = ( - 418FDE552A28BBB70055E3CD /* Helpers */, - 93996A9829682690008D080B /* Subviews */, - 938F7D592955C8CB001915CA /* Managers */, - 938F7D632955C94F001915CA /* ChatViewController.swift */, - 26A975FE2B7E843E0095C367 /* SelectTextView.swift */, - 26A976002B7E852E0095C367 /* ChatSelectTextViewFactory.swift */, - ); - path = View; - sourceTree = ""; - }; - 93996A9829682690008D080B /* Subviews */ = { - isa = PBXGroup; - children = ( - 9380EF642D111BBF006939E1 /* ChatSwipeWrapper */, - 3A299C742B84CE1400B54C61 /* FilesToolBarView */, - 3A299C672B838A7800B54C61 /* ChatMedia */, - 41A1995029D42C160031AD75 /* ChatBaseMessage */, - 413AD21A29CDDD750025F255 /* ChatReply */, - 9377FBE0296C2AB700C9211B /* ChatTransaction */, - 938F7D652955C966001915CA /* ChatInputBar.swift */, - 93A91FD0297972B7001DB1F8 /* ChatScrollButton.swift */, - 9371130E2996EDA900F64CF9 /* ChatRefreshMock.swift */, - 93996A962968209C008D080B /* ChatMessagesCollection.swift */, - 9382F61229DEC0A3005E6216 /* ChatModelView.swift */, - ); - path = Subviews; - sourceTree = ""; - }; - 93A18C872AAEAE5600D0AB98 /* DI */ = { - isa = PBXGroup; - children = ( - 93A18C882AAEAE7700D0AB98 /* WalletFactory.swift */, - 93294B992AAD624100911109 /* WalletFactoryCompose.swift */, - 93A18C852AAEACC100D0AB98 /* AdamantWalletFactoryCompose.swift */, - ); - path = DI; - sourceTree = ""; - }; - 93ADE06D2ACA66AF008ED641 /* TestVibration */ = { - isa = PBXGroup; - children = ( - 93ADE06E2ACA66AF008ED641 /* VibrationSelectionViewModel.swift */, - 93ADE06F2ACA66AF008ED641 /* VibrationSelectionView.swift */, - 93ADE0702ACA66AF008ED641 /* VibrationSelectionFactory.swift */, - ); - path = TestVibration; - sourceTree = ""; - }; - 93B28EC32B076DFD007F268B /* DTO */ = { - isa = PBXGroup; - children = ( - 93B28EC42B076E2C007F268B /* DashBlockchainInfoDTO.swift */, - 3AA388092B69173500125684 /* DashNetworkInfoDTO.swift */, - 93B28EC72B076E68007F268B /* DashResponseDTO.swift */, - 93B28EC92B076E88007F268B /* DashErrorDTO.swift */, - 93C794432B07725C00408826 /* DashGetAddressBalanceDTO.swift */, - 93C794472B0778C700408826 /* DashGetBlockDTO.swift */, - 93C794492B077A1C00408826 /* DashGetUnspentTransactionsDTO.swift */, - 93C7944B2B077B2700408826 /* DashGetAddressTransactionIds.swift */, - 93C7944D2B077C1F00408826 /* DashSendRawTransactionDTO.swift */, - ); - path = DTO; - sourceTree = ""; - }; - 93BF4A6A29E4B4B600505CD0 /* DelegatesBottomPanel */ = { - isa = PBXGroup; - children = ( - 93BF4A6529E4859900505CD0 /* DelegatesBottomPanel.swift */, - 93BF4A6B29E4B4BF00505CD0 /* DelegatesBottomPanel+Model.swift */, - ); - path = DelegatesBottomPanel; - sourceTree = ""; - }; - 93CC8DC4296F00B7003772BF /* Content */ = { - isa = PBXGroup; - children = ( - 9377FBDE296C2A2F00C9211B /* ChatTransactionContentView.swift */, - 9377FBE1296C2ACA00C9211B /* ChatTransactionContentView+Model.swift */, - ); - path = Content; - sourceTree = ""; - }; - 93CC8DC5296F00C4003772BF /* Container */ = { - isa = PBXGroup; - children = ( - 93CC8DC6296F00D6003772BF /* ChatTransactionContainerView.swift */, - 93CC8DC8296F01DE003772BF /* ChatTransactionContainerView+Model.swift */, - ); - path = Container; - sourceTree = ""; - }; - 93CCAE7C2B06D9B900EA5B94 /* DTO */ = { - isa = PBXGroup; - children = ( - 93CCAE7A2B06D9B500EA5B94 /* DogeBlocksDTO.swift */, - 93CCAE7D2B06DA6C00EA5B94 /* DogeBlockDTO.swift */, - 3AF53F8E2B3EE0DA00B30312 /* DogeNodeInfo.swift */, - ); - path = DTO; - sourceTree = ""; - }; - 93E123342A6DFCA6004DF33B /* NotificationsShared */ = { - isa = PBXGroup; - children = ( - 93E123352A6DFCED004DF33B /* Localization */, - ); - path = NotificationsShared; - sourceTree = ""; - }; - 93E123352A6DFCED004DF33B /* Localization */ = { - isa = PBXGroup; - children = ( - 93E123412A6DFE24004DF33B /* Localizable.stringsdict */, - 93E1233A2A6DFD15004DF33B /* Localizable.strings */, - 93E1234A2A6DFEF7004DF33B /* NotificationStrings.swift */, - ); - path = Localization; - sourceTree = ""; - }; - 93ED21442CC3547400AA1FC8 /* TransactionsStatusService */ = { - isa = PBXGroup; - children = ( - 937612702CC4C3CA0036EEB4 /* TransactionsStatusActor.swift */, - 93ED214D2CC3563A00AA1FC8 /* Services */, - 93ED21482CC3553000AA1FC8 /* Protocols */, - ); - path = TransactionsStatusService; - sourceTree = ""; - }; - 93ED21482CC3553000AA1FC8 /* Protocols */ = { - isa = PBXGroup; - children = ( - 93ED21492CC3555500AA1FC8 /* TxStatusServiceProtocol.swift */, - 93ED214B2CC3561800AA1FC8 /* TransactionsStatusServiceComposeProtocol.swift */, - ); - path = Protocols; - sourceTree = ""; - }; - 93ED214D2CC3563A00AA1FC8 /* Services */ = { - isa = PBXGroup; - children = ( - 93ED214E2CC3567600AA1FC8 /* TransactionsStatusServiceCompose.swift */, - 93ED21502CC3571200AA1FC8 /* TxStatusService.swift */, - ); - path = Services; - sourceTree = ""; - }; - A50A41022822F8CE006BDFE1 /* Bitcoin */ = { - isa = PBXGroup; - children = ( - A5E04225282A8BC70076CD13 /* DTO */, - A50A41062822F8CE006BDFE1 /* BtcWallet.swift */, - 93294B812AAD0BB400911109 /* BtcWalletFactory.swift */, - A50A41042822F8CE006BDFE1 /* BtcWalletService.swift */, - 93FC169A2B0197FD0062B507 /* BtcApiService.swift */, - 4186B331294200B4006594A3 /* BtcWalletService+DynamicConstants.swift */, - A50A410F2822FC35006BDFE1 /* BtcWalletService+Send.swift */, - A50A410E2822FC35006BDFE1 /* BtcWalletService+RichMessageProvider.swift */, - A50A410D2822FC35006BDFE1 /* BtcWalletService+RichMessageProviderWithStatusCheck.swift */, - A50A41052822F8CE006BDFE1 /* BtcWalletViewController.swift */, - A50A41102822FC35006BDFE1 /* BtcTransferViewController.swift */, - 64B5736C2201E196005DC968 /* BtcTransactionsViewController.swift */, - 64B5736E2209B892005DC968 /* BtcTransactionDetailsViewController.swift */, - ); - path = Bitcoin; - sourceTree = ""; - }; - A5E04225282A8BC70076CD13 /* DTO */ = { - isa = PBXGroup; - children = ( - A5E04226282A8BDC0076CD13 /* BtcBalanceResponse.swift */, - A5E04228282A998C0076CD13 /* BtcTransactionResponse.swift */, - A5E0422A282AB18B0076CD13 /* BtcUnspentTransactionResponse.swift */, - 3AA388042B67F4DD00125684 /* BtcBlockchainInfoDTO.swift */, - 3AA388062B67F53F00125684 /* BtcNetworkInfoDTO.swift */, - ); - path = DTO; - sourceTree = ""; - }; - B92CFC9A479739E2046C81E9 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 33975C0D891698AA7E74EBCC /* Pods_Adamant.framework */, - 36AB8CE9537B3B873972548B /* Pods_AdmCore.framework */, - E957E110229B04280019732A /* UserNotifications.framework */, - E957E112229B04280019732A /* UserNotificationsUI.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - E9079A7E229DEF9C0022CA0D /* MessageNotificationContentExtension */ = { - isa = PBXGroup; - children = ( - E9079A7F229DEF9C0022CA0D /* NotificationViewController.swift */, - E9079A81229DEF9C0022CA0D /* MainInterface.storyboard */, - E9079A84229DEF9C0022CA0D /* Info.plist */, - E9079A8B229DEFE40022CA0D /* Release.entitlements */, - E9B994BF22BFD678004CD645 /* Debug.entitlements */, - ); - path = MessageNotificationContentExtension; - sourceTree = ""; - }; - E913C8E51FFFA51D001A83F7 = { - isa = PBXGroup; - children = ( - E913C8F01FFFA51D001A83F7 /* Adamant */, - E9EC344520066D4A00C0E546 /* AdamantTests */, - 93E123342A6DFCA6004DF33B /* NotificationsShared */, - E96D64DC2295CD4700CA5587 /* NotificationServiceExtension */, - E957E130229B10F80019732A /* TransferNotificationContentExtension */, - E9079A7E229DEF9C0022CA0D /* MessageNotificationContentExtension */, - E9220E0C21988D9A009C9642 /* Recovered References */, - 0C42C8036B708BFC84C34963 /* Pods */, - B92CFC9A479739E2046C81E9 /* Frameworks */, - E913C8EF1FFFA51D001A83F7 /* Products */, - ); - sourceTree = ""; - }; - E913C8EF1FFFA51D001A83F7 /* Products */ = { - isa = PBXGroup; - children = ( - E913C8EE1FFFA51D001A83F7 /* Adamant.app */, - E9EC344420066D4A00C0E546 /* AdamantTests.xctest */, - E96D64DB2295CD4700CA5587 /* NotificationServiceExtension.appex */, - E957E12D229B10F80019732A /* TransferNotificationContentExtension.appex */, - E9079A7B229DEF9C0022CA0D /* MessageNotificationContentExtension.appex */, - ); - name = Products; - sourceTree = ""; - }; - E913C8F01FFFA51D001A83F7 /* Adamant */ = { - isa = PBXGroup; - children = ( - 93294B7A2AAD054C00911109 /* App */, - E913C9101FFFAA4B001A83F7 /* Helpers */, - E950651F20404997008352E5 /* Utilities */, - E913C9091FFFA95A001A83F7 /* Models */, - E913C9041FFFA8FE001A83F7 /* ServiceProtocols */, - E913C9061FFFA92E001A83F7 /* Services */, - E9E7CDB82003AA8E00DFC4DB /* SharedViews */, - E919479920000FFD001362F8 /* Modules */, - E913C9111FFFAB05001A83F7 /* Assets */, - E90847192196FE590095825D /* Adamant.xcdatamodeld */, - E913C8FD1FFFA51E001A83F7 /* Info.plist */, - 93E123312A6DF8EF004DF33B /* InfoPlist.strings */, - E9B994C222BFD73F004CD645 /* Release.entitlements */, - E9061B982077AF8E0011F104 /* Debug.entitlements */, - ); - path = Adamant; - sourceTree = ""; - }; - E913C9041FFFA8FE001A83F7 /* ServiceProtocols */ = { - isa = PBXGroup; - children = ( - 932BD15929D2F74500AA1947 /* RichMessageProviderWithStatusCheck */, - E9B3D398201F90320019EB36 /* DataProviders */, - E9E7CD8A20026B0600DFC4DB /* AccountService.swift */, - 6455E9F021075D3600B2E94C /* AddressBookService.swift */, - 3AA2D5F6280EADE3000ED971 /* SocketService.swift */, - 4164A9D628F17D4000EEF16D /* ChatTransactionService.swift */, - 648BCA6C213D384F00875EB5 /* AvatarService.swift */, - E9A174B22057EC47003667CD /* BackgroundFetchService.swift */, - E9E7CDBD2003AEFB00DFC4DB /* CellFactory.swift */, - E9E7CD8C20026B6600DFC4DB /* DialogService.swift */, - E90A494C204DA932009F6A65 /* LocalAuthentication.swift */, - E93D7ABD2052CEE1005D19DC /* NotificationsService.swift */, - E9215972206119FB0000CA5C /* ReachabilityMonitor.swift */, - 9304F8C1292F895C00173F18 /* PushNotificationsTokenService.swift */, - 41047B73294C61D10039E956 /* VisibleWalletsService.swift */, - 4153045A29C09C6C000E4BEA /* IncreaseFeeService.swift */, - 4184F1742A33106200D7B8B9 /* CrashlysticsService.swift */, - 3A9015A42A614A18002A2464 /* EmojiService.swift */, - 3A7BD00D2AA9BCE80045AAB0 /* VibroService.swift */, - 41C1698B29E7F34900FEB3CB /* RichTransactionReplyService.swift */, - 3A4193902A580C85006A6B22 /* RichTransactionReactService.swift */, - 3A2F55FB2AC6F885000A3F26 /* CoinStorage.swift */, - 3A96E37B2AED27F8001F5A52 /* PartnerQRService.swift */, - 3AF08D602B4EB3C400EB82B1 /* LanguageStorageProtocol.swift */, - 265AA1612B74E6B900CF98B0 /* ChatPreservation.swift */, - 3ACD307F2BBD86C800ABF671 /* FilesStorageProprietiesProtocol.swift */, - 3AE0A4322BC6A9EB00BF7125 /* FileApiServiceProtocol.swift */, - 3AE0A4362BC6AA6000BF7125 /* FilesNetworkManagerProtocol.swift */, - 3AF9DF0A2BFE306C009A43A8 /* ChatFileProtocol.swift */, - 9332C3A22C76C45A00164B80 /* ApiServiceComposeProtocol.swift */, - ); - path = ServiceProtocols; - sourceTree = ""; - }; - E913C9061FFFA92E001A83F7 /* Services */ = { - isa = PBXGroup; - children = ( - 3AE0A4262BC6A64900BF7125 /* FilesNetworkManager */, - 3A41938D2A580C3B006A6B22 /* RichTransactionReactService */, - 41C1698A29E7F2EE00FEB3CB /* RichTransactionReplyService */, - 3AA2D5F8280EAF49000ED971 /* SocketService */, - E9B3D39F201FA2090019EB36 /* DataProviders */, - E9E7CD922002740500DFC4DB /* AdamantAccountService.swift */, - 6455E9F221075D8000B2E94C /* AdamantAddressBookService.swift */, - E90A494A204D9EB8009F6A65 /* AdamantAuthentication.swift */, - E9E7CDBF2003AF6D00DFC4DB /* AdamantCellFactory.swift */, - E9E7CD8E20026CD300DFC4DB /* AdamantDialogService.swift */, - E93D7ABF2052CF63005D19DC /* AdamantNotificationService.swift */, - 41047B75294C62710039E956 /* AdamantVisibleWalletsService.swift */, - 4153045829C09902000E4BEA /* AdamantIncreaseFeeService.swift */, - 4184F1722A33102800D7B8B9 /* AdamantCrashlysticsService.swift */, - 3A9015A62A614A62002A2464 /* AdamantEmojiService.swift */, - 3A7BD00F2AA9BD030045AAB0 /* AdamantVibroService.swift */, - E921597420611A6A0000CA5C /* AdamantReachability.swift */, - E950273F202E257E002C1098 /* RepeaterService.swift */, - 9304F8C3292F8A3100173F18 /* AdamantPushNotificationsTokenService.swift */, - 3A2F55FD2AC6F90E000A3F26 /* AdamantCoinStorageService.swift */, - 3A96E3792AED27D7001F5A52 /* AdamantPartnerQRService.swift */, - 3AF08D5E2B4EB3A200EB82B1 /* LanguageService.swift */, - 3ACD307D2BBD86B700ABF671 /* FilesStorageProprietiesService.swift */, - 9332C3A42C76C4EC00164B80 /* ApiServiceCompose.swift */, - ); - path = Services; - sourceTree = ""; - }; - E913C9091FFFA95A001A83F7 /* Models */ = { - isa = PBXGroup; - children = ( - E91947B72000326B001362F8 /* ServerResponses */, - E95F859220094B8E0070534A /* CoreData */, - E9393FA92055D03300EE6F30 /* AdamantMessage.swift */, - 648CE39F22999C890070A2CC /* BaseBtcTransaction.swift */, - 648CE3A122999CE70070A2CC /* BTCRawTransaction.swift */, - 648DD7A12237D9A000B811FD /* DogeTransaction.swift */, - E923222521135F9000A7E5AF /* EthAccount.swift */, - 64BD2B7420E2814B00E2CD36 /* EthTransaction.swift */, - E940086A2114A70600CD2D67 /* LskAccount.swift */, - E9204B5120C9762300F3B9AB /* MessageStatus.swift */, - E9A03FD320DBC824007653A1 /* NodeVersion.swift */, - 3AF8D9E82C73ADFA007A7CBC /* IPFSNodeStatus.swift */, - E9FCA1E5218334C00005E83D /* SimpleTransactionDetails.swift */, - E971591921681D6900A5F904 /* TransactionStatus.swift */, - 648C696E22915A12006645F5 /* DashTransaction.swift */, - 9304F8BD292F88F900173F18 /* ANSPayload.swift */, - 3A4193992A5D554A006A6B22 /* Reaction.swift */, - 3A7BD0112AA9BD5A0045AAB0 /* AdamantVibroType.swift */, - 3A33F9F92A7A53DA002B8003 /* EmojiUpdateType.swift */, - 3A53BD452C6B7AF100BB1EE6 /* DownloadPolicy.swift */, - ); - path = Models; - sourceTree = ""; - }; - E913C9101FFFAA4B001A83F7 /* Helpers */ = { - isa = PBXGroup; - children = ( - 93775E452A674FA9009061AC /* Markdown+Adamant.swift */, - E9061B96207501E40011F104 /* AdamantUserInfoKey.swift */, - E94008862114F05B00CD2D67 /* AddressValidationResult.swift */, - E940088E2119A9E800CD2D67 /* BigInt+Decimal.swift */, - E9FAE5D9203DBFEF008D3A6B /* Comparable+clamped.swift */, - 937751AC2A68BCE10054BD65 /* MessageCellWrapper.swift */, - E96D64C72295C44400CA5587 /* Data+utilites.swift */, - 64A223D520F760BB005157CB /* Localization.swift */, - E9147B5E20500E9300145913 /* MyLittlePinpad+adamant.swift */, - E940088A2114F63000CD2D67 /* NSRegularExpression+adamant.swift */, - E9147B6220505C7500145913 /* QRCodeReader+adamant.swift */, - 6414C18D217DF43100373FA6 /* String+adamant.swift */, - E9256F5E2034C21100DE86E9 /* String+localized.swift */, - E98FC34320F920BD00032D65 /* UIFont+adamant.swift */, - 645AE06521E67D3300AD3623 /* UITextField+adamant.swift */, - 4E9EE86E28CE793D008359F7 /* SafeDecimalRow.swift */, - 9345769428FD0C34004E6C7A /* UIViewController+email.swift */, - 41CE1539297FF98200CC9254 /* Web3Swift+Adamant.swift */, - 93A2A85F2CB4733800DBC75E /* MainThreadAssembly.swift */, - 41CA598B29A0D84F002BFDE4 /* TaskManager.swift */, - 4133AF232A1CE1A3001A0A1E /* UITableView+Adamant.swift */, - 41A1994129D2D3920031AD75 /* SwipePanGestureRecognizer.swift */, - 4193AE1529FBEFBF002F21BE /* NSAttributedText+Adamant.swift */, - 93294B832AAD0C8F00911109 /* Assembler+Extension.swift */, - 93CCAE7F2B06E2D100EA5B94 /* ApiServiceError+Extension.swift */, - 939FA3412B0D6F0000710EC6 /* SelfRemovableHostingController.swift */, - 936658942B0AC15300BDB2D3 /* Node+UI.swift */, - 3AF53F8C2B3DCFA300B30312 /* NodeGroup+Constants.swift */, - 3A5DF1782C4698EC0005369D /* EdgeInsetLabel.swift */, - 93A2A8652CB4A1EE00DBC75E /* UnsafeSendableExtensions.swift */, - ); - path = Helpers; - sourceTree = ""; - }; - E913C9111FFFAB05001A83F7 /* Assets */ = { - isa = PBXGroup; - children = ( - 93760BDC2C65A1FA002507C3 /* Mnemonic */, - 93496BA12A6CAED100DD062F /* Fonts */, - E913C8F81FFFA51D001A83F7 /* Assets.xcassets */, - E9A174B820587B83003667CD /* notification.mp3 */, - 4198D57A28C8B7DA009337F2 /* so-proud-notification.mp3 */, - 4198D57C28C8B7F9009337F2 /* relax-message-tone.mp3 */, - 4198D57E28C8B834009337F2 /* short-success.mp3 */, - 4198D58028C8B8D1009337F2 /* default.mp3 */, - 269B830F2C74A2FF002AA1D7 /* note.mp3 */, - 269B83142C74B4EB002AA1D7 /* antic.mp3 */, - 269B831B2C74B4EC002AA1D7 /* cheers.mp3 */, - 269B83172C74B4EB002AA1D7 /* chord.mp3 */, - 269B83152C74B4EB002AA1D7 /* droplet.mp3 */, - 269B83122C74B4EA002AA1D7 /* handoff.mp3 */, - 269B831A2C74B4EC002AA1D7 /* milestone.mp3 */, - 269B83162C74B4EB002AA1D7 /* passage.mp3 */, - 269B83132C74B4EA002AA1D7 /* portal.mp3 */, - 269B83182C74B4EB002AA1D7 /* rattle.mp3 */, - 269B83192C74B4EB002AA1D7 /* rebound.mp3 */, - 269B831C2C74B4EC002AA1D7 /* slide.mp3 */, - 269B831D2C74B4EC002AA1D7 /* welcome.mp3 */, - E9256F752039A9A200DE86E9 /* LaunchScreen.storyboard */, - 4184F16D2A33023A00D7B8B9 /* GoogleService-Info.plist */, - ); - path = Assets; - sourceTree = ""; - }; - E919479920000FFD001362F8 /* Modules */ = { - isa = PBXGroup; - children = ( - 93ED21442CC3547400AA1FC8 /* TransactionsStatusService */, - 934FD99E2C783C9500336841 /* InfoService */, - 3A2478AF2BB45DE2009D89E9 /* StorageUsage */, - 9366588B2B0AB68300BDB2D3 /* CoinsNodesList */, - 3AA50DED2AEBE61C00C58FC8 /* PartnerQR */, - 93ADE06D2ACA66AF008ED641 /* TestVibration */, - 93294B942AAD31F200911109 /* ScreensFactory */, - 93294B912AAD2CA500911109 /* Welcome */, - 93294B8A2AAD2C6B00911109 /* SwiftyOnboard */, - 93294B892AAD2BD900911109 /* ShareQR */, - E940086C2114A8FD00CD2D67 /* Wallets */, - 938F7D552955C05D001915CA /* Chat */, - E9E7CDA52002AE1C00DFC4DB /* Account */, - E95F857B2008C8B20070534A /* ChatsList */, - 644EC35020EFA96700F40C73 /* Delegates */, - E919479A20001007001362F8 /* Login */, - E93EB09D20DA3F3A001F9601 /* NodesEditor */, - E982F69820235AF000566AC7 /* Settings */, - E9332B8721F1F9D100D56E72 /* Onboard */, - ); - path = Modules; - sourceTree = ""; - }; - E919479A20001007001362F8 /* Login */ = { - isa = PBXGroup; - children = ( - E9E7CDB22002B9FB00DFC4DB /* LoginFactory.swift */, - E905D39E204C281400DDB504 /* LoginViewController.swift */, - E9147B602050599000145913 /* LoginViewController+QR.swift */, - E9147B6E205088DE00145913 /* LoginViewController+Pinpad.swift */, - E90A4942204C5ED6009F6A65 /* EurekaPassphraseRow.swift */, - E90A4944204C5F60009F6A65 /* PassphraseCell.xib */, +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + D3A8604C2DA1C18D0007B599 /* Exceptions for "Adamant" folder in "Adamant" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, ); - path = Login; - sourceTree = ""; + target = E913C8ED1FFFA51D001A83F7 /* Adamant */; }; - E91947B72000326B001362F8 /* ServerResponses */ = { - isa = PBXGroup; - children = ( - E933475A225539390083839E /* DogeGetTransactionsResponse.swift */, - E9771DA622997F310099AAC7 /* ServerResponseWithTimestamp.swift */, - 648C697022915CB8006645F5 /* BTCRPCServerResponce.swift */, + D3A8604D2DA1C18D0007B599 /* Exceptions for "Adamant" folder in "NotificationServiceExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Assets/antic.mp3, + Assets/cheers.mp3, + Assets/chord.mp3, + Assets/default.mp3, + Assets/droplet.mp3, + Assets/handoff.mp3, + Assets/milestone.mp3, + Assets/note.mp3, + Assets/notification.mp3, + Assets/passage.mp3, + Assets/portal.mp3, + Assets/rattle.mp3, + Assets/rebound.mp3, + "Assets/relax-message-tone.mp3", + "Assets/short-success.mp3", + Assets/slide.mp3, + "Assets/so-proud-notification.mp3", + Assets/welcome.mp3, ); - path = ServerResponses; - sourceTree = ""; - }; - E9220E0C21988D9A009C9642 /* Recovered References */ = { - isa = PBXGroup; - children = ( + target = E96D64DA2295CD4700CA5587 /* NotificationServiceExtension */; + }; + D3A860EB2DA1C1940007B599 /* Exceptions for "AdamantTests" folder in "AdamantTests" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, ); - name = "Recovered References"; - sourceTree = ""; + target = E9EC344320066D4A00C0E546 /* AdamantTests */; }; - E9332B8721F1F9D100D56E72 /* Onboard */ = { - isa = PBXGroup; - children = ( - 64C65F4423893C7600DC0425 /* OnboardOverlay.swift */, - E9332B8821F1FA4400D56E72 /* OnboardFactory.swift */, - 645938922378395E00A2BE7C /* EulaViewController.swift */, - 645938932378395E00A2BE7C /* EulaViewController.xib */, - 645FEB32213E72C100D6BA2D /* OnboardViewController.swift */, - 645FEB33213E72C100D6BA2D /* OnboardViewController.xib */, - 644793C22166314A00FC4CF5 /* OnboardPage.swift */, - E96BBE3021F70F5E009AA738 /* ReadonlyTextView.swift */, + D3A8618E2DA1C1AD0007B599 /* Exceptions for "NotificationServiceExtension" folder in "NotificationServiceExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, ); - path = Onboard; - sourceTree = ""; + target = E96D64DA2295CD4700CA5587 /* NotificationServiceExtension */; }; - E93EB09D20DA3F3A001F9601 /* NodesEditor */ = { - isa = PBXGroup; - children = ( - 937736832B0949C700B35C7A /* NodeCell */, - 64D059FE20D3116A003AD655 /* NodesListViewController.swift */, - E93EB09E20DA3FA4001F9601 /* NodesEditorFactory.swift */, - E9A03FD120DBC0F2007653A1 /* NodeEditorViewController.swift */, + D3A8618F2DA1C1AD0007B599 /* Exceptions for "NotificationServiceExtension" folder in "TransferNotificationContentExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + WalletImages/bitcoin_notificationContent.png, ); - path = NodesEditor; - sourceTree = ""; + target = E957E12C229B10F80019732A /* TransferNotificationContentExtension */; }; - E940086C2114A8FD00CD2D67 /* Wallets */ = { - isa = PBXGroup; - children = ( - 3AFE7E502B1F6AFE00718739 /* WalletsService */, - 3A20D9392AE7F305005475A6 /* Models */, - 3A770E4A2AE14EFD0008D98F /* Mappers */, - 93A18C872AAEAE5600D0AB98 /* DI */, - A50A41022822F8CE006BDFE1 /* Bitcoin */, - E94008902119D22400CD2D67 /* Adamant */, - E94008792114ECF100CD2D67 /* Ethereum */, - 3A26D9312C3C1B55003AD832 /* Klayr */, - 64E1C82B222E958C006C4DA7 /* Doge */, - 6403F5DC22723C2800D58779 /* Dash */, - 6449BA5D235CA0930033B936 /* ERC20 */, - E94008712114EACF00CD2D67 /* WalletAccount.swift */, - E99818932120892F0018C84C /* WalletViewControllerBase.swift */, - E9981897212096ED0018C84C /* WalletViewControllerBase.xib */, - E9EC342020052ABB00C0E546 /* TransferViewControllerBase.swift */, - E99330252136B0E500CD5200 /* TransferViewControllerBase+QR.swift */, - E926E02D213EAABF005E536B /* TransferViewControllerBase+Alert.swift */, - E9E7CDC12003F5A400DFC4DB /* TransactionsListViewControllerBase.swift */, - E94E7B0B205D5E4A0042B639 /* TransactionsListViewControllerBase.xib */, - 64FA53D020E24941006783C9 /* TransactionDetailsViewControllerBase.swift */, - E9E7CDC52003F6D200DFC4DB /* TransactionTableViewCell.swift */, - E9484B77227C617D008E10F0 /* BalanceTableViewCell.swift */, - E9484B78227C617E008E10F0 /* BalanceTableViewCell.xib */, + D3A861992DA1C1B10007B599 /* Exceptions for "TransferNotificationContentExtension" folder in "TransferNotificationContentExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + "/Localized: MainInterface.storyboard", + NotificationViewController.swift, ); - path = Wallets; - sourceTree = ""; + target = E957E12C229B10F80019732A /* TransferNotificationContentExtension */; }; - E94008792114ECF100CD2D67 /* Ethereum */ = { - isa = PBXGroup; - children = ( - E940087C2114EDEE00CD2D67 /* EthWallet.swift */, - E993302121354BC300CD5200 /* EthWalletFactory.swift */, - E940087A2114ED0600CD2D67 /* EthWalletService.swift */, - 93FC169C2B019F440062B507 /* EthApiService.swift */, - 93CC94C02B17EE73004842AC /* EthApiCore.swift */, - 4186B333294200C5006594A3 /* EthWalletService+DynamicConstants.swift */, - E9AA8BF9212C166600F9249F /* EthWalletService+Send.swift */, - E9FEECA52143C300007DD7C8 /* EthWalletService+RichMessageProvider.swift */, - E971591B2168209800A5F904 /* EthWalletService+RichMessageProviderWithStatusCheck.swift */, - E9981895212095CA0018C84C /* EthWalletViewController.swift */, - E993301D212EF39700CD5200 /* EthTransferViewController.swift */, - 64FA53CC20E1300A006783C9 /* EthTransactionsViewController.swift */, - E96E86B721679C120061F80A /* EthTransactionDetailsViewController.swift */, + D3A861A32DA1C1B30007B599 /* Exceptions for "MessageNotificationContentExtension" folder in "MessageNotificationContentExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + "/Localized: MainInterface.storyboard", + NotificationViewController.swift, ); - path = Ethereum; - sourceTree = ""; + target = E9079A7A229DEF9C0022CA0D /* MessageNotificationContentExtension */; }; - E94008902119D22400CD2D67 /* Adamant */ = { - isa = PBXGroup; - children = ( - 93294B852AAD0E0A00911109 /* AdmWallet.swift */, - 93294B862AAD0E0A00911109 /* AdmWalletService.swift */, - E993301F21354B1800CD5200 /* AdmWalletFactory.swift */, - 4186B32F2941E642006594A3 /* AdmWalletService+DynamicConstants.swift */, - E9AA8C01212C5BF500F9249F /* AdmWalletService+Send.swift */, - E9240BF4215D686500187B09 /* AdmWalletService+RichMessageProvider.swift */, - E9B1AA562121ACBF00080A2A /* AdmWalletViewController.swift */, - E9B1AA5A21283E0F00080A2A /* AdmTransferViewController.swift */, - 64F085D820E2D7600006DE68 /* AdmTransactionsViewController.swift */, - E9DFB71B21624C9200CF8C7C /* AdmTransactionDetailsViewController.swift */, - E96BBE3221F71290009AA738 /* BuyAndSellViewController.swift */, +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + D3A85E0B2DA1C18D0007B599 /* Adamant */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + D3A8604C2DA1C18D0007B599 /* Exceptions for "Adamant" folder in "Adamant" target */, + D3A8604D2DA1C18D0007B599 /* Exceptions for "Adamant" folder in "NotificationServiceExtension" target */, ); path = Adamant; sourceTree = ""; }; - E950651F20404997008352E5 /* Utilities */ = { - isa = PBXGroup; - children = ( - 93760BDE2C65A284002507C3 /* WordList.swift */, - E9942B83203CBFCE00C163AF /* AdamantQRTools.swift */, - E950652220404C84008352E5 /* AdamantUriTools.swift */, - 93760BE02C65A2F3002507C3 /* Mnemonic+extended.swift */, - 41E3C9CB2A0E20F500AF0985 /* AdamantCoinTools.swift */, - E9E7CDB62003994E00DFC4DB /* AdamantUtilities+extended.swift */, + D3A860A12DA1C1930007B599 /* AdamantTests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + D3A860EB2DA1C1940007B599 /* Exceptions for "AdamantTests" folder in "AdamantTests" target */, ); - path = Utilities; + path = AdamantTests; sourceTree = ""; }; - E957E102229AF01E0019732A /* WalletImages */ = { - isa = PBXGroup; - children = ( - E957E103229AF7CA0019732A /* adamant_notificationContent.png */, - E957E104229AF7CA0019732A /* doge_notificationContent.png */, - E957E106229AF7CB0019732A /* ethereum_notificationContent.png */, - E957E105229AF7CA0019732A /* lisk_notificationContent.png */, - 412C0ED829124A3400DE2C5E /* dash_notificationContent.png */, - 4154413A2923AED000824478 /* bitcoin_notificationContent.png */, - 3A26D9512C3E7F1D003AD832 /* klayr_notificationContent.png */, + D3A860F82DA1C1980007B599 /* NotificationsShared */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = NotificationsShared; + sourceTree = ""; + }; + D3A861672DA1C1AD0007B599 /* NotificationServiceExtension */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + D3A8618E2DA1C1AD0007B599 /* Exceptions for "NotificationServiceExtension" folder in "NotificationServiceExtension" target */, + D3A8618F2DA1C1AD0007B599 /* Exceptions for "NotificationServiceExtension" folder in "TransferNotificationContentExtension" target */, ); - path = WalletImages; + path = NotificationServiceExtension; sourceTree = ""; }; - E957E130229B10F80019732A /* TransferNotificationContentExtension */ = { - isa = PBXGroup; - children = ( - E957E131229B10F80019732A /* NotificationViewController.swift */, - E957E133229B10F80019732A /* MainInterface.storyboard */, - E957E136229B10F80019732A /* Info.plist */, - E957E13D229B118E0019732A /* Release.entitlements */, - E9B994C022BFD6F9004CD645 /* Debug.entitlements */, + D3A861962DA1C1B10007B599 /* TransferNotificationContentExtension */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + D3A861992DA1C1B10007B599 /* Exceptions for "TransferNotificationContentExtension" folder in "TransferNotificationContentExtension" target */, ); path = TransferNotificationContentExtension; sourceTree = ""; }; - E95F857B2008C8B20070534A /* ChatsList */ = { - isa = PBXGroup; - children = ( - E95F857E2008C8D60070534A /* ChatListFactory.swift */, - E95F85842008CB3A0070534A /* ChatListViewController.swift */, - E94E7B00205D3F090042B639 /* ChatListViewController.xib */, - E9C51EF02013F18000385EB7 /* NewChatViewController.swift */, - E9AA8BF72129F13000F9249F /* ComplexTransferViewController.swift */, - E95F85C5200A9B070070534A /* ChatTableViewCell.swift */, - E95F85C6200A9B070070534A /* ChatTableViewCell.xib */, - 649D6BF121C27D5C009E727B /* SearchResultsViewController.swift */, - 6406D74821C7F06000196713 /* SearchResultsViewController.xib */, + D3A861A02DA1C1B30007B599 /* MessageNotificationContentExtension */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + D3A861A32DA1C1B30007B599 /* Exceptions for "MessageNotificationContentExtension" folder in "MessageNotificationContentExtension" target */, ); - path = ChatsList; + path = MessageNotificationContentExtension; sourceTree = ""; }; - E95F859220094B8E0070534A /* CoreData */ = { - isa = PBXGroup; - children = ( - E9960B2F21F5154300C840A8 /* BaseAccount+CoreDataClass.swift */, - 93496B822A6C85F400DD062F /* AdamantResources+CoreData.swift */, - E9960B3021F5154300C840A8 /* BaseAccount+CoreDataProperties.swift */, - E9960B3121F5154300C840A8 /* DummyAccount+CoreDataClass.swift */, - E9960B3221F5154300C840A8 /* DummyAccount+CoreDataProperties.swift */, - E90847202196FEA80095825D /* BaseTransaction+CoreDataClass.swift */, - E90847212196FEA80095825D /* BaseTransaction+CoreDataProperties.swift */, - E90847382196FEF50095825D /* BaseTransaction+TransactionDetails.swift */, - E90847222196FEA80095825D /* ChatTransaction+CoreDataClass.swift */, - E90847232196FEA80095825D /* ChatTransaction+CoreDataProperties.swift */, - E90847262196FEA80095825D /* MessageTransaction+CoreDataClass.swift */, - E90847272196FEA80095825D /* MessageTransaction+CoreDataProperties.swift */, - E90847242196FEA80095825D /* TransferTransaction+CoreDataClass.swift */, - E90847252196FEA80095825D /* TransferTransaction+CoreDataProperties.swift */, - E908471C2196FEA80095825D /* RichMessageTransaction+CoreDataClass.swift */, - E908471D2196FEA80095825D /* RichMessageTransaction+CoreDataProperties.swift */, - E908471E2196FEA80095825D /* CoreDataAccount+CoreDataClass.swift */, - E908471F2196FEA80095825D /* CoreDataAccount+CoreDataProperties.swift */, - E90847282196FEA80095825D /* Chatroom+CoreDataClass.swift */, - E90847292196FEA80095825D /* Chatroom+CoreDataProperties.swift */, - 3A2F55F72AC6F308000A3F26 /* CoinTransaction+CoreDataClass.swift */, - 3A2F55F82AC6F308000A3F26 /* CoinTransaction+CoreDataProperties.swift */, - 3A4068332ACD7C18007E87BD /* CoinTransaction+TransactionDetails.swift */, +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + E9079A78229DEF9C0022CA0D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E9079A7D229DEF9C0022CA0D /* UserNotificationsUI.framework in Frameworks */, + E9079A7C229DEF9C0022CA0D /* UserNotifications.framework in Frameworks */, + A5F929B8262C858F00C3E60A /* MarkdownKit in Frameworks */, + A5241B77262DEDEF009FA43E /* Clibsodium in Frameworks */, + 937751A92A68B3400054BD65 /* CommonKit in Frameworks */, + 55D1D851287B78FC00F94A4E /* SnapKit in Frameworks */, + A57282D5262C94E500C96FA8 /* DateToolsSwift in Frameworks */, ); - path = CoreData; - sourceTree = ""; + runOnlyForDeploymentPostprocessing = 0; }; - E95F85B8200A4D9C0070534A /* Parsing */ = { - isa = PBXGroup; - children = ( - E95F85BB200A4E670070534A /* ParsingModelsTests.swift */, - E95F85C3200A540B0070534A /* Chat.json */, - E95F85C1200A53E90070534A /* NormalizedTransaction.json */, - E95F85BD200A503A0070534A /* TransactionChat.json */, - E95F85B9200A4DC90070534A /* TransactionSend.json */, - E95F85BF200A51BB0070534A /* Account.json */, + E913C8EB1FFFA51D001A83F7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A5AC8DFF262E0B030053A7E2 /* SipHash in Frameworks */, + 3A8875EF27BBF38D00436195 /* Parchment in Frameworks */, + 3C06931576393125C61FB8F6 /* Pods_Adamant.framework in Frameworks */, + A50AEB0C262C81E300B37C22 /* QRCodeReader in Frameworks */, + 416F5EA4290162EB00EF0400 /* SocketIO in Frameworks */, + A5F92994262C855B00C3E60A /* MarkdownKit in Frameworks */, + 3A833C402B99CDA000238F6A /* FilesStorageKit in Frameworks */, + A50AEB04262C815200B37C22 /* EFQRCode in Frameworks */, + A544F0D4262C9878001F1A6D /* Eureka in Frameworks */, + 3AC76E3D2AB09118008042C4 /* ElegantEmojiPicker in Frameworks */, + A5785ADF262C63580001BC66 /* web3swift in Frameworks */, + 557AC306287B10D8004699D7 /* SnapKit in Frameworks */, + 9342F6C22A6A35E300A9B39F /* CommonKit in Frameworks */, + A5F0A04B262C9CA90009672A /* Swinject in Frameworks */, + A5DBBABD262C7221004AC028 /* Clibsodium in Frameworks */, + 6FB686162D3AAE8800CAB6DD /* AdamantWalletsKit in Frameworks */, + 938F7D582955C1DA001915CA /* MessageKit in Frameworks */, + A530B0D82842110D003F0210 /* (null) in Frameworks */, + 4177E5E12A52DA7100C089FE /* AdvancedContextMenuKit in Frameworks */, + A5D87BA3262CA01D00DC28F0 /* ProcedureKit in Frameworks */, + A5C99E0E262C9E3A00F7B1B7 /* Reachability in Frameworks */, + AA8FFFCA2D4E6435001D8576 /* CryptoSwift in Frameworks */, + A5DBBAF0262C72EF004AC028 /* LiskKit in Frameworks */, + 93FA403629401BFC00D20DB6 /* PopupKit in Frameworks */, + 4184F1712A33044E00D7B8B9 /* FirebaseCrashlytics in Frameworks */, + A5DBBAEE262C72EF004AC028 /* BitcoinKit in Frameworks */, + A57282CA262C94CD00C96FA8 /* DateToolsSwift in Frameworks */, + 3A075C9E2B98A3B100714E3B /* FilesPickerKit in Frameworks */, ); - path = Parsing; - sourceTree = ""; + runOnlyForDeploymentPostprocessing = 0; }; - E96D64C42295C0AF00CA5587 /* Core */ = { - isa = PBXGroup; - children = ( - E95F856A200789450070534A /* JSModels.swift */, - E9220E0221983155009C9642 /* JSAdamantCore.swift */, - E9220E0121983155009C9642 /* adamant-core.js */, - E95F85762007E8EC0070534A /* JSAdamantCoreTests.swift */, - E9220E07219879B9009C9642 /* NativeCoreTests.swift */, + E957E12A229B10F80019732A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E957E12F229B10F80019732A /* UserNotificationsUI.framework in Frameworks */, + E957E12E229B10F80019732A /* UserNotifications.framework in Frameworks */, + A5F929B6262C858700C3E60A /* MarkdownKit in Frameworks */, + A5241B70262DEDE1009FA43E /* Clibsodium in Frameworks */, + 937751A72A68B33A0054BD65 /* CommonKit in Frameworks */, + 55D1D84F287B78F200F94A4E /* SnapKit in Frameworks */, + A57282D3262C94DF00C96FA8 /* DateToolsSwift in Frameworks */, ); - path = Core; - sourceTree = ""; + runOnlyForDeploymentPostprocessing = 0; }; - E96D64DC2295CD4700CA5587 /* NotificationServiceExtension */ = { - isa = PBXGroup; - children = ( - E957E102229AF01E0019732A /* WalletImages */, - E96D64DD2295CD4700CA5587 /* NotificationService.swift */, - E96D64DF2295CD4700CA5587 /* Info.plist */, - E9B994C122BFD723004CD645 /* Release.entitlements */, - E9771D7D22995C870099AAC7 /* Debug.entitlements */, + E96D64D82295CD4700CA5587 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 937751A52A68B3320054BD65 /* CommonKit in Frameworks */, + A57282D1262C94DA00C96FA8 /* DateToolsSwift in Frameworks */, + A5F929AF262C857D00C3E60A /* MarkdownKit in Frameworks */, + A5241B7E262DEDFE009FA43E /* Clibsodium in Frameworks */, ); - path = NotificationServiceExtension; - sourceTree = ""; + runOnlyForDeploymentPostprocessing = 0; }; - E982F69820235AF000566AC7 /* Settings */ = { - isa = PBXGroup; - children = ( - 9306E08E2CF8C4EF00A99BA4 /* PKGenerator */, - 2621AB352C60E52900046D7A /* Notifications */, - 411742FE2A39B1B1008CD98A /* Contribute */, - 4197B9C72952FAA2004CAF64 /* VisibleWallets */, - E948E03A20235E2300975D6B /* SettingsFactory.swift */, - E90055F420EBF5DA00D0CB2D /* AboutViewController.swift */, - E90055F620EC200900D0CB2D /* SecurityViewController.swift */, - E90055F820ECD86800D0CB2D /* SecurityViewController+StayIn.swift */, - E90055FA20ECE78A00D0CB2D /* SecurityViewController+notifications.swift */, - E9942B7F203C058C00C163AF /* QRGeneratorViewController.swift */, - E9484B7C2285BAD8008E10F0 /* PrivateKeyGenerator.swift */, + E9EC344120066D4A00C0E546 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B1C2BA722D90BF34001FE840 /* SwiftyMocky in Frameworks */, + B18F4D712D986CD300F6917F /* SwiftyMockyXCTest in Frameworks */, ); - path = Settings; - sourceTree = ""; + runOnlyForDeploymentPostprocessing = 0; }; - E9B3D398201F90320019EB36 /* DataProviders */ = { +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0C42C8036B708BFC84C34963 /* Pods */ = { isa = PBXGroup; children = ( - E9B3D399201F90570019EB36 /* AccountsProvider.swift */, - E93B0D732028B21400126346 /* ChatsProvider.swift */, - E9722065201F42BB004F2AAD /* CoreDataStack.swift */, - E9B3D39D201F99F40019EB36 /* DataProvider.swift */, - E972206A201F44CA004F2AAD /* TransfersProvider.swift */, + ADDFD2FA17E41CCBD11A1733 /* Pods-Adamant.debug.xcconfig */, + AD258997F050B24C0051CC8D /* Pods-Adamant.release.xcconfig */, + 74D5744703A7ECC98E244B14 /* Pods-AdmCore.debug.xcconfig */, + 4A4D67BD3DC89C07D1351248 /* Pods-AdmCore.release.xcconfig */, ); - path = DataProviders; + path = Pods; sourceTree = ""; }; - E9B3D39F201FA2090019EB36 /* DataProviders */ = { + B92CFC9A479739E2046C81E9 /* Frameworks */ = { isa = PBXGroup; children = ( - E9B3D3A0201FA26B0019EB36 /* AdamantAccountsProvider.swift */, - E93B0D752028B28E00126346 /* AdamantChatsProvider.swift */, - E9A174B62057F1B3003667CD /* AdamantChatsProvider+backgroundFetch.swift */, - E987024820C2B1F700E393F4 /* AdamantChatsProvider+fakeMessages.swift */, - 649D6BEF21BFF481009E727B /* AdamantChatsProvider+search.swift */, - E9B3D3A8202082450019EB36 /* AdamantTransfersProvider.swift */, - E9A174B42057EDCE003667CD /* AdamantTransfersProvider+backgroundFetch.swift */, - 4164A9D828F17DA700EEF16D /* AdamantChatTransactionService.swift */, - E9722067201F42CC004F2AAD /* InMemoryCoreDataStack.swift */, - 93760BD62C656CF8002507C3 /* DefaultNodesProvider.swift */, + 33975C0D891698AA7E74EBCC /* Pods_Adamant.framework */, + 36AB8CE9537B3B873972548B /* Pods_AdmCore.framework */, + E957E110229B04280019732A /* UserNotifications.framework */, + E957E112229B04280019732A /* UserNotificationsUI.framework */, ); - path = DataProviders; + name = Frameworks; sourceTree = ""; }; - E9E7CDA52002AE1C00DFC4DB /* Account */ = { + E913C8E51FFFA51D001A83F7 = { isa = PBXGroup; children = ( - E9E7CDB02002B97B00DFC4DB /* AccountFactory.swift */, - 85B405002D3012BD000AB744 /* AccountViewController */, - E983AE2020E655C500497E1A /* AccountHeaderView.swift */, - 269E13512B594B2D008D1CA7 /* AccountFooterView.swift */, - E941CCDA20E786D700C96220 /* AccountHeader.xib */, - E983AE2C20E6720D00497E1A /* AccountFooter.xib */, - E941CCDC20E7B70200C96220 /* WalletCollectionViewCell.swift */, - E941CCDD20E7B70200C96220 /* WalletCollectionViewCell.xib */, - E9D1BE1B211DABE100E86B72 /* WalletPagingItem.swift */, + D3A85E0B2DA1C18D0007B599 /* Adamant */, + D3A860A12DA1C1930007B599 /* AdamantTests */, + D3A860F82DA1C1980007B599 /* NotificationsShared */, + D3A861672DA1C1AD0007B599 /* NotificationServiceExtension */, + D3A861962DA1C1B10007B599 /* TransferNotificationContentExtension */, + D3A861A02DA1C1B30007B599 /* MessageNotificationContentExtension */, + E9220E0C21988D9A009C9642 /* Recovered References */, + 0C42C8036B708BFC84C34963 /* Pods */, + B92CFC9A479739E2046C81E9 /* Frameworks */, + E913C8EF1FFFA51D001A83F7 /* Products */, ); - path = Account; sourceTree = ""; }; - E9E7CDB82003AA8E00DFC4DB /* SharedViews */ = { + E913C8EF1FFFA51D001A83F7 /* Products */ = { isa = PBXGroup; children = ( - E921597A206503000000CA5C /* ButtonsStripeView.swift */, - E921597C2065031D0000CA5C /* ButtonsStripe.xib */, - 93496B9F2A6CAE9300DD062F /* LogoFullHeader.xib */, - E9942B86203D9E5100C163AF /* EurekaQRRow.swift */, - E9942B88203D9ECA00C163AF /* QrCell.xib */, - E921534C20EE1E8700C0843F /* EurekaAlertLabelRow.swift */, - E921534D20EE1E8700C0843F /* AlertLabelCell.xib */, - E926E031213EC43B005E536B /* FullscreenAlertView.swift */, - 9306E0972CF8C67B00A99BA4 /* AdamantSecureField.swift */, - E9B4E1A7210F079E007E77FC /* DoubleDetailsTableViewCell.swift */, - E9B4E1A9210F08BE007E77FC /* DoubleDetailsTableViewCell.xib */, - 649D6BEB21BD5A53009E727B /* UISuffixTextField.swift */, - 55E69E162868D7920025D82E /* CheckmarkView.swift */, - 937EDFBF2C9CF6B300F219BB /* VersionFooterView.swift */, - 557AC307287B1365004699D7 /* CheckmarkRowView.swift */, - 551F66E528959A5200DE5D69 /* LoadingView.swift */, - 93F3914F2962F5D400BFD6AE /* SpinnerView.swift */, - 4133AED329769EEC00F3D017 /* UpdatingIndicatorView.swift */, - 41A1994529D2FCF80031AD75 /* ReplyView.swift */, + E913C8EE1FFFA51D001A83F7 /* Adamant.app */, + E9EC344420066D4A00C0E546 /* AdamantTests.xctest */, + E96D64DB2295CD4700CA5587 /* NotificationServiceExtension.appex */, + E957E12D229B10F80019732A /* TransferNotificationContentExtension.appex */, + E9079A7B229DEF9C0022CA0D /* MessageNotificationContentExtension.appex */, ); - path = SharedViews; + name = Products; sourceTree = ""; }; - E9EC344520066D4A00C0E546 /* AdamantTests */ = { + E9220E0C21988D9A009C9642 /* Recovered References */ = { isa = PBXGroup; children = ( - 5551CC8D28A8B72D00B52AD0 /* Stubs */, - E95F85B8200A4D9C0070534A /* Parsing */, - E96D64C42295C0AF00CA5587 /* Core */, - E950652020404BF0008352E5 /* AdamantUriBuilding.swift */, - E9EC344620066D4A00C0E546 /* AddressValidationTests.swift */, - E94883E6203F07CD00F6E1B0 /* PassphraseValidation.swift */, - E95F85702007D98D0070534A /* CurrencyFormatterTests.swift */, - E9C51ECE200E2D1100385EB7 /* FeeTests.swift */, - E95F85742007E4790070534A /* HexAndBytesUtilitiesTest.swift */, - E95F85B6200A4D8F0070534A /* TestTools.swift */, - 55D1D854287B890300F94A4E /* AddressGeneratorTests.swift */, - 551F66E72895B3DA00DE5D69 /* AdamantHealthCheckServiceTests.swift */, - 55FBAAFA28C550920066E629 /* NodesAllowanceTests.swift */, - E9EC344820066D4A00C0E546 /* Info.plist */, ); - path = AdamantTests; + name = "Recovered References"; sourceTree = ""; }; /* End PBXGroup section */ @@ -2856,9 +416,11 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + D3A860F82DA1C1980007B599 /* NotificationsShared */, + ); name = MessageNotificationContentExtension; packageProductDependencies = ( - A5DBBAE6262C72BD004AC028 /* CryptoSwift */, A5F929B7262C858F00C3E60A /* MarkdownKit */, A57282D4262C94E500C96FA8 /* DateToolsSwift */, A5241B76262DEDEF009FA43E /* Clibsodium */, @@ -2873,15 +435,14 @@ isa = PBXNativeTarget; buildConfigurationList = E913C9001FFFA51E001A83F7 /* Build configuration list for PBXNativeTarget "Adamant" */; buildPhases = ( - 9372E0412C9BC178006DF0B3 /* Run Script - Git Data */, 47866E9AB7D201F2CED0064C /* [CP] Check Pods Manifest.lock */, + 9372E0412C9BC178006DF0B3 /* Run Script - Git Data */, E913C8EA1FFFA51D001A83F7 /* Sources */, E913C8EB1FFFA51D001A83F7 /* Frameworks */, E913C8EC1FFFA51D001A83F7 /* Resources */, 629616F00016639A2AFC5FC7 /* [CP] Embed Pods Frameworks */, E96D64E62295CD4700CA5587 /* Embed Foundation Extensions */, A5AC8E01262E0B030053A7E2 /* Embed Frameworks */, - 41079EBC28AE974300C32DAF /* Run Script - SwiftLint */, ); buildRules = ( ); @@ -2890,11 +451,14 @@ E957E138229B10F80019732A /* PBXTargetDependency */, E9079A86229DEF9C0022CA0D /* PBXTargetDependency */, ); + fileSystemSynchronizedGroups = ( + D3A85E0B2DA1C18D0007B599 /* Adamant */, + D3A860F82DA1C1980007B599 /* NotificationsShared */, + ); name = Adamant; packageProductDependencies = ( A5785ADE262C63580001BC66 /* web3swift */, A5DBBABC262C7221004AC028 /* Clibsodium */, - A5DBBADB262C729B004AC028 /* CryptoSwift */, A5DBBAED262C72EF004AC028 /* BitcoinKit */, A5DBBAEF262C72EF004AC028 /* LiskKit */, A50AEB03262C815200B37C22 /* EFQRCode */, @@ -2917,6 +481,8 @@ 3AC76E3C2AB09118008042C4 /* ElegantEmojiPicker */, 3A075C9D2B98A3B100714E3B /* FilesPickerKit */, 3A833C3F2B99CDA000238F6A /* FilesStorageKit */, + 6FB686152D3AAE8800CAB6DD /* AdamantWalletsKit */, + AA8FFFC92D4E6435001D8576 /* CryptoSwift */, ); productName = Adamant; productReference = E913C8EE1FFFA51D001A83F7 /* Adamant.app */; @@ -2934,9 +500,11 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + D3A860F82DA1C1980007B599 /* NotificationsShared */, + ); name = TransferNotificationContentExtension; packageProductDependencies = ( - A5DBBAE4262C72B7004AC028 /* CryptoSwift */, A5F929B5262C858700C3E60A /* MarkdownKit */, A57282D2262C94DF00C96FA8 /* DateToolsSwift */, A5241B6F262DEDE1009FA43E /* Clibsodium */, @@ -2959,9 +527,12 @@ ); dependencies = ( ); + fileSystemSynchronizedGroups = ( + D3A860F82DA1C1980007B599 /* NotificationsShared */, + D3A861672DA1C1AD0007B599 /* NotificationServiceExtension */, + ); name = NotificationServiceExtension; packageProductDependencies = ( - A5DBBAE2262C72B0004AC028 /* CryptoSwift */, A5F929AE262C857D00C3E60A /* MarkdownKit */, A57282D0262C94DA00C96FA8 /* DateToolsSwift */, A5241B7D262DEDFE009FA43E /* Clibsodium */, @@ -2975,6 +546,7 @@ isa = PBXNativeTarget; buildConfigurationList = E9EC344B20066D4A00C0E546 /* Build configuration list for PBXNativeTarget "AdamantTests" */; buildPhases = ( + D332166F2D9AB16700D70FF8 /* Sourcery */, E9EC344020066D4A00C0E546 /* Sources */, E9EC344120066D4A00C0E546 /* Frameworks */, E9EC344220066D4A00C0E546 /* Resources */, @@ -2984,6 +556,9 @@ dependencies = ( E9EC344A20066D4A00C0E546 /* PBXTargetDependency */, ); + fileSystemSynchronizedGroups = ( + D3A860A12DA1C1930007B599 /* AdamantTests */, + ); name = AdamantTests; productName = AdamantTests; productReference = E9EC344420066D4A00C0E546 /* AdamantTests.xctest */; @@ -3051,7 +626,6 @@ }; }; buildConfigurationList = E913C8E91FFFA51D001A83F7 /* Build configuration list for PBXProject "Adamant" */; - compatibilityVersion = "Xcode 8.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -3065,7 +639,6 @@ packageReferences = ( A5785ADD262C63580001BC66 /* XCRemoteSwiftPackageReference "web3swift" */, A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium" */, - A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift" */, A50AEB02262C815200B37C22 /* XCRemoteSwiftPackageReference "EFQRCode" */, A50AEB0A262C81E300B37C22 /* XCRemoteSwiftPackageReference "QRCodeReader.swift" */, A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit" */, @@ -3081,7 +654,10 @@ 938F7D562955C1DA001915CA /* XCRemoteSwiftPackageReference "MessageKit" */, 4184F16F2A33044E00D7B8B9 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, 3AC76E3B2AB09118008042C4 /* XCRemoteSwiftPackageReference "Elegant-Emoji-Picker" */, + AA8FFFC82D4E6435001D8576 /* XCRemoteSwiftPackageReference "CryptoSwift" */, + B1C2BA702D90BF34001FE840 /* XCRemoteSwiftPackageReference "SwiftyMocky" */, ); + preferredProjectObjectVersion = 77; productRefGroup = E913C8EF1FFFA51D001A83F7 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -3100,9 +676,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - E9079A83229DEF9C0022CA0D /* MainInterface.storyboard in Resources */, - 93E123482A6DFECC004DF33B /* Localizable.strings in Resources */, - 93E123492A6DFECC004DF33B /* Localizable.stringsdict in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3110,62 +683,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - E9256F762039A9A200DE86E9 /* LaunchScreen.storyboard in Resources */, - E9981898212096ED0018C84C /* WalletViewControllerBase.xib in Resources */, - 4198D57F28C8B834009337F2 /* short-success.mp3 in Resources */, - 4198D57D28C8B7FA009337F2 /* relax-message-tone.mp3 in Resources */, - E983AE2D20E6720D00497E1A /* AccountFooter.xib in Resources */, - E95F85C8200A9B070070534A /* ChatTableViewCell.xib in Resources */, - E913C8F91FFFA51D001A83F7 /* Assets.xcassets in Resources */, - 269B83282C74B4EC002AA1D7 /* chord.mp3 in Resources */, - 93496BB42A6CAED100DD062F /* Roboto_400_italic.ttf in Resources */, - E9942B89203D9ECA00C163AF /* QrCell.xib in Resources */, - 93496BB22A6CAED100DD062F /* Exo+2_400_italic.ttf in Resources */, - 93496BAD2A6CAED100DD062F /* Roboto_700_normal.ttf in Resources */, - E921597D2065031D0000CA5C /* ButtonsStripe.xib in Resources */, - 269B83342C74B4EC002AA1D7 /* welcome.mp3 in Resources */, - 93496BAE2A6CAED100DD062F /* Exo+2_700_normal.ttf in Resources */, - 93E1232F2A6DF8EF004DF33B /* InfoPlist.strings in Resources */, - 269B83262C74B4EC002AA1D7 /* passage.mp3 in Resources */, - 269B832A2C74B4EC002AA1D7 /* rattle.mp3 in Resources */, - E90A4945204C6204009F6A65 /* PassphraseCell.xib in Resources */, - E941CCDF20E7B70200C96220 /* WalletCollectionViewCell.xib in Resources */, - 93496BB32A6CAED100DD062F /* Exo+2_500_normal.ttf in Resources */, - E90EA5C321BA8BF400A2CE25 /* DelegateDetailsViewController.xib in Resources */, - 269B83242C74B4EC002AA1D7 /* droplet.mp3 in Resources */, - E94E7B01205D3F090042B639 /* ChatListViewController.xib in Resources */, - E941CCDB20E786D800C96220 /* AccountHeader.xib in Resources */, - 269B83322C74B4EC002AA1D7 /* slide.mp3 in Resources */, - 93496BB72A6CAED100DD062F /* Roboto_500_normal.ttf in Resources */, - 269B83102C74A2FF002AA1D7 /* note.mp3 in Resources */, - 4198D58128C8B8D1009337F2 /* default.mp3 in Resources */, - 93E123382A6DFD15004DF33B /* Localizable.strings in Resources */, - 93496BB52A6CAED100DD062F /* Roboto_300_normal.ttf in Resources */, - 269B83202C74B4EC002AA1D7 /* portal.mp3 in Resources */, - 4184F16E2A33023A00D7B8B9 /* GoogleService-Info.plist in Resources */, - 93496BB62A6CAED100DD062F /* Roboto_400_normal.ttf in Resources */, - E94E7B0C205D5E4A0042B639 /* TransactionsListViewControllerBase.xib in Resources */, - E9484B7A227CA93B008E10F0 /* BalanceTableViewCell.xib in Resources */, - 6406D74A21C7F06000196713 /* SearchResultsViewController.xib in Resources */, - 269B832E2C74B4EC002AA1D7 /* milestone.mp3 in Resources */, - E9B4E1AA210F1803007E77FC /* DoubleDetailsTableViewCell.xib in Resources */, - 269B832C2C74B4EC002AA1D7 /* rebound.mp3 in Resources */, - 6458548C211B3AB1004C5909 /* WelcomeViewController.xib in Resources */, - 93496BAF2A6CAED100DD062F /* Exo+2_100_normal.ttf in Resources */, - 269B83302C74B4EC002AA1D7 /* cheers.mp3 in Resources */, - 269B831E2C74B4EC002AA1D7 /* handoff.mp3 in Resources */, - 645938952378395E00A2BE7C /* EulaViewController.xib in Resources */, - 269B83222C74B4EC002AA1D7 /* antic.mp3 in Resources */, - 93496BA02A6CAE9300DD062F /* LogoFullHeader.xib in Resources */, - E9A174B920587B84003667CD /* notification.mp3 in Resources */, - 93760BE22C65A424002507C3 /* english.txt in Resources */, - 645FEB35213E72C100D6BA2D /* OnboardViewController.xib in Resources */, - E9FAE5E3203ED1AE008D3A6B /* ShareQrViewController.xib in Resources */, - 93496BB02A6CAED100DD062F /* Exo+2_300_normal.ttf in Resources */, - E921534F20EE1E8700C0843F /* AlertLabelCell.xib in Resources */, - 93496BB12A6CAED100DD062F /* Exo+2_400_normal.ttf in Resources */, - 93E1233F2A6DFE24004DF33B /* Localizable.stringsdict in Resources */, - 4198D57B28C8B7DA009337F2 /* so-proud-notification.mp3 in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3173,10 +690,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - E957E135229B10F80019732A /* MainInterface.storyboard in Resources */, - 93E123472A6DFECB004DF33B /* Localizable.stringsdict in Resources */, - 93E123462A6DFECB004DF33B /* Localizable.strings in Resources */, - 4154413C2923AED000824478 /* bitcoin_notificationContent.png in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3184,33 +697,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2657A0CD2C707D800021E7E6 /* short-success.mp3 in Resources */, - 269B832D2C74B4EC002AA1D7 /* rebound.mp3 in Resources */, - 4154413B2923AED000824478 /* bitcoin_notificationContent.png in Resources */, - 269B83232C74B4EC002AA1D7 /* antic.mp3 in Resources */, - 2657A0CC2C707D7E0021E7E6 /* relax-message-tone.mp3 in Resources */, - E957E107229AF7CB0019732A /* adamant_notificationContent.png in Resources */, - E957E108229AF7CB0019732A /* doge_notificationContent.png in Resources */, - 93E123442A6DFECB004DF33B /* Localizable.strings in Resources */, - 269B83252C74B4EC002AA1D7 /* droplet.mp3 in Resources */, - E957E10A229AF7CB0019732A /* ethereum_notificationContent.png in Resources */, - 2657A0CA2C707D780021E7E6 /* notification.mp3 in Resources */, - 269B832F2C74B4EC002AA1D7 /* milestone.mp3 in Resources */, - 93E123452A6DFECB004DF33B /* Localizable.stringsdict in Resources */, - 2657A0CB2C707D7B0021E7E6 /* so-proud-notification.mp3 in Resources */, - 269B83332C74B4EC002AA1D7 /* slide.mp3 in Resources */, - 269B83352C74B4EC002AA1D7 /* welcome.mp3 in Resources */, - 269B83212C74B4EC002AA1D7 /* portal.mp3 in Resources */, - 269B83312C74B4EC002AA1D7 /* cheers.mp3 in Resources */, - E957E109229AF7CB0019732A /* lisk_notificationContent.png in Resources */, - 269B83112C74A34F002AA1D7 /* note.mp3 in Resources */, - 269B83292C74B4EC002AA1D7 /* chord.mp3 in Resources */, - 412C0ED929124A3400DE2C5E /* dash_notificationContent.png in Resources */, - 2657A0CE2C707D830021E7E6 /* default.mp3 in Resources */, - 269B83272C74B4EC002AA1D7 /* passage.mp3 in Resources */, - 3A26D9522C3E7F1E003AD832 /* klayr_notificationContent.png in Resources */, - 269B832B2C74B4EC002AA1D7 /* rattle.mp3 in Resources */, - 269B831F2C74B4EC002AA1D7 /* handoff.mp3 in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3218,36 +704,12 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - E96D64BF2295C06400CA5587 /* adamant-core.js in Resources */, - E96D64D02295C82B00CA5587 /* NormalizedTransaction.json in Resources */, - E96D64D22295C82B00CA5587 /* TransactionSend.json in Resources */, - E96D64D12295C82B00CA5587 /* TransactionChat.json in Resources */, - E96D64CF2295C82B00CA5587 /* Chat.json in Resources */, - E95F85C0200A51BB0070534A /* Account.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 41079EBC28AE974300C32DAF /* Run Script - SwiftLint */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Run Script - SwiftLint"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/SwiftLint/swiftlint\"\n"; - }; 47866E9AB7D201F2CED0064C /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -3275,15 +737,12 @@ buildActionMask = 2147483647; files = ( ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Adamant/Pods-Adamant-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/FreakingSimpleRoundImageView/FreakingSimpleRoundImageView.framework", - "${BUILT_PRODUCTS_DIR}/MyLittlePinpad/MyLittlePinpad.framework", + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Adamant/Pods-Adamant-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FreakingSimpleRoundImageView.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MyLittlePinpad.framework", + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Adamant/Pods-Adamant-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -3309,6 +768,24 @@ shellPath = /bin/sh; shellScript = "$SCRIPT_INPUT_FILE_0 xcode\n"; }; + D332166F2D9AB16700D70FF8 /* Sourcery */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = Sourcery; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ -x /opt/homebrew/bin/sourcery ]; then\n /opt/homebrew/bin/sourcery --config .sourcery.yml\nelse\n echo \"🚨 Sourcery is not installed. Please install it to proceed.\" >&2\n exit 1\nfi\n"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -3316,8 +793,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - E9079A80229DEF9C0022CA0D /* NotificationViewController.swift in Sources */, - 93E1234E2A6DFF62004DF33B /* NotificationStrings.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3325,477 +800,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 93CCAE7E2B06DA6C00EA5B94 /* DogeBlockDTO.swift in Sources */, - 6448C291235CA6E100F3F15B /* ERC20WalletService+RichMessageProviderWithStatusCheck.swift in Sources */, - 9332C3A32C76C45A00164B80 /* ApiServiceComposeProtocol.swift in Sources */, - E9256F5F2034C21100DE86E9 /* String+localized.swift in Sources */, - 6414C18E217DF43100373FA6 /* String+adamant.swift in Sources */, - 93A118532993241D00E144CC /* ChatMessagesListFactory.swift in Sources */, - E908472A2196FEA80095825D /* RichMessageTransaction+CoreDataClass.swift in Sources */, - 4E9EE86F28CE793D008359F7 /* SafeDecimalRow.swift in Sources */, - 93ADE0732ACA66AF008ED641 /* VibrationSelectionFactory.swift in Sources */, - 3AFE7E522B1F6B3400718739 /* WalletServiceProtocol.swift in Sources */, - 937751AB2A68BB390054BD65 /* ChatTransactionCell.swift in Sources */, - 64A223D620F760BB005157CB /* Localization.swift in Sources */, - 64E1C82F222E95F6006C4DA7 /* DogeWallet.swift in Sources */, - E9484B7D2285BAD9008E10F0 /* PrivateKeyGenerator.swift in Sources */, - E94E7B08205D4CB80042B639 /* ShareQRFactory.swift in Sources */, - 9377FBDF296C2A2F00C9211B /* ChatTransactionContentView.swift in Sources */, - E9960B3621F5154300C840A8 /* DummyAccount+CoreDataProperties.swift in Sources */, - 4186B332294200B4006594A3 /* BtcWalletService+DynamicConstants.swift in Sources */, - 3AFE7E432B19E4D900718739 /* WalletServiceCompose.swift in Sources */, - 3A26D93D2C3C1CC3003AD832 /* KlyNodeApiService.swift in Sources */, - 93A118512993167500E144CC /* ChatMessageBackgroundColor.swift in Sources */, - 93760BD72C656CF8002507C3 /* DefaultNodesProvider.swift in Sources */, - 3A26D93B2C3C1C97003AD832 /* KlyApiCore.swift in Sources */, - 2621AB372C60E74A00046D7A /* NotificationsView.swift in Sources */, - 936658A32B0ADE4400BDB2D3 /* CoinsNodesListView+Row.swift in Sources */, - 648CE3A6229AD1CD0070A2CC /* DashWalletService+Send.swift in Sources */, - 3AF9DF0B2BFE306C009A43A8 /* ChatFileProtocol.swift in Sources */, - E987024920C2B1F700E393F4 /* AdamantChatsProvider+fakeMessages.swift in Sources */, - 934FD9B02C78481500336841 /* InfoServiceApiError.swift in Sources */, - 6403F5E222723F7500D58779 /* DashWallet.swift in Sources */, - 26A975FF2B7E843E0095C367 /* SelectTextView.swift in Sources */, - 93294B822AAD0BB400911109 /* BtcWalletFactory.swift in Sources */, - 648DD7A42237DB9E00B811FD /* DogeWalletService+Send.swift in Sources */, - 93294B7D2AAD067000911109 /* AppContainer.swift in Sources */, - 93ED214C2CC3561800AA1FC8 /* TransactionsStatusServiceComposeProtocol.swift in Sources */, - 41A1994229D2D3920031AD75 /* SwipePanGestureRecognizer.swift in Sources */, - E9942B84203CBFCE00C163AF /* AdamantQRTools.swift in Sources */, - 4184F1732A33102800D7B8B9 /* AdamantCrashlysticsService.swift in Sources */, - 6403F5E422723F8C00D58779 /* DashWalletService.swift in Sources */, - 9371E561295CD53100438F2C /* ChatLocalization.swift in Sources */, - 93BF4A6629E4859900505CD0 /* DelegatesBottomPanel.swift in Sources */, - 9332C39D2C76BE7500164B80 /* FileApiServiceResult.swift in Sources */, - 9322E877297042FA00B8357C /* ChatMessage.swift in Sources */, - E908472F2196FEA80095825D /* BaseTransaction+CoreDataProperties.swift in Sources */, - 93496B832A6C85F400DD062F /* AdamantResources+CoreData.swift in Sources */, - 41A1995229D42C460031AD75 /* ChatMessageCell.swift in Sources */, - E94008872114F05B00CD2D67 /* AddressValidationResult.swift in Sources */, - E9E7CD8F20026CD300DFC4DB /* AdamantDialogService.swift in Sources */, - E993301E212EF39700CD5200 /* EthTransferViewController.swift in Sources */, - 648CE3A42299A94D0070A2CC /* DashTransactionDetailsViewController.swift in Sources */, - 934FD9AC2C78443600336841 /* InfoServiceHistoryItemDTO.swift in Sources */, - E90847362196FEA80095825D /* Chatroom+CoreDataClass.swift in Sources */, - 3A4068342ACD7C18007E87BD /* CoinTransaction+TransactionDetails.swift in Sources */, - E921597B206503000000CA5C /* ButtonsStripeView.swift in Sources */, - 93FC16A12B01DE120062B507 /* ERC20ApiService.swift in Sources */, - 3A299C692B838AA600B54C61 /* ChatMediaCell.swift in Sources */, - 93294B9A2AAD624100911109 /* WalletFactoryCompose.swift in Sources */, - 41C1698E29E7F36900FEB3CB /* AdamantRichTransactionReplyService.swift in Sources */, - 649D6BF221C27D5C009E727B /* SearchResultsViewController.swift in Sources */, - E9E7CD8D20026B6600DFC4DB /* DialogService.swift in Sources */, - 3ACD30802BBD86C800ABF671 /* FilesStorageProprietiesProtocol.swift in Sources */, - E9E7CDB72003994E00DFC4DB /* AdamantUtilities+extended.swift in Sources */, - 3A2478B32BB461A7009D89E9 /* StorageUsageViewModel.swift in Sources */, - E9147B6320505C7500145913 /* QRCodeReader+adamant.swift in Sources */, - E90055F720EC200900D0CB2D /* SecurityViewController.swift in Sources */, - 939FA3422B0D6F0000710EC6 /* SelfRemovableHostingController.swift in Sources */, - 644EC35E20F34F1E00F40C73 /* DelegateDetailsViewController.swift in Sources */, - E9942B80203C058C00C163AF /* QRGeneratorViewController.swift in Sources */, - 4184F1772A33173100D7B8B9 /* ContributeView.swift in Sources */, - 3A7BD0122AA9BD5A0045AAB0 /* AdamantVibroType.swift in Sources */, - 9306E0982CF8C67B00A99BA4 /* AdamantSecureField.swift in Sources */, - E921597520611A6A0000CA5C /* AdamantReachability.swift in Sources */, - 3A2478AE2BB42967009D89E9 /* ChatDropView.swift in Sources */, - E9960B3321F5154300C840A8 /* BaseAccount+CoreDataClass.swift in Sources */, - E9FCA1E6218334C00005E83D /* SimpleTransactionDetails.swift in Sources */, - 41A1995829D5733D0031AD75 /* ChatMessageCell+Model.swift in Sources */, - 3A299C7D2B85F98700B54C61 /* ChatFile.swift in Sources */, - 932F77592989F999006D8801 /* ChatCellManager.swift in Sources */, - 9377FBE2296C2ACA00C9211B /* ChatTransactionContentView+Model.swift in Sources */, - E933475B225539390083839E /* DogeGetTransactionsResponse.swift in Sources */, - 9340078029AC341100A20622 /* ChatAction.swift in Sources */, - 648DD7A02236A59200B811FD /* DogeTransactionDetailsViewController.swift in Sources */, - 3AA6DF402BA9941E00EA2E16 /* MediaContainerView.swift in Sources */, - 557AC308287B1365004699D7 /* CheckmarkRowView.swift in Sources */, - 9390C5052976B53000270CDF /* ChatDialog.swift in Sources */, - 269B833D2C74E661002AA1D7 /* NotificationSoundsFactory.swift in Sources */, - 6455E9F321075D8000B2E94C /* AdamantAddressBookService.swift in Sources */, - 3A26D9472C3D37B5003AD832 /* KlyWalletViewController.swift in Sources */, - 3A53BD462C6B7AF100BB1EE6 /* DownloadPolicy.swift in Sources */, - 9304F8BE292F88F900173F18 /* ANSPayload.swift in Sources */, - 41CA598C29A0D84F002BFDE4 /* TaskManager.swift in Sources */, - E9E7CD9120026FA100DFC4DB /* AppAssembly.swift in Sources */, - 64BD2B7520E2814B00E2CD36 /* EthTransaction.swift in Sources */, - 93B28EC22B076D31007F268B /* DashApiService.swift in Sources */, - E908472B2196FEA80095825D /* RichMessageTransaction+CoreDataProperties.swift in Sources */, - 269B83372C74D1F9002AA1D7 /* NotificationSoundsView.swift in Sources */, - A578BDE52623051C00090141 /* DashWalletService+Transactions.swift in Sources */, - 93C794442B07725C00408826 /* DashGetAddressBalanceDTO.swift in Sources */, - 6449BA69235CA0930033B936 /* ERC20TransferViewController.swift in Sources */, - 93684A2A29EFA28A00F9EFFE /* FixedTextMessageSizeCalculator.swift in Sources */, - E9E7CD8B20026B0600DFC4DB /* AccountService.swift in Sources */, - 41E3C9CC2A0E20F500AF0985 /* AdamantCoinTools.swift in Sources */, - 93ADE0712ACA66AF008ED641 /* VibrationSelectionViewModel.swift in Sources */, - 93CC94C12B17EE73004842AC /* EthApiCore.swift in Sources */, - 93FC169D2B019F440062B507 /* EthApiService.swift in Sources */, - 9371130F2996EDA900F64CF9 /* ChatRefreshMock.swift in Sources */, - 3AA388052B67F4DD00125684 /* BtcBlockchainInfoDTO.swift in Sources */, - 93547BCA29E2262D00B0914B /* WelcomeViewController.swift in Sources */, - 41047B74294C61D10039E956 /* VisibleWalletsService.swift in Sources */, - 3AE0A42A2BC6A64900BF7125 /* FilesNetworkManager.swift in Sources */, - 648CE3AC229AD2190070A2CC /* DashTransferViewController.swift in Sources */, - 3A7BD0102AA9BD030045AAB0 /* AdamantVibroService.swift in Sources */, - 85B405022D3012D5000AB744 /* AccountViewController+Form.swift in Sources */, - 937EDFC02C9CF6B300F219BB /* VersionFooterView.swift in Sources */, - A5E04227282A8BDC0076CD13 /* BtcBalanceResponse.swift in Sources */, - 64F085D920E2D7600006DE68 /* AdmTransactionsViewController.swift in Sources */, - 9322E87B2970431200B8357C /* ChatMessageFactory.swift in Sources */, - 648DD7AA2239150E00B811FD /* DogeWalletService+RichMessageProviderWithStatusCheck.swift in Sources */, - E9D1BE1C211DABE100E86B72 /* WalletPagingItem.swift in Sources */, - E940086E2114AA2E00CD2D67 /* WalletCoreProtocol.swift in Sources */, - 645FEB34213E72C100D6BA2D /* OnboardViewController.swift in Sources */, - E9B3D39A201F90570019EB36 /* AccountsProvider.swift in Sources */, - 4186B338294200E8006594A3 /* DogeWalletService+DynamicConstants.swift in Sources */, - 3A96E37C2AED27F8001F5A52 /* PartnerQRService.swift in Sources */, - E950652320404C84008352E5 /* AdamantUriTools.swift in Sources */, - 3A41938F2A580C57006A6B22 /* AdamantRichTransactionReactService.swift in Sources */, - 931224A92C7AA0E4009E0ED0 /* InfoServiceApiService+Extension.swift in Sources */, - E95F85C7200A9B070070534A /* ChatTableViewCell.swift in Sources */, - A50A41082822F8CE006BDFE1 /* BtcWalletService.swift in Sources */, - 937736822B0949C500B35C7A /* NodeCell+Model.swift in Sources */, - 934FD9AA2C7842C800336841 /* InfoServiceResponseDTO.swift in Sources */, - 931224AD2C7AA67B009E0ED0 /* InfoServiceHistoryRequestDTO.swift in Sources */, - 6455E9F121075D3600B2E94C /* AddressBookService.swift in Sources */, - 93C794482B0778C700408826 /* DashGetBlockDTO.swift in Sources */, - 6449BA6D235CA0930033B936 /* ERC20TransactionsViewController.swift in Sources */, - E983AE2A20E65F3200497E1A /* AccountViewController.swift in Sources */, - 3A299C762B84CE4100B54C61 /* FilesToolbarView.swift in Sources */, - 3A299C712B83975700B54C61 /* ChatMediaContnentView.swift in Sources */, - 6449BA70235CA0930033B936 /* ERC20WalletFactory.swift in Sources */, - 4184F1752A33106200D7B8B9 /* CrashlysticsService.swift in Sources */, - 4197B9C92952FAFF004CAF64 /* VisibleWalletsCheckmarkView.swift in Sources */, - E9E7CD932002740500DFC4DB /* AdamantAccountService.swift in Sources */, - 41330F7629F1509400CB587C /* AdamantCellAnimation.swift in Sources */, - 64FA53CD20E1300B006783C9 /* EthTransactionsViewController.swift in Sources */, - 6449BA6A235CA0930033B936 /* ERC20Wallet.swift in Sources */, - E9147B612050599000145913 /* LoginViewController+QR.swift in Sources */, - 9399F5ED29A85A48006C3E30 /* ChatCacheService.swift in Sources */, - 3A9015A92A615893002A2464 /* ChatMessagesListViewModel.swift in Sources */, - 936658952B0AC15300BDB2D3 /* Node+UI.swift in Sources */, - 26A976012B7E852E0095C367 /* ChatSelectTextViewFactory.swift in Sources */, - 41A1995429D56E340031AD75 /* ChatMessageReplyCell.swift in Sources */, - 93294B872AAD0E0A00911109 /* AdmWallet.swift in Sources */, - 6449BA6B235CA0930033B936 /* ERC20TransactionDetailsViewController.swift in Sources */, - E907350E2256779C00BF02CC /* DogeMainnet.swift in Sources */, - 3A299C732B83975D00B54C61 /* ChatMediaContnentView+Model.swift in Sources */, - 93ED214A2CC3555500AA1FC8 /* TxStatusServiceProtocol.swift in Sources */, - 41CE153A297FF98200CC9254 /* Web3Swift+Adamant.swift in Sources */, - 93CC8DC9296F01DE003772BF /* ChatTransactionContainerView+Model.swift in Sources */, - E9147B6F205088DE00145913 /* LoginViewController+Pinpad.swift in Sources */, - 934FD9B62C78519600336841 /* InfoServiceApiCommands.swift in Sources */, - E9FAE5E2203ED1AE008D3A6B /* ShareQrViewController.swift in Sources */, - E983AE2120E655C500497E1A /* AccountHeaderView.swift in Sources */, - E971591C2168209800A5F904 /* EthWalletService+RichMessageProviderWithStatusCheck.swift in Sources */, - 93A2A8602CB4733800DBC75E /* MainThreadAssembly.swift in Sources */, - 644EC35220EFA9A300F40C73 /* DelegatesFactory.swift in Sources */, - 3A26D9492C3D3804003AD832 /* KlyTransferViewController.swift in Sources */, - E96BBE3121F70F5E009AA738 /* ReadonlyTextView.swift in Sources */, - A50A41112822FC35006BDFE1 /* BtcWalletService+RichMessageProviderWithStatusCheck.swift in Sources */, - E926E032213EC43B005E536B /* FullscreenAlertView.swift in Sources */, - 644EC35B20EFB8E900F40C73 /* AdamantDelegateCell.swift in Sources */, 6403F5DB2272389800D58779 /* (null) in Sources */, - 648DD7A62237DC4000B811FD /* DogeTransferViewController.swift in Sources */, - 93E1234B2A6DFEF7004DF33B /* NotificationStrings.swift in Sources */, - 3AA388072B67F53F00125684 /* BtcNetworkInfoDTO.swift in Sources */, - E9960B3421F5154300C840A8 /* BaseAccount+CoreDataProperties.swift in Sources */, - 4153045929C09902000E4BEA /* AdamantIncreaseFeeService.swift in Sources */, - 3AA50DEF2AEBE65D00C58FC8 /* PartnerQRView.swift in Sources */, - E90A494B204D9EB8009F6A65 /* AdamantAuthentication.swift in Sources */, - 936658972B0ACB1500BDB2D3 /* CoinsNodesListViewModel.swift in Sources */, - E9215973206119FB0000CA5C /* ReachabilityMonitor.swift in Sources */, - 3A2F55FC2AC6F885000A3F26 /* CoinStorage.swift in Sources */, - 3AA50DF12AEBE66A00C58FC8 /* PartnerQRViewModel.swift in Sources */, - 6449BA6C235CA0930033B936 /* ERC20WalletViewController.swift in Sources */, - E9E7CDC22003F5A400DFC4DB /* TransactionsListViewControllerBase.swift in Sources */, - E905D39F204C281400DDB504 /* LoginViewController.swift in Sources */, - A50A41142822FC35006BDFE1 /* BtcTransferViewController.swift in Sources */, - 93294B8E2AAD2C6B00911109 /* SwiftyOnboardPage.swift in Sources */, - E971591A21681D6900A5F904 /* TransactionStatus.swift in Sources */, - 648CE3A022999C890070A2CC /* BaseBtcTransaction.swift in Sources */, - A50A410A2822F8CE006BDFE1 /* BtcWallet.swift in Sources */, - E908472C2196FEA80095825D /* CoreDataAccount+CoreDataClass.swift in Sources */, - E9A03FD220DBC0F2007653A1 /* NodeEditorViewController.swift in Sources */, - E9393FAA2055D03300EE6F30 /* AdamantMessage.swift in Sources */, - 938F7D692955C9EC001915CA /* ChatViewModel.swift in Sources */, - E90A494D204DA932009F6A65 /* LocalAuthentication.swift in Sources */, - 3A26D9352C3C1BE2003AD832 /* KlyWalletService.swift in Sources */, - 41047B70294B5EE10039E956 /* VisibleWalletsViewController.swift in Sources */, - 2621AB392C60E7AE00046D7A /* NotificationsViewModel.swift in Sources */, - 93B28EC82B076E68007F268B /* DashResponseDTO.swift in Sources */, - 648BCA6D213D384F00875EB5 /* AvatarService.swift in Sources */, - 3A26D94B2C3D3838003AD832 /* KlyTransactionsViewController.swift in Sources */, - E940088F2119A9E800CD2D67 /* BigInt+Decimal.swift in Sources */, - 934FD9B82C7854AF00336841 /* InfoServiceMapperProtocol.swift in Sources */, - E9E7CDC72003F6D200DFC4DB /* TransactionTableViewCell.swift in Sources */, - 938F7D5D2955C8F9001915CA /* ChatLayoutManager.swift in Sources */, - 6403F5DE22723C6800D58779 /* DashMainnet.swift in Sources */, - E940088B2114F63000CD2D67 /* NSRegularExpression+adamant.swift in Sources */, - 3AE0A4372BC6AA6000BF7125 /* FilesNetworkManagerProtocol.swift in Sources */, - 932B34E92974AA4A002A75BA /* ChatPreservationProtocol.swift in Sources */, - E9B3D3A9202082450019EB36 /* AdamantTransfersProvider.swift in Sources */, - 6449BA71235CA0930033B936 /* ERC20WalletService+RichMessageProvider.swift in Sources */, - 938F7D642955C94F001915CA /* ChatViewController.swift in Sources */, - E9A174B72057F1B3003667CD /* AdamantChatsProvider+backgroundFetch.swift in Sources */, - E96BBE3321F71290009AA738 /* BuyAndSellViewController.swift in Sources */, - 64EAB37422463E020018D9B2 /* InfoServiceProtocol.swift in Sources */, - 93CC8DC7296F00D6003772BF /* ChatTransactionContainerView.swift in Sources */, - E9E7CDB32002B9FB00DFC4DB /* LoginFactory.swift in Sources */, - E941CCDE20E7B70200C96220 /* WalletCollectionViewCell.swift in Sources */, - 4186B33A294200F4006594A3 /* DashWalletService+DynamicConstants.swift in Sources */, - 3A299C7B2B85EABB00B54C61 /* FileListContentView.swift in Sources */, - 3AF08D5F2B4EB3A200EB82B1 /* LanguageService.swift in Sources */, - E9AA8BFA212C166600F9249F /* EthWalletService+Send.swift in Sources */, - 411743042A39B257008CD98A /* ContributeViewModel.swift in Sources */, - 411DB8332A14D01F006AB158 /* ChatKeyboardManager.swift in Sources */, - 6449BA68235CA0930033B936 /* ERC20WalletService.swift in Sources */, - 3A9365A92C41332F0073D9A7 /* KLYWalletService+DynamicConstants.swift in Sources */, - 93B28ECA2B076E88007F268B /* DashErrorDTO.swift in Sources */, - 644793C32166314A00FC4CF5 /* OnboardPage.swift in Sources */, - 9345769528FD0C34004E6C7A /* UIViewController+email.swift in Sources */, - 64E1C831222E9617006C4DA7 /* DogeWalletService.swift in Sources */, - 269E13522B594B2D008D1CA7 /* AccountFooterView.swift in Sources */, - 3A4193912A580C85006A6B22 /* RichTransactionReactService.swift in Sources */, - 93FC169B2B0197FD0062B507 /* BtcApiService.swift in Sources */, - 3AA2D5F7280EADE3000ED971 /* SocketService.swift in Sources */, - 9380EF632D1119DD006939E1 /* ChatSwipeWrapper.swift in Sources */, - E95F85802008C8D70070534A /* ChatListFactory.swift in Sources */, - 41A1994429D2D3CF0031AD75 /* MessageModel.swift in Sources */, - 93775E462A674FA9009061AC /* Markdown+Adamant.swift in Sources */, - 3AE0A42C2BC6A64900BF7125 /* IPFSApiCore.swift in Sources */, - E9942B87203D9E5100C163AF /* EurekaQRRow.swift in Sources */, - 3ACD307E2BBD86B700ABF671 /* FilesStorageProprietiesService.swift in Sources */, - 3AA50DF32AEBE67C00C58FC8 /* PartnerQRFactory.swift in Sources */, - E9AA8C02212C5BF500F9249F /* AdmWalletService+Send.swift in Sources */, - E90847332196FEA80095825D /* TransferTransaction+CoreDataProperties.swift in Sources */, - 9366588D2B0AB6BD00BDB2D3 /* CoinsNodesListState.swift in Sources */, - E99818942120892F0018C84C /* WalletViewControllerBase.swift in Sources */, - 3AA6DF462BA9BEB700EA2E16 /* MediaContentView.swift in Sources */, - E9B3D39E201F99F40019EB36 /* DataProvider.swift in Sources */, - 93BF4A6C29E4B4BF00505CD0 /* DelegatesBottomPanel+Model.swift in Sources */, - 93294B882AAD0E0A00911109 /* AdmWalletService.swift in Sources */, - 648DD7A82239147800B811FD /* DogeWalletService+RichMessageProvider.swift in Sources */, - 3A2478B52BB46617009D89E9 /* StorageUsageFactory.swift in Sources */, - 3AE0A4352BC6AA1B00BF7125 /* FileManagerError.swift in Sources */, - E9E7CDC02003AF6D00DFC4DB /* AdamantCellFactory.swift in Sources */, - E9DFB71C21624C9200CF8C7C /* AdmTransactionDetailsViewController.swift in Sources */, - 6403F5E022723F6400D58779 /* DashWalletFactory.swift in Sources */, - E94008722114EACF00CD2D67 /* WalletAccount.swift in Sources */, - 3A7BD00E2AA9BCE80045AAB0 /* VibroService.swift in Sources */, - 3AF9DF0D2C049161009A43A8 /* CircularProgressView.swift in Sources */, - 9306E0932CF8C50E00A99BA4 /* PKGeneratorView.swift in Sources */, - 9306E0942CF8C50E00A99BA4 /* PKGeneratorFactory.swift in Sources */, - 9306E0952CF8C50E00A99BA4 /* PKGeneratorViewModel.swift in Sources */, - 9306E0962CF8C50E00A99BA4 /* PKGeneratorState.swift in Sources */, - E93B0D742028B21400126346 /* ChatsProvider.swift in Sources */, - A50A41092822F8CE006BDFE1 /* BtcWalletViewController.swift in Sources */, - E9722066201F42BB004F2AAD /* CoreDataStack.swift in Sources */, - E913C8F21FFFA51D001A83F7 /* AppDelegate.swift in Sources */, - 648CE3A222999CE70070A2CC /* BTCRawTransaction.swift in Sources */, - 648DD79E2236A0B500B811FD /* DogeTransactionsViewController.swift in Sources */, - 3A26D9392C3C1C62003AD832 /* KlyWalletFactory.swift in Sources */, - 3A299C6B2B838F2300B54C61 /* ChatMediaContainerView.swift in Sources */, - 64B5736F2209B892005DC968 /* BtcTransactionDetailsViewController.swift in Sources */, - 938F7D612955C92B001915CA /* ChatDataSourceManager.swift in Sources */, - E96D64C82295C44400CA5587 /* Data+utilites.swift in Sources */, - 64E1C82D222E95E2006C4DA7 /* DogeWalletFactory.swift in Sources */, - E90055F920ECD86800D0CB2D /* SecurityViewController+StayIn.swift in Sources */, - E90847322196FEA80095825D /* TransferTransaction+CoreDataClass.swift in Sources */, - 93760BDF2C65A284002507C3 /* WordList.swift in Sources */, - 93996A972968209C008D080B /* ChatMessagesCollection.swift in Sources */, - 645AE06621E67D3300AD3623 /* UITextField+adamant.swift in Sources */, - 41047B72294B5F210039E956 /* VisibleWalletsTableViewCell.swift in Sources */, - E90847392196FEF50095825D /* BaseTransaction+TransactionDetails.swift in Sources */, - 3AE0A4312BC6A9C900BF7125 /* IPFSDTO.swift in Sources */, - 649D6BF021BFF481009E727B /* AdamantChatsProvider+search.swift in Sources */, - E908473B219707200095825D /* AccountViewController+StayIn.swift in Sources */, - 3AF0A6CA2BBAF5850019FF47 /* ChatFileService.swift in Sources */, - 93C7944C2B077B2700408826 /* DashGetAddressTransactionIds.swift in Sources */, - 3A26D9502C3D3A5A003AD832 /* KlyWalletService+WalletCore.swift in Sources */, - E9204B5220C9762400F3B9AB /* MessageStatus.swift in Sources */, - 3A770E4C2AE14F130008D98F /* SimpleTransactionDetails+Hashable.swift in Sources */, - E908471B2196FE590095825D /* Adamant.xcdatamodeld in Sources */, - E940087B2114ED0600CD2D67 /* EthWalletService.swift in Sources */, - 93294B902AAD2C6B00911109 /* SwiftyOnboardOverlay.swift in Sources */, - E948E03B20235E2300975D6B /* SettingsFactory.swift in Sources */, - 4186B3302941E642006594A3 /* AdmWalletService+DynamicConstants.swift in Sources */, - E95F85852008CB3A0070534A /* ChatListViewController.swift in Sources */, - 3AE0A42E2BC6A96B00BF7125 /* IPFS+Constants.swift in Sources */, - 9380EF682D112BB9006939E1 /* ChatSwipeManager.swift in Sources */, - E9FEECA62143C300007DD7C8 /* EthWalletService+RichMessageProvider.swift in Sources */, - 6403F5E622723FDA00D58779 /* DashWalletViewController.swift in Sources */, - 931224AF2C7AA88E009E0ED0 /* InfoService.swift in Sources */, - 93C7944E2B077C1F00408826 /* DashSendRawTransactionDTO.swift in Sources */, - 4193AE1629FBEFBF002F21BE /* NSAttributedText+Adamant.swift in Sources */, - 931224AB2C7AA212009E0ED0 /* InfoServiceRatesRequestDTO.swift in Sources */, - 4164A9D928F17DA700EEF16D /* AdamantChatTransactionService.swift in Sources */, - E993302221354BC300CD5200 /* EthWalletFactory.swift in Sources */, - 418FDE502A25CA340055E3CD /* ChatMenuManager.swift in Sources */, - E90055F520EBF5DA00D0CB2D /* AboutViewController.swift in Sources */, - E908472E2196FEA80095825D /* BaseTransaction+CoreDataClass.swift in Sources */, - 64D059FF20D3116B003AD655 /* NodesListViewController.swift in Sources */, - E91E5BF220DAF05500B06B3C /* NodeCell.swift in Sources */, - 4133AF242A1CE1A3001A0A1E /* UITableView+Adamant.swift in Sources */, - 41A1995629D56EAA0031AD75 /* ChatMessageReplyCell+Model.swift in Sources */, - 934FD9BC2C78567300336841 /* InfoServiceApiServiceProtocol.swift in Sources */, - E90847312196FEA80095825D /* ChatTransaction+CoreDataProperties.swift in Sources */, - 3A2F55FA2AC6F308000A3F26 /* CoinTransaction+CoreDataProperties.swift in Sources */, - E99330262136B0E500CD5200 /* TransferViewControllerBase+QR.swift in Sources */, - E9B1AA5B21283E0F00080A2A /* AdmTransferViewController.swift in Sources */, - 3A26D94D2C3D387B003AD832 /* KlyTransactionDetailsViewController.swift in Sources */, - 648C697322916192006645F5 /* DashTransactionsViewController.swift in Sources */, - 3AA6DF442BA997C000EA2E16 /* FileListContainerView.swift in Sources */, - 55E69E172868D7920025D82E /* CheckmarkView.swift in Sources */, - 9304F8C2292F895C00173F18 /* PushNotificationsTokenService.swift in Sources */, - E940086B2114A70600CD2D67 /* LskAccount.swift in Sources */, - E9B3D3A1201FA26B0019EB36 /* AdamantAccountsProvider.swift in Sources */, - E9FAE5DA203DBFEF008D3A6B /* Comparable+clamped.swift in Sources */, - 93A91FD329799298001DB1F8 /* ChatStartPosition.swift in Sources */, - 3A2F55F92AC6F308000A3F26 /* CoinTransaction+CoreDataClass.swift in Sources */, - 93A91FD1297972B7001DB1F8 /* ChatScrollButton.swift in Sources */, - 3AA3880A2B69173500125684 /* DashNetworkInfoDTO.swift in Sources */, - 41C1698C29E7F34900FEB3CB /* RichTransactionReplyService.swift in Sources */, - 9390C5032976B42800270CDF /* ChatDialogManager.swift in Sources */, - E926E02E213EAABF005E536B /* TransferViewControllerBase+Alert.swift in Sources */, - 936658A52B0AE67A00BDB2D3 /* CoinsNodesListFactory.swift in Sources */, - 2621AB3B2C613C8100046D7A /* NotificationsFactory.swift in Sources */, - E9B1AA572121ACC000080A2A /* AdmWalletViewController.swift in Sources */, - 93C7944A2B077A1C00408826 /* DashGetUnspentTransactionsDTO.swift in Sources */, - E9240BF5215D686500187B09 /* AdmWalletService+RichMessageProvider.swift in Sources */, - 648C697122915CB8006645F5 /* BTCRPCServerResponce.swift in Sources */, - E9A174B32057EC47003667CD /* BackgroundFetchService.swift in Sources */, - 3AE0A4332BC6A9EB00BF7125 /* FileApiServiceProtocol.swift in Sources */, - E9E7CDBE2003AEFB00DFC4DB /* CellFactory.swift in Sources */, - 3A299C782B84D11100B54C61 /* FilesToolbarCollectionViewCell.swift in Sources */, - 93ED21512CC3571200AA1FC8 /* TxStatusService.swift in Sources */, - 9366589D2B0ADBAF00BDB2D3 /* CoinsNodesListView.swift in Sources */, - 411743022A39B208008CD98A /* ContributeState.swift in Sources */, - 9366588F2B0AB97500BDB2D3 /* CoinsNodesListMapper.swift in Sources */, - 93F391502962F5D400BFD6AE /* SpinnerView.swift in Sources */, - 93A18C892AAEAE7700D0AB98 /* WalletFactory.swift in Sources */, - E923222621135F9000A7E5AF /* EthAccount.swift in Sources */, - 3A7FD6F52C076D86002AF7D9 /* FileMessageStatus.swift in Sources */, - E9061B97207501E40011F104 /* AdamantUserInfoKey.swift in Sources */, - 934FD9A42C783D2E00336841 /* InfoServiceStatusDTO.swift in Sources */, - 3A299C6D2B838F8F00B54C61 /* ChatMediaContainerView+Model.swift in Sources */, - E93D7ABE2052CEE1005D19DC /* NotificationsService.swift in Sources */, - E940087D2114EDEE00CD2D67 /* EthWallet.swift in Sources */, - A5E0422B282AB18B0076CD13 /* BtcUnspentTransactionResponse.swift in Sources */, - E972206B201F44CA004F2AAD /* TransfersProvider.swift in Sources */, - 3A20D93B2AE7F316005475A6 /* AdamantTransactionDetails.swift in Sources */, - 93294B962AAD320B00911109 /* ScreensFactory.swift in Sources */, - 3A9015A72A614A62002A2464 /* AdamantEmojiService.swift in Sources */, - 93ADE0722ACA66AF008ED641 /* VibrationSelectionView.swift in Sources */, - 931224B12C7ACFE6009E0ED0 /* InfoServiceAssembly.swift in Sources */, - 648C696F22915A12006645F5 /* DashTransaction.swift in Sources */, - 3AF53F8F2B3EE0DA00B30312 /* DogeNodeInfo.swift in Sources */, - 3A41939A2A5D554A006A6B22 /* Reaction.swift in Sources */, - 6449BA6F235CA0930033B936 /* ERC20WalletService+Send.swift in Sources */, - A50A41132822FC35006BDFE1 /* BtcWalletService+Send.swift in Sources */, - A5E04229282A998C0076CD13 /* BtcTransactionResponse.swift in Sources */, - 4186B334294200C5006594A3 /* EthWalletService+DynamicConstants.swift in Sources */, - 3A26D93F2C3C1CED003AD832 /* KlyServiceApiService.swift in Sources */, - 93CCAE802B06E2D100EA5B94 /* ApiServiceError+Extension.swift in Sources */, - 93294B842AAD0C8F00911109 /* Assembler+Extension.swift in Sources */, - 938F7D5B2955C8DA001915CA /* ChatDisplayManager.swift in Sources */, - E9722068201F42CC004F2AAD /* InMemoryCoreDataStack.swift in Sources */, - 4133AED429769EEC00F3D017 /* UpdatingIndicatorView.swift in Sources */, - 551F66E628959A5300DE5D69 /* LoadingView.swift in Sources */, - 934FD9A62C783DB700336841 /* InfoServiceStatus.swift in Sources */, - 93B28EC52B076E2C007F268B /* DashBlockchainInfoDTO.swift in Sources */, - E98FC34420F920BD00032D65 /* UIFont+adamant.swift in Sources */, - 644EC35720EFAAB700F40C73 /* DelegatesListViewController.swift in Sources */, - 4153045B29C09C6C000E4BEA /* IncreaseFeeService.swift in Sources */, - E9C51EF12013F18000385EB7 /* NewChatViewController.swift in Sources */, - E9B4E1A8210F079E007E77FC /* DoubleDetailsTableViewCell.swift in Sources */, - E9502740202E257E002C1098 /* RepeaterService.swift in Sources */, - E93D7AC02052CF63005D19DC /* AdamantNotificationService.swift in Sources */, - 934FD9B42C78514E00336841 /* InfoServiceApiCore.swift in Sources */, - 3AF53F8D2B3DCFA300B30312 /* NodeGroup+Constants.swift in Sources */, - 411743002A39B1D2008CD98A /* ContributeFactory.swift in Sources */, - 93A2A8662CB4A1EE00DBC75E /* UnsafeSendableExtensions.swift in Sources */, - A5E04224282A830B0076CD13 /* BtcTransactionsViewController.swift in Sources */, - 645938942378395E00A2BE7C /* EulaViewController.swift in Sources */, - 3AA2D5FA280EAF5D000ED971 /* AdamantSocketService.swift in Sources */, - 649D6BEC21BD5A53009E727B /* UISuffixTextField.swift in Sources */, - E93B0D762028B28E00126346 /* AdamantChatsProvider.swift in Sources */, - 9380EF662D111BD1006939E1 /* ChatSwipeWrapperModel.swift in Sources */, - 3A33F9FA2A7A53DA002B8003 /* EmojiUpdateType.swift in Sources */, - 934FD9A82C783E0C00336841 /* InfoServiceMapper.swift in Sources */, - 936658932B0AC03700BDB2D3 /* CoinsNodesListStrings.swift in Sources */, - 3A5DF1792C4698EC0005369D /* EdgeInsetLabel.swift in Sources */, - 3AF8D9E92C73ADFA007A7CBC /* IPFSNodeStatus.swift in Sources */, - E993302021354B1800CD5200 /* AdmWalletFactory.swift in Sources */, - E9332B8921F1FA4400D56E72 /* OnboardFactory.swift in Sources */, - 938F7D722955CE72001915CA /* ChatFactory.swift in Sources */, - 3A26D9372C3C1C01003AD832 /* KlyWallet.swift in Sources */, - 93CCAE792B06D81D00EA5B94 /* DogeApiService.swift in Sources */, - 938F7D5F2955C90D001915CA /* ChatInputBarManager.swift in Sources */, - E908472D2196FEA80095825D /* CoreDataAccount+CoreDataProperties.swift in Sources */, - 64FA53D120E24942006783C9 /* TransactionDetailsViewControllerBase.swift in Sources */, - 41047B76294C62710039E956 /* AdamantVisibleWalletsService.swift in Sources */, - 93294B982AAD364F00911109 /* AdamantScreensFactory.swift in Sources */, - 64BD2B7720E2820300E2CD36 /* TransactionDetails.swift in Sources */, - 3A9015A52A614A18002A2464 /* EmojiService.swift in Sources */, - 9322E875297042F000B8357C /* ChatSender.swift in Sources */, - 93ED214F2CC3567600AA1FC8 /* TransactionsStatusServiceCompose.swift in Sources */, - E96E86B821679C120061F80A /* EthTransactionDetailsViewController.swift in Sources */, - 3A26D9432C3C2E19003AD832 /* KlyWalletService+StatusCheck.swift in Sources */, - 265AA1622B74E6B900CF98B0 /* ChatPreservation.swift in Sources */, - 4164A9D728F17D4000EEF16D /* ChatTransactionService.swift in Sources */, - 3A2478B12BB45DF8009D89E9 /* StorageUsageView.swift in Sources */, - E90A4943204C5ED6009F6A65 /* EurekaPassphraseRow.swift in Sources */, - E90847302196FEA80095825D /* ChatTransaction+CoreDataClass.swift in Sources */, - 3A96E37A2AED27D7001F5A52 /* AdamantPartnerQRService.swift in Sources */, - E9EC342120052ABB00C0E546 /* TransferViewControllerBase.swift in Sources */, - 937173F52C8049E0009D5191 /* InfoService+Constants.swift in Sources */, - 931224B32C7AD5DD009E0ED0 /* InfoServiceTicker.swift in Sources */, - 9304F8C4292F8A3100173F18 /* AdamantPushNotificationsTokenService.swift in Sources */, - 9300F94629D0149100FEDDB8 /* RichMessageProviderWithStatusCheck.swift in Sources */, - 269B833A2C74D4AA002AA1D7 /* NotificationSoundsViewModel.swift in Sources */, - 416380E12A51765F00F90E6D /* ChatReactionsView.swift in Sources */, - E921534E20EE1E8700C0843F /* EurekaAlertLabelRow.swift in Sources */, - 3AE0A42B2BC6A64900BF7125 /* IPFSApiService.swift in Sources */, - 64C65F4523893C7600DC0425 /* OnboardOverlay.swift in Sources */, - 93A18C862AAEACC100D0AB98 /* AdamantWalletFactoryCompose.swift in Sources */, - 3A26D9452C3D336A003AD832 /* KlyWalletService+RichMessageProvider.swift in Sources */, - E9484B79227C617E008E10F0 /* BalanceTableViewCell.swift in Sources */, - E90847352196FEA80095825D /* MessageTransaction+CoreDataProperties.swift in Sources */, - 937612712CC4C3CA0036EEB4 /* TransactionsStatusActor.swift in Sources */, - E9771DA722997F310099AAC7 /* ServerResponseWithTimestamp.swift in Sources */, - 934FD9AE2C7846BA00336841 /* InfoServiceHistoryItem.swift in Sources */, - E9A03FD420DBC824007653A1 /* NodeVersion.swift in Sources */, - 648CE3AA229AD1F90070A2CC /* DashWalletService+RichMessageProviderWithStatusCheck.swift in Sources */, - 937751AD2A68BCE10054BD65 /* MessageCellWrapper.swift in Sources */, - 41A1994629D2FCF80031AD75 /* ReplyView.swift in Sources */, - E90847342196FEA80095825D /* MessageTransaction+CoreDataClass.swift in Sources */, - E9960B3521F5154300C840A8 /* DummyAccount+CoreDataClass.swift in Sources */, - 64E1C833222EA0F0006C4DA7 /* DogeWalletViewController.swift in Sources */, - 934FD9B22C7849C800336841 /* InfoServiceApiResult.swift in Sources */, - E93EB09F20DA3FA4001F9601 /* NodesEditorFactory.swift in Sources */, - 93294B8F2AAD2C6B00911109 /* SwiftyOnboard.swift in Sources */, - 93760BE12C65A2F3002507C3 /* Mnemonic+extended.swift in Sources */, - 41BCB310295C6082004B12AB /* VisibleWalletsResetTableViewCell.swift in Sources */, - 93CCAE7B2B06D9B500EA5B94 /* DogeBlocksDTO.swift in Sources */, - 3AF08D612B4EB3C400EB82B1 /* LanguageStorageProtocol.swift in Sources */, - E9E7CDB12002B97B00DFC4DB /* AccountFactory.swift in Sources */, - E9AA8BF82129F13000F9249F /* ComplexTransferViewController.swift in Sources */, - E9A174B52057EDCE003667CD /* AdamantTransfersProvider+backgroundFetch.swift in Sources */, - 934FD9BA2C78565400336841 /* InfoServiceApiService.swift in Sources */, - 9382F61329DEC0A3005E6216 /* ChatModelView.swift in Sources */, - E9147B5F20500E9300145913 /* MyLittlePinpad+adamant.swift in Sources */, - E9981896212095CA0018C84C /* EthWalletViewController.swift in Sources */, - 648DD7A22237D9A000B811FD /* DogeTransaction.swift in Sources */, - E90847372196FEA80095825D /* Chatroom+CoreDataProperties.swift in Sources */, - 648CE3A8229AD1E20070A2CC /* DashWalletService+RichMessageProvider.swift in Sources */, - 9332C3A52C76C4EC00164B80 /* ApiServiceCompose.swift in Sources */, - E90055FB20ECE78A00D0CB2D /* SecurityViewController+notifications.swift in Sources */, - 938F7D662955C966001915CA /* ChatInputBar.swift in Sources */, - A50A41122822FC35006BDFE1 /* BtcWalletService+RichMessageProvider.swift in Sources */, - 3AFE7E412B18D88B00718739 /* WalletService.swift in Sources */, - 3A2F55FE2AC6F90E000A3F26 /* AdamantCoinStorageService.swift in Sources */, - 3A26D9412C3C2DC4003AD832 /* KlyWalletService+Send.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3803,8 +808,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - E957E132229B10F80019732A /* NotificationViewController.swift in Sources */, - 93E1234D2A6DFF62004DF33B /* NotificationStrings.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3812,8 +815,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - E96D64DE2295CD4700CA5587 /* NotificationService.swift in Sources */, - 93E1234C2A6DFF62004DF33B /* NotificationStrings.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3821,22 +822,6 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - E9C51ECF200E2D1100385EB7 /* FeeTests.swift in Sources */, - 5551CC8F28A8B75300B52AD0 /* ApiServiceStub.swift in Sources */, - E9EC344720066D4A00C0E546 /* AddressValidationTests.swift in Sources */, - E94883E7203F07CD00F6E1B0 /* PassphraseValidation.swift in Sources */, - E95F85B7200A4D8F0070534A /* TestTools.swift in Sources */, - E95F85BC200A4E670070534A /* ParsingModelsTests.swift in Sources */, - 55FBAAFB28C550920066E629 /* NodesAllowanceTests.swift in Sources */, - E96D64C02295C06400CA5587 /* JSModels.swift in Sources */, - E96D64BE2295C06400CA5587 /* JSAdamantCore.swift in Sources */, - E95F85752007E4790070534A /* HexAndBytesUtilitiesTest.swift in Sources */, - E95F85712007D98D0070534A /* CurrencyFormatterTests.swift in Sources */, - 55D1D855287B890300F94A4E /* AddressGeneratorTests.swift in Sources */, - 551F66E82895B3DA00DE5D69 /* AdamantHealthCheckServiceTests.swift in Sources */, - E96D64C12295C06400CA5587 /* JSAdamantCoreTests.swift in Sources */, - E96D64C22295C06400CA5587 /* NativeCoreTests.swift in Sources */, - E950652120404BF0008352E5 /* AdamantUriBuilding.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -3868,58 +853,6 @@ }; /* End PBXTargetDependency section */ -/* Begin PBXVariantGroup section */ - 93E123312A6DF8EF004DF33B /* InfoPlist.strings */ = { - isa = PBXVariantGroup; - children = ( - 93E123302A6DF8EF004DF33B /* en */, - 93E123322A6DF8F1004DF33B /* de */, - 93E123332A6DF8F2004DF33B /* ru */, - 3AF08D5B2B4E7FFC00EB82B1 /* zh */, - ); - name = InfoPlist.strings; - sourceTree = ""; - }; - 93E1233A2A6DFD15004DF33B /* Localizable.strings */ = { - isa = PBXVariantGroup; - children = ( - 93E123392A6DFD15004DF33B /* de */, - 93E1233B2A6DFD18004DF33B /* ru */, - 93E1233C2A6DFD19004DF33B /* en */, - 3AF08D5D2B4E7FFC00EB82B1 /* zh */, - ); - name = Localizable.strings; - sourceTree = ""; - }; - 93E123412A6DFE24004DF33B /* Localizable.stringsdict */ = { - isa = PBXVariantGroup; - children = ( - 93E123402A6DFE24004DF33B /* en */, - 93E123422A6DFE27004DF33B /* de */, - 93E123432A6DFE2E004DF33B /* ru */, - 3AF08D5C2B4E7FFC00EB82B1 /* zh */, - ); - name = Localizable.stringsdict; - sourceTree = ""; - }; - E9079A81229DEF9C0022CA0D /* MainInterface.storyboard */ = { - isa = PBXVariantGroup; - children = ( - E9079A82229DEF9C0022CA0D /* Base */, - ); - name = MainInterface.storyboard; - sourceTree = ""; - }; - E957E133229B10F80019732A /* MainInterface.storyboard */ = { - isa = PBXVariantGroup; - children = ( - E957E134229B10F80019732A /* Base */, - ); - name = MainInterface.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - /* Begin XCBuildConfiguration section */ E9079A88229DEF9C0022CA0D /* Debug */ = { isa = XCBuildConfiguration; @@ -3937,7 +870,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.10.0; + MARKETING_VERSION = 3.11.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger-dev.MessageNotificationContentExtension"; @@ -3967,7 +900,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.10.0; + MARKETING_VERSION = 3.11.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger.MessageNotificationContentExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -4123,7 +1056,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.10.0; + MARKETING_VERSION = 3.11.0; PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger-dev"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -4154,7 +1087,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.10.0; + MARKETING_VERSION = 3.11.0; PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -4182,7 +1115,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.10.0; + MARKETING_VERSION = 3.11.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger-dev.TransferNotificationContentExtension"; @@ -4212,7 +1145,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.10.0; + MARKETING_VERSION = 3.11.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger.TransferNotificationContentExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -4243,7 +1176,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.10.0; + MARKETING_VERSION = 3.11.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger-dev.NotificationServiceExtension"; @@ -4273,7 +1206,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.10.0; + MARKETING_VERSION = 3.11.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "im.adamant.adamant-messenger.NotificationServiceExtension"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -4514,14 +1447,6 @@ minimumVersion = 0.9.1; }; }; - A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/krzyzanowskim/CryptoSwift.git"; - requirement = { - kind = upToNextMinorVersion; - minimumVersion = 1.5.0; - }; - }; A5F0A049262C9CA90009672A /* XCRemoteSwiftPackageReference "Swinject" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Swinject/Swinject.git"; @@ -4538,6 +1463,22 @@ minimumVersion = 1.7.0; }; }; + AA8FFFC82D4E6435001D8576 /* XCRemoteSwiftPackageReference "CryptoSwift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Adamant-im/CryptoSwift.git"; + requirement = { + kind = revision; + revision = ae1cc15340ddf0f607981f9118fe80d7fd7cd2b5; + }; + }; + B1C2BA702D90BF34001FE840 /* XCRemoteSwiftPackageReference "SwiftyMocky" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/qonto/SwiftyMocky"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.2.2; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -4588,6 +1529,10 @@ package = 557AC304287B10D8004699D7 /* XCRemoteSwiftPackageReference "SnapKit" */; productName = SnapKit; }; + 6FB686152D3AAE8800CAB6DD /* AdamantWalletsKit */ = { + isa = XCSwiftPackageProductDependency; + productName = AdamantWalletsKit; + }; 9342F6C12A6A35E300A9B39F /* CommonKit */ = { isa = XCSwiftPackageProductDependency; productName = CommonKit; @@ -4688,26 +1633,6 @@ package = A5DBBABB262C7221004AC028 /* XCRemoteSwiftPackageReference "swift-sodium" */; productName = Clibsodium; }; - A5DBBADB262C729B004AC028 /* CryptoSwift */ = { - isa = XCSwiftPackageProductDependency; - package = A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift" */; - productName = CryptoSwift; - }; - A5DBBAE2262C72B0004AC028 /* CryptoSwift */ = { - isa = XCSwiftPackageProductDependency; - package = A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift" */; - productName = CryptoSwift; - }; - A5DBBAE4262C72B7004AC028 /* CryptoSwift */ = { - isa = XCSwiftPackageProductDependency; - package = A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift" */; - productName = CryptoSwift; - }; - A5DBBAE6262C72BD004AC028 /* CryptoSwift */ = { - isa = XCSwiftPackageProductDependency; - package = A5DBBADA262C729B004AC028 /* XCRemoteSwiftPackageReference "CryptoSwift" */; - productName = CryptoSwift; - }; A5DBBAED262C72EF004AC028 /* BitcoinKit */ = { isa = XCSwiftPackageProductDependency; productName = BitcoinKit; @@ -4741,20 +1666,22 @@ package = A5F92992262C855A00C3E60A /* XCRemoteSwiftPackageReference "MarkdownKit" */; productName = MarkdownKit; }; -/* End XCSwiftPackageProductDependency section */ - -/* Begin XCVersionGroup section */ - E90847192196FE590095825D /* Adamant.xcdatamodeld */ = { - isa = XCVersionGroup; - children = ( - E908471A2196FE590095825D /* Adamant.xcdatamodel */, - ); - currentVersion = E908471A2196FE590095825D /* Adamant.xcdatamodel */; - path = Adamant.xcdatamodeld; - sourceTree = ""; - versionGroupType = wrapper.xcdatamodel; + AA8FFFC92D4E6435001D8576 /* CryptoSwift */ = { + isa = XCSwiftPackageProductDependency; + package = AA8FFFC82D4E6435001D8576 /* XCRemoteSwiftPackageReference "CryptoSwift" */; + productName = CryptoSwift; + }; + B18F4D702D986CD300F6917F /* SwiftyMockyXCTest */ = { + isa = XCSwiftPackageProductDependency; + package = B1C2BA702D90BF34001FE840 /* XCRemoteSwiftPackageReference "SwiftyMocky" */; + productName = SwiftyMockyXCTest; + }; + B1C2BA712D90BF34001FE840 /* SwiftyMocky */ = { + isa = XCSwiftPackageProductDependency; + package = B1C2BA702D90BF34001FE840 /* XCRemoteSwiftPackageReference "SwiftyMocky" */; + productName = SwiftyMocky; }; -/* End XCVersionGroup section */ +/* End XCSwiftPackageProductDependency section */ }; rootObject = E913C8E61FFFA51D001A83F7 /* Project object */; } diff --git a/Adamant.xcodeproj/xcshareddata/xcschemes/Adamant.Release.xcscheme b/Adamant.xcodeproj/xcshareddata/xcschemes/Adamant.Release.xcscheme index e269d92cc..beb771eb6 100644 --- a/Adamant.xcodeproj/xcshareddata/xcschemes/Adamant.Release.xcscheme +++ b/Adamant.xcodeproj/xcshareddata/xcschemes/Adamant.Release.xcscheme @@ -51,8 +51,8 @@ + + diff --git a/Adamant.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Adamant.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5828488fd..b147e3738 100644 --- a/Adamant.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Adamant.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -10,6 +10,15 @@ "version": "0.20220203.2" } }, + { + "package": "AEXML", + "repositoryURL": "https://github.com/tadija/AEXML.git", + "state": { + "branch": null, + "revision": "db806756c989760b35108146381535aec231092b", + "version": "4.7.0" + } + }, { "package": "Alamofire", "repositoryURL": "https://github.com/Alamofire/Alamofire.git", @@ -37,13 +46,31 @@ "version": "0.9.1" } }, + { + "package": "Chalk", + "repositoryURL": "https://github.com/luoxiu/Chalk", + "state": { + "branch": null, + "revision": "8a9d3373bd754fb62f7881d9f639d376f5e4d5a5", + "version": "0.2.1" + } + }, + { + "package": "Commander", + "repositoryURL": "https://github.com/kylef/Commander", + "state": { + "branch": null, + "revision": "4a1f2fb82fb6cef613c4a25d2e38f702e4d812c2", + "version": "0.9.2" + } + }, { "package": "CryptoSwift", - "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift.git", + "repositoryURL": "https://github.com/Adamant-im/CryptoSwift.git", "state": { "branch": null, - "revision": "039f56c5d7960f277087a0be51f5eb04ed0ec073", - "version": "1.5.1" + "revision": "ae1cc15340ddf0f607981f9118fe80d7fd7cd2b5", + "version": null } }, { @@ -208,6 +235,15 @@ "version": null } }, + { + "package": "PathKit", + "repositoryURL": "https://github.com/kylef/PathKit", + "state": { + "branch": null, + "revision": "3bfd2737b700b9a36565a8c94f4ad2b050a5e574", + "version": "1.0.1" + } + }, { "package": "ProcedureKit", "repositoryURL": "https://github.com/ProcedureKit/ProcedureKit.git", @@ -235,6 +271,15 @@ "version": "10.1.1" } }, + { + "package": "Rainbow", + "repositoryURL": "https://github.com/luoxiu/Rainbow", + "state": { + "branch": null, + "revision": "9d8fcb4c64e816a135c31162a0c319e9f1f09c35", + "version": "0.1.1" + } + }, { "package": "Reachability", "repositoryURL": "https://github.com/ashleymills/Reachability.swift", @@ -253,6 +298,15 @@ "version": "5.1.0" } }, + { + "package": "ShellOut", + "repositoryURL": "https://github.com/JohnSundell/ShellOut", + "state": { + "branch": null, + "revision": "e1577acf2b6e90086d01a6d5e2b8efdaae033568", + "version": "2.3.0" + } + }, { "package": "SipHash", "repositoryURL": "https://github.com/attaswift/SipHash", @@ -280,6 +334,15 @@ "version": null } }, + { + "package": "Spectre", + "repositoryURL": "https://github.com/kylef/Spectre.git", + "state": { + "branch": null, + "revision": "26cc5e9ae0947092c7139ef7ba612e34646086c7", + "version": "0.10.1" + } + }, { "package": "Starscream", "repositoryURL": "https://github.com/daltoniam/Starscream", @@ -334,6 +397,15 @@ "version": "2.3.0" } }, + { + "package": "swiftymocky", + "repositoryURL": "https://github.com/qonto/SwiftyMocky", + "state": { + "branch": null, + "revision": "a53a970ffbb18527f307069bc1109aad16a86522", + "version": "4.2.2" + } + }, { "package": "Swinject", "repositoryURL": "https://github.com/Swinject/Swinject.git", @@ -351,6 +423,24 @@ "revision": "8a026108ae5ff730ac83e9b574c8cf1c14413c94", "version": "3.2.2" } + }, + { + "package": "XcodeProj", + "repositoryURL": "https://github.com/tuist/xcodeproj", + "state": { + "branch": null, + "revision": "b1caa062d4aaab3e3d2bed5fe0ac5f8ce9bf84f4", + "version": "8.27.7" + } + }, + { + "package": "Yams", + "repositoryURL": "https://github.com/jpsim/Yams", + "state": { + "branch": null, + "revision": "b4b8042411dc7bbb696300a34a4bf3ba1b7ad19b", + "version": "5.3.1" + } } ] }, diff --git a/Adamant/Adamant.xcdatamodeld/Adamant.xcdatamodel/contents b/Adamant/Adamant.xcdatamodeld/Adamant.xcdatamodel/contents index 7ef578c34..ebf5ff422 100644 --- a/Adamant/Adamant.xcdatamodeld/Adamant.xcdatamodel/contents +++ b/Adamant/Adamant.xcdatamodeld/Adamant.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -34,6 +34,7 @@ + @@ -49,6 +50,7 @@ + @@ -76,6 +78,7 @@ + diff --git a/Adamant/App/AppDelegate.swift b/Adamant/App/AppDelegate.swift index b795ccac3..7f5fff758 100644 --- a/Adamant/App/AppDelegate.swift +++ b/Adamant/App/AppDelegate.swift @@ -6,12 +6,13 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import Swinject -import CryptoSwift -import CoreData +import AdamantWalletsKit import CommonKit +import CoreData +import CryptoSwift import FilesStorageKit +import Swinject +import UIKit // MARK: - Constants extension String.adamant { @@ -19,18 +20,21 @@ extension String.adamant { static var account: String { String.localized("Tabs.Account", comment: "Main tab bar: Account page") } - + static var chats: String { String.localized("Tabs.Chats", comment: "Main tab bar: Chats page") } - + static var settings: String { String.localized("Tabs.Settings", comment: "Main tab bar: Settings page") } } - + struct application { - static let deviceTokenSendFailed = String.localized("Application.deviceTokenErrorFormat", comment: "Application: Failed to send deviceToken to ANS error format. %@ for error description") + static let deviceTokenSendFailed = String.localized( + "Application.deviceTokenErrorFormat", + comment: "Application: Failed to send deviceToken to ANS error format. %@ for error description" + ) } } @@ -38,7 +42,7 @@ extension StoreKey { struct application { static let welcomeScreensIsShown = "app.welcomeScreensIsShown" static let eulaAccepted = "app.eulaAccepted" - + private init() {} } } @@ -50,18 +54,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var repeater: RepeaterService! var container: AppContainer! var screensFactory: ScreensFactory! - + // MARK: Dependencies var accountService: AccountService! var notificationService: NotificationsService! var dialogService: DialogService! var addressBookService: AddressBookService! var pushNotificationsTokenService: PushNotificationsTokenService! - var visibleWalletsService: VisibleWalletsService! - + // MARK: - Lifecycle - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // MARK: 1. Initiating Swinject container = AppContainer() screensFactory = AdamantScreensFactory(assembler: container.assembler) @@ -70,18 +73,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate { dialogService = container.resolve(DialogService.self) addressBookService = container.resolve(AddressBookService.self) pushNotificationsTokenService = container.resolve(PushNotificationsTokenService.self) - visibleWalletsService = container.resolve(VisibleWalletsService.self) - + // MARK: 1.1 Configure Firebase if needed - + container .resolve(CrashlyticsService.self)? .configureIfNeeded() - + // MARK: 2. Init UI let window = UIWindow(frame: UIScreen.main.bounds) self.window = window - + let rootTabBarController = UITabBarController() if #available(iOS 18.0, *) { rootTabBarController.mode = .tabSidebar @@ -90,28 +92,29 @@ class AppDelegate: UIResponder, UIApplicationDelegate { window.rootViewController = rootTabBarController rootTabBarController.view.backgroundColor = .adamant.backgroundColor window.tintColor = UIColor.adamant.primary - + // MARK: 3. Prepare pages let chatList = UINavigationController( rootViewController: screensFactory.makeChatList() ) - + let account = UINavigationController( rootViewController: screensFactory.makeAccount() ) - - let tabScreens: TabScreens = UIScreen.main.traitCollection.userInterfaceIdiom == .pad + + let tabScreens: TabScreens = + UIScreen.main.traitCollection.userInterfaceIdiom == .pad ? .splitControllers(makeSplitController(), makeSplitController()) : .navigationControllers(chatList, account) - + tabScreens.viewControllers.0.tabBarItem.title = .adamant.tabItems.chats tabScreens.viewControllers.0.tabBarItem.image = .asset(named: "chats_tab") tabScreens.viewControllers.0.tabBarItem.badgeColor = .adamant.primary - + tabScreens.viewControllers.1.tabBarItem.title = .adamant.tabItems.account tabScreens.viewControllers.1.tabBarItem.image = .asset(named: "account-tab") tabScreens.viewControllers.1.tabBarItem.badgeColor = .adamant.primary - + let resetScreensAction: @MainActor () -> Void switch tabScreens { case let .splitControllers(leftController, rightController): @@ -127,9 +130,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate { rightController.popToRootViewController(animated: false) } } - + resetScreensAction() - + let tabBarAppearance: UITabBarAppearance = UITabBarAppearance() tabBarAppearance.configureWithDefaultBackground() UITabBar.appearance().standardAppearance = tabBarAppearance @@ -139,54 +142,46 @@ class AppDelegate: UIResponder, UIApplicationDelegate { UINavigationBar.appearance().standardAppearance = navigationBarAppearance UITabBar.appearance().scrollEdgeAppearance = tabBarAppearance UINavigationBar.appearance().scrollEdgeAppearance = navigationBarAppearance - + rootTabBarController.setViewControllers( tabScreens.arrayOfViewControllers, animated: false ) - + window.makeKeyAndVisible() - + // MARK: 4. Setup dialog service dialogService.setup(window: window) - + // MARK: 5. Show login let login = screensFactory.makeLogin() let welcomeIsShown = UserDefaults.standard.bool(forKey: StoreKey.application.welcomeScreensIsShown) - + login.requestBiometryOnFirstTimeActive = welcomeIsShown login.modalPresentationStyle = .overFullScreen window.rootViewController?.present(login, animated: false, completion: nil) - + if !welcomeIsShown { - if isMacOS { - var size: CGSize = .init(width: 900, height: 900) - window.frame = .init(origin: window.frame.origin, size: size) - window.windowScene?.sizeRestrictions?.minimumSize = size - window.windowScene?.sizeRestrictions?.maximumSize = size - window.windowScene?.sizeRestrictions?.allowsFullScreen = false - } - let welcome = screensFactory.makeOnboard() welcome.modalPresentationStyle = .overFullScreen login.present(welcome, animated: true, completion: nil) UserDefaults.standard.set(true, forKey: StoreKey.application.welcomeScreensIsShown) } - + // MARK: 6 Reachability & Autoupdate repeater = RepeaterService() - + // Configure reachability if let reachability = container.resolve(ReachabilityMonitor.self) { reachability.start() - + if reachability.connection { dialogService.dissmisNoConnectionNotification() } else { dialogService.showNoConnectionNotification() repeater.pauseAll() } - + NotificationCenter.default.addObserver( forName: Notification.Name.AdamantReachabilityMonitor.reachabilityChanged, object: reachability, @@ -194,10 +189,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { ) { [weak self] notification in MainActor.assumeIsolatedSafe { guard let connection = notification.userInfo?[AdamantUserInfoKey.ReachabilityMonitor.connection] as? Bool, - let repeater = self?.repeater else { - return + let repeater = self?.repeater + else { + return } - + if connection { self?.dialogService.dissmisNoConnectionNotification() repeater.resumeAll() @@ -208,62 +204,77 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } } - + // Setup transactions statuses observing if let service = container.resolve(TransactionsStatusServiceComposeProtocol.self) { Task { await service.startObserving() } } - + // Setup transactions reply observing if let service = container.resolve(RichTransactionReplyService.self) { Task { await service.startObserving() } } - + // Setup transactions react observing if let service = container.resolve(RichTransactionReactService.self) { Task { await service.startObserving() } } - + // Register repeater services if let chatsProvider = container.resolve(ChatsProvider.self) { - repeater.registerForegroundCall(label: "chatsProvider", interval: 10, queue: .global(qos: .utility), callback: { - Task { - await chatsProvider.update(notifyState: false) + repeater.registerForegroundCall( + label: "chatsProvider", + interval: 10, + queue: .global(qos: .utility), + callback: { + Task { + await chatsProvider.update(notifyState: false) + } } - }) - + ) + } else { dialogService.showError(withMessage: "Failed to register ChatsProvider autoupdate. Please, report a bug", supportEmail: true, error: nil) } - + if let transfersProvider = container.resolve(TransfersProvider.self) { - repeater.registerForegroundCall(label: "transfersProvider", interval: 15, queue: .global(qos: .utility), callback: { - Task { - await transfersProvider.update() + repeater.registerForegroundCall( + label: "transfersProvider", + interval: 15, + queue: .global(qos: .utility), + callback: { + Task { + await transfersProvider.update() + } } - }) + ) } else { dialogService.showError(withMessage: "Failed to register TransfersProvider autoupdate. Please, report a bug", supportEmail: true, error: nil) } - + if let accountService = container.resolve(AccountService.self) { repeater.registerForegroundCall(label: "accountService", interval: 15, queue: .global(qos: .utility), callback: accountService.update) } else { dialogService.showError(withMessage: "Failed to register AccountService autoupdate. Please, report a bug", supportEmail: true, error: nil) } - + if let addressBookService = container.resolve(AddressBookService.self) { - repeater.registerForegroundCall(label: "addressBookService", interval: 15, queue: .global(qos: .utility), callback: { - Task { - await addressBookService.update() + repeater.registerForegroundCall( + label: "addressBookService", + interval: 15, + queue: .global(qos: .utility), + callback: { + Task { + await addressBookService.update() + } } - }) + ) } else { dialogService.showError(withMessage: "Failed to register AddressBookService autoupdate. Please, report a bug", supportEmail: true, error: nil) } - + if let currencyInfoService = container.resolve(InfoServiceProtocol.self) { - currencyInfoService.update() // Initial update + currencyInfoService.update() // Initial update repeater.registerForegroundCall( label: "currencyInfoService", interval: 60, @@ -276,7 +287,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } else { dialogService.showError(withMessage: "Failed to register InfoServiceProtocol autoupdate. Please, report a bug", supportEmail: true, error: nil) } - + // MARK: 7. Logout reset NotificationCenter.default.addObserver( forName: Notification.Name.AdamantAccountService.userLoggedOut, @@ -287,7 +298,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { resetScreensAction() } } - + // MARK: 8. Welcome messages Task { for await notification in NotificationCenter.default.notifications( @@ -296,40 +307,42 @@ class AppDelegate: UIResponder, UIApplicationDelegate { await handleWelcomeMessages(notification: notification) } } - + // MARK: 9. Notifications pushNotificationsTokenService.sendTokenDeletionTransactions() UNUserNotificationCenter.current().delegate = self - + + setupInitialUserDefaults() + return true } - + // MARK: Timers - + func applicationWillResignActive(_ application: UIApplication) { repeater.pauseAll() } - + func applicationDidEnterBackground(_ application: UIApplication) { repeater.pauseAll() Task { await addressBookService.saveIfNeeded() } } - + // MARK: Notifications - + func applicationDidBecomeActive(_ application: UIApplication) { if accountService.account != nil { notificationService.removeAllDeliveredNotifications() } - + guard container.resolve(ReachabilityMonitor.self)?.connection == true else { return } - + repeater.resumeAll() } - + func application( _: UIApplication, shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplication.ExtensionPointIdentifier @@ -351,13 +364,17 @@ extension AppDelegate { ) { pushNotificationsTokenService.setToken(deviceToken) } - + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { if let service = container.resolve(DialogService.self) { - service.showError(withMessage: String.localizedStringWithFormat(NotificationStrings.registrationRemotesFormat, error.localizedDescription), supportEmail: true, error: error) + service.showError( + withMessage: String.localizedStringWithFormat(NotificationStrings.registrationRemotesFormat, error.localizedDescription), + supportEmail: true, + error: error + ) } } - + func openDialog( chatListNav: UINavigationController, tabbar: UITabBarController, @@ -365,46 +382,44 @@ extension AppDelegate { transactionID: String, senderAddress: String ) { - if - let chatVCNav = chatListNav.viewControllers.last as? UINavigationController, + if let chatVCNav = chatListNav.viewControllers.last as? UINavigationController, let chatVC = chatVCNav.viewControllers.first as? ChatViewController, chatVC.viewModel.chatroom?.partner?.address == senderAddress { chatVC.messagesCollectionView.scrollToBottom(animated: true) return } - - if - let chatVC = chatListNav.viewControllers.last as? ChatViewController, + + if let chatVC = chatListNav.viewControllers.last as? ChatViewController, chatVC.viewModel.chatroom?.partner?.address == senderAddress { chatVC.messagesCollectionView.scrollToBottom(animated: true) return } - + let chatroom = chatListVC.chatsController?.fetchedObjects?.first( where: { $0.partner?.address == senderAddress } ) - + guard let chatroom = chatroom else { return } - + chatListNav.popToRootViewController(animated: true) chatListNav.dismiss(animated: true, completion: nil) tabbar.selectedIndex = 0 - + let vc = chatListVC.chatViewController(for: chatroom, with: transactionID) - + vc.hidesBottomBarWhenPushed = true - + if let split = chatListVC.splitViewController { let chat = UINavigationController(rootViewController: vc) split.showDetailViewController(chat, sender: chatListVC) } else { chatListNav.pushViewController(vc, animated: true) } - + chatListVC.selectChatroomRow(chatroom: chatroom) } } @@ -419,43 +434,45 @@ extension AppDelegate: UNUserNotificationCenterDelegate { [atomicCompletionHandler = Atomic(completionHandler)] in atomicCompletionHandler.isolated { $0() } } - + Task { @MainActor in var chatListNav: UINavigationController? var chatListVC: ChatListViewController? - + let userInfo = response.notification.request.content.userInfo - + guard let transactionID = userInfo[AdamantNotificationUserInfoKeys.transactionId] as? String, - let transactionRaw = userInfo[AdamantNotificationUserInfoKeys.transaction] as? String, - let data = transactionRaw.data(using: .utf8), - let trs = try? JSONDecoder().decode(Transaction.self, from: data), - let tabbar = window?.rootViewController as? UITabBarController + let transactionRaw = userInfo[AdamantNotificationUserInfoKeys.transaction] as? String, + let data = transactionRaw.data(using: .utf8), + let trs = try? JSONDecoder().decode(Transaction.self, from: data), + let tabbar = window?.rootViewController as? UITabBarController else { completionHandler() return } - + if let split = tabbar.viewControllers?.first as? UISplitViewController, - let navigation = split.viewControllers.first as? UINavigationController, - let vc = navigation.viewControllers.first as? ChatListViewController { + let navigation = split.viewControllers.first as? UINavigationController, + let vc = navigation.viewControllers.first as? ChatListViewController + { chatListNav = navigation chatListVC = vc } - + if let navigation = tabbar.viewControllers?.first as? UINavigationController, - let vc = navigation.viewControllers.first as? ChatListViewController { + let vc = navigation.viewControllers.first as? ChatListViewController + { chatListNav = navigation chatListVC = vc } - + guard let chatListVC = chatListVC, - let chatListNav = chatListNav + let chatListNav = chatListNav else { completionHandler() return } - + chatListVC.performOnMessagesLoaded { [weak self] in DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self?.dialogService.dismissProgress() @@ -468,7 +485,7 @@ extension AppDelegate: UNUserNotificationCenterDelegate { ) } } - + completionHandler() } } @@ -478,45 +495,45 @@ extension AppDelegate: UNUserNotificationCenterDelegate { extension AppDelegate { func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { let container = AppContainer() - + guard let notificationsService = container.resolve(NotificationsService.self) else { - UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalNever) - completionHandler(.failed) - return + UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalNever) + completionHandler(.failed) + return } - + notificationsService.startBackgroundBatchNotifications() - + Task { let results = await fetchBackgroundData(notificationsService: notificationsService) - + notificationsService.stopBackgroundBatchNotifications() - + for result in results { switch result { case .newData: completionHandler(.newData) return - + case .noData: break - + case .failed: completionHandler(.failed) return } } - + completionHandler(.noData) } } - + func fetchBackgroundData(notificationsService: NotificationsService) async -> [FetchResult] { let services: [BackgroundFetchService] = [ container.resolve(ChatsProvider.self) as! BackgroundFetchService, container.resolve(TransfersProvider.self) as! BackgroundFetchService ] - + return await withTaskGroup(of: FetchResult.self) { group in for case let service in services { group.addTask { @Sendable in @@ -524,9 +541,9 @@ extension AppDelegate { return result } } - + var results: [FetchResult] = [] - + for await result in group { results.append(result) } @@ -543,20 +560,20 @@ extension AppDelegate { guard let synced = notification.userInfo?[AdamantUserInfoKey.ChatProvider.initiallySynced] as? Bool, synced else { return } - + guard let stack = container.resolve(CoreDataStack.self), let chatProvider = container.resolve(ChatsProvider.self) else { fatalError("Whoa...") } - + let request = NSFetchRequest(entityName: MessageTransaction.entityName) - + let unread: Bool if let count = try? stack.container.viewContext.count(for: request), count > 0 { unread = false } else { unread = true } - + if let adelina = AdamantContacts.adelina.welcomeMessage { _ = try? await chatProvider.fakeReceived( message: adelina.message, @@ -567,7 +584,7 @@ extension AppDelegate { showsChatroom: true ) } - + if let exchenge = AdamantContacts.adamantExchange.welcomeMessage { _ = try? await chatProvider.fakeReceived( message: exchenge.message, @@ -578,7 +595,7 @@ extension AppDelegate { showsChatroom: true ) } - + if let betOnBitcoin = AdamantContacts.betOnBitcoin.welcomeMessage { _ = try? await chatProvider.fakeReceived( message: betOnBitcoin.message, @@ -589,7 +606,7 @@ extension AppDelegate { showsChatroom: false ) } - + if let welcome = AdamantContacts.donate.welcomeMessage { _ = try? await chatProvider.fakeReceived( message: welcome.message, @@ -600,7 +617,7 @@ extension AppDelegate { showsChatroom: true ) } - + // TODO: Figireout why we cant use AdamantContacts.adamantWelcomeWallet.address for senderId (chat is not shown) if let welcome = AdamantContacts.adamantWelcomeWallet.welcomeMessage { _ = try? await chatProvider.fakeReceived( @@ -625,57 +642,59 @@ extension AppDelegate { processDeepLink(url) return true } - + func application( _ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void ) -> Bool { guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, - let url = userActivity.webpageURL + let url = userActivity.webpageURL else { return true } - + processDeepLink(url) return true } - + func processDeepLink(_ url: URL) { var adamantAdr: AdamantAddress? = url.absoluteString.getAdamantAddress() - + if adamantAdr == nil { adamantAdr = url.absoluteString.getLegacyAdamantAddress() } - + guard let adamantAdr = adamantAdr else { return } - + var chatList: UINavigationController? var chatDetail: ChatListViewController? - + guard let tabbar = window?.rootViewController as? UITabBarController else { return } - + if let split = tabbar.viewControllers?.first as? UISplitViewController, - let navigation = split.viewControllers.first as? UINavigationController, - let vc = navigation.viewControllers.first as? ChatListViewController { + let navigation = split.viewControllers.first as? UINavigationController, + let vc = navigation.viewControllers.first as? ChatListViewController + { chatList = navigation chatDetail = vc } - + if let navigation = tabbar.viewControllers?.first as? UINavigationController, - let vc = navigation.viewControllers.first as? ChatListViewController { + let vc = navigation.viewControllers.first as? ChatListViewController + { chatList = navigation chatDetail = vc } - + guard let chatList = chatList, - let chatDetail = chatDetail + let chatDetail = chatDetail else { return } - + chatDetail.performOnMessagesLoaded { [weak self] in DispatchQueue.main.asyncAfter(deadline: .now() + 1) { self?.startNewDialog( @@ -687,7 +706,7 @@ extension AppDelegate { } } } - + func startNewDialog( with adamantAdr: AdamantAddress, chatList: UINavigationController, @@ -697,10 +716,10 @@ extension AppDelegate { chatList.popToRootViewController(animated: false) chatList.dismiss(animated: false, completion: nil) tabbar.selectedIndex = 0 - + let newChat = screensFactory.makeNewChat() newChat.delegate = chatDetail.self - + if let split = chatDetail.splitViewController { split.showDetailViewController(newChat, sender: chatDetail.self) } else if let nav = chatDetail.navigationController { @@ -709,17 +728,21 @@ extension AppDelegate { newChat.modalPresentationStyle = .overFullScreen chatDetail.present(newChat, animated: true) } - + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { newChat.startNewChat(with: adamantAdr.address, name: adamantAdr.name, message: adamantAdr.message) } } + + func setupInitialUserDefaults() { + UserDefaultsManager.setInitialUserDefaults() + } } private enum TabScreens { case splitControllers(UISplitViewController, UISplitViewController) case navigationControllers(UINavigationController, UINavigationController) - + var viewControllers: (UIViewController, UIViewController) { switch self { case let .splitControllers(leftController, rightController): @@ -728,7 +751,7 @@ private enum TabScreens { return (leftController, rightController) } } - + var arrayOfViewControllers: [UIViewController] { switch self { case let .splitControllers(leftController, rightController): diff --git a/Adamant/App/DI/AppAssembly.swift b/Adamant/App/DI/AppAssembly.swift index 8f0287660..ad913f1af 100644 --- a/Adamant/App/DI/AppAssembly.swift +++ b/Adamant/App/DI/AppAssembly.swift @@ -6,42 +6,45 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Swinject +import AdamantWalletsKit import BitcoinKit import CommonKit -import FilesStorageKit import FilesPickerKit +import FilesStorageKit +import Swinject struct AppAssembly: MainThreadAssembly { func assembleOnMainThread(container: Container) { // MARK: - Standalone services // MARK: AdamantCore container.register(AdamantCore.self) { _ in NativeAdamantCore() }.inObjectScope(.container) - + // MARK: FilesStorageProtocol container.register(FilesStorageProtocol.self) { _ in FilesStorageKit() }.inObjectScope(.container) - + container.register(FilesPickerProtocol.self) { r in FilesPickerKit(storageKit: r.resolve(FilesStorageProtocol.self)!) } - + // MARK: CellFactory - container.register(CellFactory.self) { _ in AdamantCellFactory() }.inObjectScope(.container) - + container.register(CellFactory.self) { _ in + AdamantCellFactory() + }.inObjectScope(.container) + // MARK: Secured Store - container.register(SecuredStore.self) { _ in + container.register(SecureStore.self) { _ in KeychainStore(secureStorage: AdamantSecureStorage()) }.inObjectScope(.container) - + // MARK: LocalAuthentication container.register(LocalAuthentication.self) { _ in AdamantAuthentication() }.inObjectScope(.container) - + // MARK: Reachability container.register(ReachabilityMonitor.self) { _ in AdamantReachability() }.inObjectScope(.container) - + // MARK: AdamantAvatarService container.register(AvatarService.self) { _ in AdamantAvatarService() }.inObjectScope(.container) - + // MARK: - Services with dependencies // MARK: DialogService container.register(DialogService.self) { r in @@ -50,85 +53,123 @@ struct AppAssembly: MainThreadAssembly { notificationsService: r.resolve(NotificationsService.self)! ) }.inObjectScope(.container) - + // MARK: Notifications container.register(NotificationsService.self) { r in AdamantNotificationsService( - securedStore: r.resolve(SecuredStore.self)!, + SecureStore: r.resolve(SecureStore.self)!, vibroService: r.resolve(VibroService.self)! ) - }.initCompleted { (r, c) in // Weak reference + }.initCompleted { (r, c) in // Weak reference guard let service = c as? AdamantNotificationsService else { return } service.accountService = r.resolve(AccountService.self) service.chatsProvider = r.resolve(ChatsProvider.self) }.inObjectScope(.container) - + + // MARK: Wallet storage + container.register(AnyTokensStorage.self) { _ in + TokensStorage() + } + .inObjectScope(.container) + + initWalletsStorage(from: container) + // MARK: VisibleWalletsService container.register(VisibleWalletsService.self) { r in AdamantVisibleWalletsService( - securedStore: r.resolve(SecuredStore.self)!, + SecureStore: r.resolve(SecureStore.self)!, accountService: r.resolve(AccountService.self)!, walletsServiceCompose: r.resolve(WalletServiceCompose.self)! ) }.inObjectScope(.container) - + + // MARK: WalletStoreServiceProtocol + container.register(WalletStoreServiceProtocol.self) { r in + AdamantWalletStoreService( + visibleWalletsService: r.resolve(VisibleWalletsService.self)!, + walletServiceCompose: r.resolve(WalletServiceCompose.self)! + ) + }.inObjectScope(.container) + + // MARK: Secret Wallets + container.register(SecretWalletsFactory.self) { r in + SecretWalletsFactory( + visibleWalletsService: r.resolve(VisibleWalletsService.self)!, + accountService: r.resolve(AccountService.self)!, + SecureStore: r.resolve(SecureStore.self)! + ) + }.inObjectScope(.container) + + container.register(SecretWalletsManagerProtocol.self) { r in + AdamantSecretWalletsManager( + walletsStoreService: r.resolve(WalletStoreServiceProtocol.self)!, + secretWalletsFactory: r.resolve(SecretWalletsFactory.self)! + ) + }.inObjectScope(.container) + + container.register(WalletStoreServiceProviderProtocol.self) { r in + AdamantWalletStoreServiceProvider( + secretWalletsManager: r.resolve(SecretWalletsManagerProtocol.self)! + ) + }.inObjectScope(.container) + // MARK: IncreaseFeeService container.register(IncreaseFeeService.self) { r in AdamantIncreaseFeeService( - securedStore: r.resolve(SecuredStore.self)! + SecureStore: r.resolve(SecureStore.self)! ) }.inObjectScope(.container) - + // MARK: EmojiService container.register(EmojiService.self) { r in AdamantEmojiService( - securedStore: r.resolve(SecuredStore.self)! + SecureStore: r.resolve(SecureStore.self)! ) }.inObjectScope(.container) - + // MARK: VibroService container.register(VibroService.self) { _ in AdamantVibroService() }.inObjectScope(.container) - + // MARK: CrashlysticsService container.register(CrashlyticsService.self) { r in AdamantCrashlyticsService( - securedStore: r.resolve(SecuredStore.self)! + SecureStore: r.resolve(SecureStore.self)! ) }.inObjectScope(.container) - + // MARK: PushNotificationsTokenService container.register(PushNotificationsTokenService.self) { r in AdamantPushNotificationsTokenService( - securedStore: r.resolve(SecuredStore.self)!, + SecureStore: r.resolve(SecureStore.self)!, apiService: r.resolve(AdamantApiServiceProtocol.self)!, adamantCore: r.resolve(AdamantCore.self)!, accountService: r.resolve(AccountService.self)! ) }.inObjectScope(.container) - + // MARK: NodesStorage container.register(NodesStorageProtocol.self) { r in NodesStorage( - securedStore: r.resolve(SecuredStore.self)!, + SecureStore: r.resolve(SecureStore.self)!, nodesMergingService: r.resolve(NodesMergingServiceProtocol.self)!, defaultNodes: { [provider = r.resolve(DefaultNodesProvider.self)!] groups in provider.get(groups) } ) }.inObjectScope(.container) - + // MARK: NodesAdditionalParamsStorage container.register(NodesAdditionalParamsStorageProtocol.self) { r in - NodesAdditionalParamsStorage(securedStore: r.resolve(SecuredStore.self)!) + NodesAdditionalParamsStorage(SecureStore: r.resolve(SecureStore.self)!) }.inObjectScope(.container) - + // MARK: ApiCore container.register(APICoreProtocol.self) { _ in APICore() }.inObjectScope(.container) - + // MARK: ApiService container.register(AdamantApiServiceProtocol.self) { r in AdamantApiService( @@ -143,101 +184,129 @@ struct AppAssembly: MainThreadAssembly { adamantCore: r.resolve(AdamantCore.self)! ) }.inObjectScope(.container) - + // MARK: IPFSApiService container.register(IPFSApiService.self) { r in - IPFSApiService(healthCheckWrapper: .init( - service: .init(apiCore: r.resolve(APICoreProtocol.self)!), - nodesStorage: r.resolve(NodesStorageProtocol.self)!, - nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, - isActive: true, - params: NodeGroup.ipfs.blockchainHealthCheckParams, - connection: r.resolve(ReachabilityMonitor.self)!.connectionPublisher - )) + IPFSApiService( + healthCheckWrapper: .init( + service: .init(apiCore: r.resolve(APICoreProtocol.self)!), + nodesStorage: r.resolve(NodesStorageProtocol.self)!, + nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, + isActive: true, + params: NodeGroup.ipfs.blockchainHealthCheckParams, + connection: r.resolve(ReachabilityMonitor.self)!.connectionPublisher + ) + ) }.inObjectScope(.container) - + // MARK: FilesNetworkManagerProtocol container.register(FilesNetworkManagerProtocol.self) { r in FilesNetworkManager(ipfsService: r.resolve(IPFSApiService.self)!) }.inObjectScope(.container) - + // MARK: BtcApiService container.register(BtcApiService.self) { r in - BtcApiService(api: .init( - service: .init(apiCore: r.resolve(APICoreProtocol.self)!), - nodesStorage: r.resolve(NodesStorageProtocol.self)!, - nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, - isActive: true, - params: NodeGroup.btc.blockchainHealthCheckParams, - connection: r.resolve(ReachabilityMonitor.self)!.connectionPublisher - )) + BtcApiService( + api: .init( + service: .init(apiCore: r.resolve(APICoreProtocol.self)!), + nodesStorage: r.resolve(NodesStorageProtocol.self)!, + nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, + isActive: true, + params: NodeGroup.btc.blockchainHealthCheckParams, + connection: r.resolve(ReachabilityMonitor.self)!.connectionPublisher + ) + ) }.inObjectScope(.container) - + + // MARK: BitcointTransactionFactoryProtocol + container.register(BitcoinKitTransactionFactoryProtocol.self) { _ in + BitcoinKitTransactionFactory() + }.inObjectScope(.transient) + // MARK: DogeApiService container.register(DogeApiService.self) { r in - DogeApiService(api: .init( - service: .init(apiCore: r.resolve(APICoreProtocol.self)!), - nodesStorage: r.resolve(NodesStorageProtocol.self)!, - nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, - isActive: true, - params: NodeGroup.doge.blockchainHealthCheckParams, - connection: r.resolve(ReachabilityMonitor.self)!.connectionPublisher - )) + DogeApiService( + api: .init( + service: .init(apiCore: r.resolve(APICoreProtocol.self)!), + nodesStorage: r.resolve(NodesStorageProtocol.self)!, + nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, + isActive: true, + params: NodeGroup.doge.blockchainHealthCheckParams, + connection: r.resolve(ReachabilityMonitor.self)!.connectionPublisher + ) + ) }.inObjectScope(.container) - + // MARK: DashApiService container.register(DashApiService.self) { r in - DashApiService(api: .init( - service: .init(apiCore: r.resolve(APICoreProtocol.self)!), - nodesStorage: r.resolve(NodesStorageProtocol.self)!, - nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, - isActive: true, - params: NodeGroup.dash.blockchainHealthCheckParams, - connection: r.resolve(ReachabilityMonitor.self)!.connectionPublisher - )) + DashApiService( + api: .init( + service: .init(apiCore: r.resolve(APICoreProtocol.self)!), + nodesStorage: r.resolve(NodesStorageProtocol.self)!, + nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, + isActive: true, + params: NodeGroup.dash.blockchainHealthCheckParams, + connection: r.resolve(ReachabilityMonitor.self)!.connectionPublisher + ) + ) + }.inObjectScope(.container) + + // MARK: DashLastTransactionStorage + container.register(DashLastTransactionStorageProtocol.self) { r in + DashLastTransactionStorage(SecureStore: r.resolve(SecureStore.self)!) }.inObjectScope(.container) - + // MARK: LskNodeApiService container.register(KlyNodeApiService.self) { r in - KlyNodeApiService(api: .init( - service: .init(), - nodesStorage: r.resolve(NodesStorageProtocol.self)!, - nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, - isActive: true, - params: NodeGroup.klyNode.blockchainHealthCheckParams, - connection: r.resolve(ReachabilityMonitor.self)!.connectionPublisher - )) + KlyNodeApiService( + api: .init( + service: .init(), + nodesStorage: r.resolve(NodesStorageProtocol.self)!, + nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, + isActive: true, + params: NodeGroup.klyNode.blockchainHealthCheckParams, + connection: r.resolve(ReachabilityMonitor.self)!.connectionPublisher + ) + ) }.inObjectScope(.container) - + // MARK: KlyServiceApiService container.register(KlyServiceApiService.self) { r in - KlyServiceApiService(api: .init( - service: .init(), - nodesStorage: r.resolve(NodesStorageProtocol.self)!, - nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, - isActive: true, - params: NodeGroup.klyService.blockchainHealthCheckParams, - connection: r.resolve(ReachabilityMonitor.self)!.connectionPublisher - )) + KlyServiceApiService( + api: .init( + service: .init(), + nodesStorage: r.resolve(NodesStorageProtocol.self)!, + nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, + isActive: true, + params: NodeGroup.klyService.blockchainHealthCheckParams, + connection: r.resolve(ReachabilityMonitor.self)!.connectionPublisher + ) + ) + }.inObjectScope(.container) + + container.register(KlyTransactionFactoryProtocol.self) { r in + KlyTransactionFactory() }.inObjectScope(.container) - + // MARK: EthApiService container.register(EthApiService.self) { r in r.resolve(ERC20ApiService.self)! }.inObjectScope(.transient) - + // MARK: ERC20ApiService container.register(ERC20ApiService.self) { r in - ERC20ApiService(api: .init( - service: .init(apiCore: r.resolve(APICoreProtocol.self)!), - nodesStorage: r.resolve(NodesStorageProtocol.self)!, - nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, - isActive: true, - params: NodeGroup.eth.blockchainHealthCheckParams, - connection: r.resolve(ReachabilityMonitor.self)!.connectionPublisher - )) + ERC20ApiService( + api: .init( + service: .init(apiCore: r.resolve(APICoreProtocol.self)!), + nodesStorage: r.resolve(NodesStorageProtocol.self)!, + nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, + isActive: true, + params: NodeGroup.eth.blockchainHealthCheckParams, + connection: r.resolve(ReachabilityMonitor.self)!.connectionPublisher + ) + ) }.inObjectScope(.container) - + // MARK: SocketService container.register(SocketService.self) { r in AdamantSocketService( @@ -245,25 +314,25 @@ struct AppAssembly: MainThreadAssembly { nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)! ) }.inObjectScope(.container) - + // MARK: AccountService container.register(AccountService.self) { r in AdamantAccountService( apiService: r.resolve(AdamantApiServiceProtocol.self)!, adamantCore: r.resolve(AdamantCore.self)!, - dialogService: r.resolve(DialogService.self)!, - securedStore: r.resolve(SecuredStore.self)!, + SecureStore: r.resolve(SecureStore.self)!, walletServiceCompose: r.resolve(WalletServiceCompose.self)!, currencyInfoService: r.resolve(InfoServiceProtocol.self)!, + coreDataStack: r.resolve(CoreDataStack.self)!, connection: r.resolve(ReachabilityMonitor.self)!.connectionPublisher ) }.inObjectScope(.container).initCompleted { (r, c) in guard let service = c as? AdamantAccountService else { return } service.notificationsService = r.resolve(NotificationsService.self)! service.pushNotificationsTokenService = r.resolve(PushNotificationsTokenService.self)! - service.visibleWalletService = r.resolve(VisibleWalletsService.self)! + service.walletsStoreService = r.resolve(WalletStoreServiceProtocol.self)! } - + // MARK: AddressBookServeice container.register(AddressBookService.self) { r in AdamantAddressBookService( @@ -273,18 +342,18 @@ struct AppAssembly: MainThreadAssembly { dialogService: r.resolve(DialogService.self)! ) }.inObjectScope(.container) - + // MARK: LanguageStorageProtocol container.register(LanguageStorageProtocol.self) { _ in LanguageStorageService() }.inObjectScope(.container) - + // MARK: - Data Providers // MARK: CoreData Stack container.register(CoreDataStack.self) { _ in try! InMemoryCoreDataStack(modelUrl: AdamantResources.coreDataModel) }.inObjectScope(.container) - + // MARK: Accounts container.register(AccountsProvider.self) { r in AdamantAccountsProvider( @@ -293,7 +362,7 @@ struct AppAssembly: MainThreadAssembly { addressBookService: r.resolve(AddressBookService.self)! ) }.inObjectScope(.container) - + // MARK: Transfers container.register(TransfersProvider.self) { r in AdamantTransfersProvider( @@ -302,12 +371,12 @@ struct AppAssembly: MainThreadAssembly { adamantCore: r.resolve(AdamantCore.self)!, accountService: r.resolve(AccountService.self)!, accountsProvider: r.resolve(AccountsProvider.self)!, - securedStore: r.resolve(SecuredStore.self)!, + SecureStore: r.resolve(SecureStore.self)!, transactionService: r.resolve(ChatTransactionService.self)!, chatsProvider: r.resolve(ChatsProvider.self)! ) }.inObjectScope(.container) - + // MARK: ChatFileService container.register(ChatFileProtocol.self) { r in ChatFileService( @@ -318,14 +387,14 @@ struct AppAssembly: MainThreadAssembly { adamantCore: r.resolve(AdamantCore.self)! ) }.inObjectScope(.container) - + // MARK: FilesStorageProprietiesService container.register(FilesStorageProprietiesProtocol.self) { r in FilesStorageProprietiesService( - securedStore: r.resolve(SecuredStore.self)! + SecureStore: r.resolve(SecureStore.self)! ) }.inObjectScope(.container) - + // MARK: Chats container.register(ChatsProvider.self) { r in AdamantChatsProvider( @@ -336,11 +405,11 @@ struct AppAssembly: MainThreadAssembly { adamantCore: r.resolve(AdamantCore.self)!, accountsProvider: r.resolve(AccountsProvider.self)!, transactionService: r.resolve(ChatTransactionService.self)!, - securedStore: r.resolve(SecuredStore.self)!, + SecureStore: r.resolve(SecureStore.self)!, walletServiceCompose: r.resolve(WalletServiceCompose.self)! ) }.inObjectScope(.container) - + // MARK: Chat Transaction Service container.register(ChatTransactionService.self) { r in AdamantChatTransactionService( @@ -348,7 +417,7 @@ struct AppAssembly: MainThreadAssembly { walletServiceCompose: r.resolve(WalletServiceCompose.self)! ) }.inObjectScope(.container) - + // MARK: Rich transaction status service container.register(TransactionsStatusServiceComposeProtocol.self) { r in TransactionsStatusServiceCompose( @@ -356,18 +425,18 @@ struct AppAssembly: MainThreadAssembly { walletServiceCompose: r.resolve(WalletServiceCompose.self)! ) }.inObjectScope(.container) - + // MARK: Rich transaction reply service container.register(RichTransactionReplyService.self) { r in AdamantRichTransactionReplyService( coreDataStack: r.resolve(CoreDataStack.self)!, apiService: r.resolve(AdamantApiServiceProtocol.self)!, adamantCore: r.resolve(AdamantCore.self)!, - accountService: r.resolve(AccountService.self)!, + accountService: r.resolve(AccountService.self)!, walletServiceCompose: r.resolve(WalletServiceCompose.self)! ) }.inObjectScope(.container) - + // MARK: Rich transaction react service container.register(RichTransactionReactService.self) { r in AdamantRichTransactionReactService( @@ -377,19 +446,19 @@ struct AppAssembly: MainThreadAssembly { accountService: r.resolve(AccountService.self)! ) }.inObjectScope(.container) - + // MARK: Bitcoin AddressConverterFactory container.register(AddressConverterFactory.self) { _ in AddressConverterFactory() }.inObjectScope(.container) - + // MARK: Chat Preservation container.register(ChatPreservationProtocol.self) { _ in ChatPreservation() }.inObjectScope(.container) - + // MARK: Wallet Service Compose - container.register(WalletServiceCompose.self) { r in + container.register(WalletServiceCompose.self) { _ in var wallets: [WalletCoreProtocol] = [ AdmWalletService(), BtcWalletService(), @@ -398,23 +467,23 @@ struct AppAssembly: MainThreadAssembly { DogeWalletService(), DashWalletService() ] - + let erc20WalletServices = ERC20Token.supportedTokens.map { ERC20WalletService(token: $0) } - + wallets.append(contentsOf: erc20WalletServices) - + return AdamantWalletServiceCompose(wallets: wallets) }.inObjectScope(.container).initCompleted { (_, c) in guard let service = c as? AdamantWalletServiceCompose else { return } let wallets = service.getWallets().map { $0.core } - + for case let wallet as SwinjectDependentService in wallets { wallet.injectDependencies(from: container) } } - + // MARK: ApiService Compose container.register(ApiServiceComposeProtocol.self) { ApiServiceCompose( @@ -429,15 +498,35 @@ struct AppAssembly: MainThreadAssembly { infoService: $0.resolve(InfoServiceApiServiceProtocol.self)! ) }.inObjectScope(.transient) - + // MARK: NodesMergingService container.register(NodesMergingServiceProtocol.self) { _ in NodesMergingService() }.inObjectScope(.transient) - + // MARK: DefaultNodesProvider container.register(DefaultNodesProvider.self) { _ in DefaultNodesProvider() }.inObjectScope(.transient) + + container.register(EthBIP32ServiceProtocol.self) { r in + EthBIP32Service(ethApiService: r.resolve(ERC20ApiService.self)!) + }.inObjectScope(.container) + + container.register(CoreDataRealationMapperProtocol.self) { r in + CoreDataRealationMapper(stack: r.resolve(CoreDataStack.self)!) + }.inObjectScope(.container) + } + + // Needs to be called on the register stage to setup the wallets for other dependencies + // That's wrong to do it on registering stage of the app. But it's need to be done as it is done + private func initWalletsStorage(from container: Container) { + let storage = container.resolve(AnyTokensStorage.self) + + if let storage { + storage.loadTokens() + ERC20Token.supportedTokens = ERC20TokenAssembly.getERC20Tokens(tokensStorage: storage) + CoinInfoProvider.storage = storage + } } } diff --git a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/Contents.json b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/Contents.json index 1f0404d29..2cf6ac831 100644 --- a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,116 +1,38 @@ { "images" : [ { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "icon-bold-20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "icon-bold-20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "adamant-icon-bold-29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "adamant-icon-bold-29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "adamant-icon-bold-40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "adamant-icon-bold-40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "adamant-icon-bold-60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "adamant-icon-bold-60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "adamant-icon-bold-20.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "adamant-icon-bold-20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "adamant-icon-bold-29.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "adamant-icon-bold-29@2x-1.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "adamant-icon-bold-40.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "adamant-icon-bold-40@2x-1.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "adamant-icon-bold-76.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "adamant-icon-bold-76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "adamant-icon-bold-83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "adamant-icon-1024.png", - "scale" : "1x" + "filename" : "iOS_white.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "iOS_dark.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "filename" : "iOS_tint.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-1024.png b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-1024.png deleted file mode 100644 index 94ec98329..000000000 Binary files a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-1024.png and /dev/null differ diff --git a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-20.png b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-20.png deleted file mode 100644 index ce5b18fd4..000000000 Binary files a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-20.png and /dev/null differ diff --git a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-20@2x.png b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-20@2x.png deleted file mode 100644 index dab170366..000000000 Binary files a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-20@2x.png and /dev/null differ diff --git a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-29.png b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-29.png deleted file mode 100644 index aa44cf243..000000000 Binary files a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-29.png and /dev/null differ diff --git a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-29@2x-1.png b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-29@2x-1.png deleted file mode 100644 index fcbed194f..000000000 Binary files a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-29@2x-1.png and /dev/null differ diff --git a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-29@2x.png b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-29@2x.png deleted file mode 100644 index fcbed194f..000000000 Binary files a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-29@2x.png and /dev/null differ diff --git a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-29@3x.png b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-29@3x.png deleted file mode 100644 index 400396b5d..000000000 Binary files a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-29@3x.png and /dev/null differ diff --git a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-40.png b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-40.png deleted file mode 100644 index dab170366..000000000 Binary files a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-40.png and /dev/null differ diff --git a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-40@2x-1.png b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-40@2x-1.png deleted file mode 100644 index d2ec0eaa7..000000000 Binary files a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-40@2x-1.png and /dev/null differ diff --git a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-40@2x.png b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-40@2x.png deleted file mode 100644 index d2ec0eaa7..000000000 Binary files a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-40@2x.png and /dev/null differ diff --git a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-40@3x.png b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-40@3x.png deleted file mode 100644 index fbc6fdab4..000000000 Binary files a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-40@3x.png and /dev/null differ diff --git a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-60@2x.png b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-60@2x.png deleted file mode 100644 index fbc6fdab4..000000000 Binary files a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-60@2x.png and /dev/null differ diff --git a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-60@3x.png b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-60@3x.png deleted file mode 100644 index 380b5dd20..000000000 Binary files a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-60@3x.png and /dev/null differ diff --git a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-76.png b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-76.png deleted file mode 100644 index eba51fbce..000000000 Binary files a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-76.png and /dev/null differ diff --git a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-76@2x.png b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-76@2x.png deleted file mode 100644 index 89f36c13b..000000000 Binary files a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-76@2x.png and /dev/null differ diff --git a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-83.5@2x.png b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-83.5@2x.png deleted file mode 100644 index 9b35e407f..000000000 Binary files a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/adamant-icon-bold-83.5@2x.png and /dev/null differ diff --git a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/iOS_dark.png b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/iOS_dark.png new file mode 100644 index 000000000..b6b119e3e Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/iOS_dark.png differ diff --git a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/iOS_tint.png b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/iOS_tint.png new file mode 100644 index 000000000..2cab46d0d Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/iOS_tint.png differ diff --git a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/iOS_white.png b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/iOS_white.png new file mode 100644 index 000000000..b329f3d1f Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/iOS_white.png differ diff --git a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/icon-bold-20@2x.png b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/icon-bold-20@2x.png deleted file mode 100644 index dab170366..000000000 Binary files a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/icon-bold-20@2x.png and /dev/null differ diff --git a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/icon-bold-20@3x.png b/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/icon-bold-20@3x.png deleted file mode 100644 index 784e05992..000000000 Binary files a/Adamant/Assets/Assets.xcassets/AppIcon.appiconset/icon-bold-20@3x.png and /dev/null differ diff --git a/Adamant/Helpers/AddressValidationResult.swift b/Adamant/Helpers/AddressValidationResult.swift index 7113b7fde..4a936609a 100644 --- a/Adamant/Helpers/AddressValidationResult.swift +++ b/Adamant/Helpers/AddressValidationResult.swift @@ -11,7 +11,7 @@ import Foundation enum AddressValidationResult { case valid case invalid(description: String?) - + var isValid: Bool { switch self { case .valid: @@ -20,7 +20,7 @@ enum AddressValidationResult { return false } } - + var errorDescription: String? { switch self { case .valid: diff --git a/Adamant/Helpers/ApiServiceError+Extension.swift b/Adamant/Helpers/ApiServiceError+Extension.swift index a1610bea2..9ca2d394b 100644 --- a/Adamant/Helpers/ApiServiceError+Extension.swift +++ b/Adamant/Helpers/ApiServiceError+Extension.swift @@ -13,28 +13,28 @@ extension ApiServiceError: RichError { var message: String { localizedDescription } - + var level: ErrorLevel { switch self { case .accountNotFound, .notLogged, .networkError, .requestCancelled, .noEndpointsAvailable: return .warning - + case .serverError, .commonError: return .error - + case .internalError: return .internalError } } - + var internalError: Error? { switch self { case .accountNotFound, .notLogged, .serverError, .requestCancelled, .commonError, .noEndpointsAvailable: return nil - + case .internalError(_, let error): return error - + case .networkError(let error): return error } diff --git a/Adamant/Helpers/BigInt+Decimal.swift b/Adamant/Helpers/BigInt+Decimal.swift index 2e62c6eeb..5378956c8 100644 --- a/Adamant/Helpers/BigInt+Decimal.swift +++ b/Adamant/Helpers/BigInt+Decimal.swift @@ -6,13 +6,13 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation import BigInt +import Foundation extension BigInt { func asDecimal(exponent: Int) -> Decimal { let decim = Decimal(floatLiteral: Double(self)) - + if exponent != 0 { return Decimal(sign: decim.sign, exponent: exponent, significand: decim) } else { @@ -24,18 +24,18 @@ extension BigInt { extension BigUInt { func asDecimal(exponent: Int) -> Decimal { let decim = Decimal(string: String(self)) ?? 0 - + if exponent != 0 { return Decimal(sign: .plus, exponent: exponent, significand: decim) } else { return decim } } - + func asDouble() -> Double { return Double(string: String(self)) ?? 0 } - + func toWei() -> BigUInt { return BigUInt(self * 1_000_000_000) } diff --git a/Adamant/Helpers/Comparable+clamped.swift b/Adamant/Helpers/Comparable+clamped.swift index f492a5c6a..2b861212f 100644 --- a/Adamant/Helpers/Comparable+clamped.swift +++ b/Adamant/Helpers/Comparable+clamped.swift @@ -13,11 +13,11 @@ extension Comparable { if self < min { return min } - + if self > max { return max } - + return self } } diff --git a/Adamant/Helpers/CoreDataHelpers/ChatMessagesSortDescriptor.swift b/Adamant/Helpers/CoreDataHelpers/ChatMessagesSortDescriptor.swift new file mode 100644 index 000000000..49478e6f7 --- /dev/null +++ b/Adamant/Helpers/CoreDataHelpers/ChatMessagesSortDescriptor.swift @@ -0,0 +1,18 @@ +// +// CoreData+Helpers.swift +// Adamant +// +// Created by Sergei Veretennikov on 21.02.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import CoreData + +extension Array where Element == NSSortDescriptor { + static func sortChatTransactions(ascending: Bool) -> [NSSortDescriptor] { + [ + NSSortDescriptor(key: "timestampMs", ascending: ascending), + NSSortDescriptor(key: "transactionId", ascending: ascending) + ] + } +} diff --git a/Adamant/Helpers/CustomFieldRow.swift b/Adamant/Helpers/CustomFieldRow.swift new file mode 100644 index 000000000..5b7ec2dd8 --- /dev/null +++ b/Adamant/Helpers/CustomFieldRow.swift @@ -0,0 +1,473 @@ +import Eureka +import UIKit + +// Copy of _FieldCell from Eureka with generic descendant of UITextField class +open class CustomFieldCell: Cell, UITextFieldDelegate, TextFieldCell where T: Equatable, T: InputTypeInitiable { + + weak var _textField: TextFieldType! + public var textField: UITextField! { + _textField + } + weak var titleLabel: UILabel? + + fileprivate var observingTitleText = false + private var awakeFromNibCalled = false + + open var dynamicConstraints = [NSLayoutConstraint]() + + private var calculatedTitlePercentage: CGFloat = 0.7 + + public required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + + let textField = TextFieldType() + self._textField = textField + textField.translatesAutoresizingMaskIntoConstraints = false + + super.init(style: style, reuseIdentifier: reuseIdentifier) + + setupTitleLabel() + + contentView.addSubview(titleLabel!) + contentView.addSubview(textField) + + NotificationCenter.default.addObserver(forName: UIApplication.willResignActiveNotification, object: nil, queue: nil) { [weak self] _ in + guard let me = self else { return } + guard me.observingTitleText else { return } + me.titleLabel?.removeObserver(me, forKeyPath: "text") + me.observingTitleText = false + } + NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in + guard let me = self else { return } + guard !me.observingTitleText else { return } + me.titleLabel?.addObserver(me, forKeyPath: "text", options: [.new, .old], context: nil) + me.observingTitleText = true + } + + NotificationCenter.default.addObserver(forName: UIContentSizeCategory.didChangeNotification, object: nil, queue: nil) { [weak self] _ in + self?.setupTitleLabel() + self?.setNeedsUpdateConstraints() + } + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + open override func awakeFromNib() { + super.awakeFromNib() + awakeFromNibCalled = true + } + + deinit { + textField?.delegate = nil + textField?.removeTarget(self, action: nil, for: .allEvents) + guard !awakeFromNibCalled else { return } + if observingTitleText { + titleLabel?.removeObserver(self, forKeyPath: "text") + } + imageView?.removeObserver(self, forKeyPath: "image") + NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil) + NotificationCenter.default.removeObserver(self, name: UIContentSizeCategory.didChangeNotification, object: nil) + } + + open override func setup() { + super.setup() + selectionStyle = .none + + if !awakeFromNibCalled { + titleLabel?.addObserver(self, forKeyPath: "text", options: [.new, .old], context: nil) + observingTitleText = true + imageView?.addObserver(self, forKeyPath: "image", options: [.new, .old], context: nil) + } + textField.addTarget(self, action: #selector(CustomFieldCell.textFieldDidChange(_:)), for: .editingChanged) + + if let titleLabel = titleLabel { + // Make sure the title takes over most of the empty space so that the text field starts editing at the back. + let priority = UILayoutPriority(rawValue: titleLabel.contentHuggingPriority(for: .horizontal).rawValue + 1) + textField.setContentHuggingPriority(priority, for: .horizontal) + } + } + + open override func update() { + super.update() + detailTextLabel?.text = nil + + if !awakeFromNibCalled { + if let title = row.title { + switch row.cellStyle { + case .subtitle: + textField.textAlignment = .left + textField.clearButtonMode = .whileEditing + default: + textField.textAlignment = title.isEmpty ? .left : .right + textField.clearButtonMode = title.isEmpty ? .whileEditing : .never + } + } else { + textField.textAlignment = .left + textField.clearButtonMode = .whileEditing + } + } else { + textLabel?.text = nil + titleLabel?.text = row.title + if #available(iOS 13.0, *) { + titleLabel?.textColor = row.isDisabled ? .tertiaryLabel : .label + } else { + titleLabel?.textColor = row.isDisabled ? .gray : .black + } + } + textField.delegate = self + textField.text = row.displayValueFor?(row.value) + textField.isEnabled = !row.isDisabled + if #available(iOS 13.0, *) { + textField.textColor = row.isDisabled ? .tertiaryLabel : .label + } else { + textField.textColor = row.isDisabled ? .gray : .black + } + textField.font = .preferredFont(forTextStyle: .body) + if let placeholder = (row as? FieldRowConformance)?.placeholder { + if let color = (row as? FieldRowConformance)?.placeholderColor { + textField.attributedPlaceholder = NSAttributedString(string: placeholder, attributes: [.foregroundColor: color]) + } else { + textField.placeholder = (row as? FieldRowConformance)?.placeholder + } + } + if row.isHighlighted { + titleLabel?.textColor = tintColor + } + } + + open override func cellCanBecomeFirstResponder() -> Bool { + return !row.isDisabled && textField?.canBecomeFirstResponder == true + } + + open override func cellBecomeFirstResponder(withDirection: Direction) -> Bool { + return textField.becomeFirstResponder() + } + + open override func cellResignFirstResponder() -> Bool { + return textField.resignFirstResponder() + } + + open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) { + let obj = object as AnyObject? + + if let keyPathValue = keyPath, let changeType = change?[NSKeyValueChangeKey.kindKey], + ((obj === titleLabel && keyPathValue == "text") || (obj === imageView && keyPathValue == "image")) + && (changeType as? NSNumber)?.uintValue == NSKeyValueChange.setting.rawValue + { + setNeedsUpdateConstraints() + updateConstraintsIfNeeded() + } + } + + // MARK: Helpers + + open func customConstraints() { + + guard !awakeFromNibCalled else { return } + contentView.removeConstraints(dynamicConstraints) + dynamicConstraints = [] + + switch row.cellStyle { + case .subtitle: + var views: [String: AnyObject] = ["textField": textField] + + if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty { + views["titleLabel"] = titleLabel + dynamicConstraints += NSLayoutConstraint.constraints( + withVisualFormat: "V:|-[titleLabel]-3-[textField]-|", + options: .alignAllLeading, + metrics: nil, + views: views + ) + titleLabel.setContentHuggingPriority( + UILayoutPriority(textField.contentHuggingPriority(for: .vertical).rawValue + 1), + for: .vertical + ) + dynamicConstraints.append( + NSLayoutConstraint( + item: titleLabel, + attribute: .centerX, + relatedBy: .equal, + toItem: textField, + attribute: .centerX, + multiplier: 1, + constant: 0 + ) + ) + } else { + dynamicConstraints.append( + NSLayoutConstraint( + item: textField!, + attribute: .centerY, + relatedBy: .equal, + toItem: contentView, + attribute: .centerY, + multiplier: 1, + constant: 0 + ) + ) + } + + if let imageView = imageView, imageView.image != nil { + views["imageView"] = imageView + if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty { + dynamicConstraints += NSLayoutConstraint.constraints( + withVisualFormat: "H:[imageView]-(15)-[titleLabel]-|", + options: [], + metrics: nil, + views: views + ) + dynamicConstraints += NSLayoutConstraint.constraints( + withVisualFormat: "H:[imageView]-(15)-[textField]-|", + options: [], + metrics: nil, + views: views + ) + } else { + dynamicConstraints += NSLayoutConstraint.constraints( + withVisualFormat: "H:[imageView]-(15)-[textField]-|", + options: [], + metrics: nil, + views: views + ) + } + } else { + if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty { + dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[titleLabel]-|", options: [], metrics: nil, views: views) + dynamicConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[textField]-|", options: [], metrics: nil, views: views) + } else { + dynamicConstraints += NSLayoutConstraint.constraints( + withVisualFormat: "H:|-[textField]-|", + options: .alignAllLeft, + metrics: nil, + views: views + ) + } + } + + default: + var views: [String: AnyObject] = ["textField": textField] + dynamicConstraints += NSLayoutConstraint.constraints( + withVisualFormat: "V:|-[textField]-|", + options: .alignAllLastBaseline, + metrics: nil, + views: views + ) + + if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty { + views["titleLabel"] = titleLabel + dynamicConstraints += NSLayoutConstraint.constraints( + withVisualFormat: "V:|-[titleLabel]-|", + options: .alignAllLastBaseline, + metrics: nil, + views: views + ) + dynamicConstraints.append( + NSLayoutConstraint( + item: titleLabel, + attribute: .centerY, + relatedBy: .equal, + toItem: textField, + attribute: .centerY, + multiplier: 1, + constant: 0 + ) + ) + } + + if let imageView = imageView, imageView.image != nil { + views["imageView"] = imageView + if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty { + dynamicConstraints += NSLayoutConstraint.constraints( + withVisualFormat: "H:[imageView]-(15)-[titleLabel]-[textField]-|", + options: [], + metrics: nil, + views: views + ) + dynamicConstraints.append( + NSLayoutConstraint( + item: titleLabel, + attribute: .width, + relatedBy: (row as? FieldRowConformance)?.titlePercentage != nil ? .equal : .lessThanOrEqual, + toItem: contentView, + attribute: .width, + multiplier: calculatedTitlePercentage, + constant: 0.0 + ) + ) + } else { + dynamicConstraints += NSLayoutConstraint.constraints( + withVisualFormat: "H:[imageView]-(15)-[textField]-|", + options: [], + metrics: nil, + views: views + ) + } + } else { + if let titleLabel = titleLabel, let text = titleLabel.text, !text.isEmpty { + dynamicConstraints += NSLayoutConstraint.constraints( + withVisualFormat: "H:|-[titleLabel]-[textField]-|", + options: [], + metrics: nil, + views: views + ) + dynamicConstraints.append( + NSLayoutConstraint( + item: titleLabel, + attribute: .width, + relatedBy: (row as? FieldRowConformance)?.titlePercentage != nil ? .equal : .lessThanOrEqual, + toItem: contentView, + attribute: .width, + multiplier: calculatedTitlePercentage, + constant: 0.0 + ) + ) + } else { + dynamicConstraints += NSLayoutConstraint.constraints( + withVisualFormat: "H:|-[textField]-|", + options: .alignAllLeft, + metrics: nil, + views: views + ) + } + } + } + contentView.addConstraints(dynamicConstraints) + } + + open override func updateConstraints() { + customConstraints() + super.updateConstraints() + } + + @objc open func textFieldDidChange(_ textField: UITextField) { + + guard textField.markedTextRange == nil else { return } + + guard let textValue = textField.text else { + row.value = nil + return + } + guard let fieldRow = row as? FieldRowConformance, let formatter = fieldRow.formatter else { + row.value = textValue.isEmpty ? nil : (T.init(string: textValue) ?? row.value) + return + } + if fieldRow.useFormatterDuringInput { + let unsafePointer = UnsafeMutablePointer.allocate(capacity: 1) + defer { + unsafePointer.deallocate() + } + let value: AutoreleasingUnsafeMutablePointer = AutoreleasingUnsafeMutablePointer.init(unsafePointer) + let errorDesc: AutoreleasingUnsafeMutablePointer? = nil + if formatter.getObjectValue(value, for: textValue, errorDescription: errorDesc) { + row.value = value.pointee as? T + guard var selStartPos = textField.selectedTextRange?.start else { return } + let oldVal = textField.text + textField.text = row.displayValueFor?(row.value) + selStartPos = + (formatter as? FormatterProtocol)?.getNewPosition( + forPosition: selStartPos, + inTextInput: textField, + oldValue: oldVal, + newValue: textField.text + ) ?? selStartPos + textField.selectedTextRange = textField.textRange(from: selStartPos, to: selStartPos) + return + } + } else { + let unsafePointer = UnsafeMutablePointer.allocate(capacity: 1) + defer { + unsafePointer.deallocate() + } + let value: AutoreleasingUnsafeMutablePointer = AutoreleasingUnsafeMutablePointer.init(unsafePointer) + let errorDesc: AutoreleasingUnsafeMutablePointer? = nil + if formatter.getObjectValue(value, for: textValue, errorDescription: errorDesc) { + row.value = value.pointee as? T + } else { + row.value = textValue.isEmpty ? nil : (T.init(string: textValue) ?? row.value) + } + } + } + + // MARK: Helpers + + private func setupTitleLabel() { + titleLabel = self.textLabel + titleLabel?.translatesAutoresizingMaskIntoConstraints = false + titleLabel?.setContentHuggingPriority(UILayoutPriority(rawValue: 500), for: .horizontal) + titleLabel?.setContentCompressionResistancePriority(UILayoutPriority(rawValue: 1000), for: .horizontal) + } + + private func displayValue(useFormatter: Bool) -> String? { + guard let v = row.value else { return nil } + if let formatter = (row as? FormatterConformance)?.formatter, useFormatter { + return textField?.isFirstResponder == true ? formatter.editingString(for: v) : formatter.string(for: v) + } + return String(describing: v) + } + + // MARK: TextFieldDelegate + + open func textFieldDidBeginEditing(_ textField: UITextField) { + formViewController()?.beginEditing(of: self) + formViewController()?.textInputDidBeginEditing(textField, cell: self) + if let fieldRowConformance = row as? FormatterConformance, fieldRowConformance.formatter != nil, + fieldRowConformance.useFormatterOnDidBeginEditing ?? fieldRowConformance.useFormatterDuringInput + { + textField.text = displayValue(useFormatter: true) + } else { + textField.text = displayValue(useFormatter: false) + } + } + + open func textFieldDidEndEditing(_ textField: UITextField) { + formViewController()?.endEditing(of: self) + formViewController()?.textInputDidEndEditing(textField, cell: self) + textFieldDidChange(textField) + textField.text = displayValue(useFormatter: (row as? FormatterConformance)?.formatter != nil) + } + + open func textFieldShouldReturn(_ textField: UITextField) -> Bool { + return formViewController()?.textInputShouldReturn(textField, cell: self) ?? true + } + + open func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + return formViewController()?.textInput(textField, shouldChangeCharactersInRange: range, replacementString: string, cell: self) ?? true + } + + open func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { + return formViewController()?.textInputShouldBeginEditing(textField, cell: self) ?? true + } + + open func textFieldShouldClear(_ textField: UITextField) -> Bool { + return formViewController()?.textInputShouldClear(textField, cell: self) ?? true + } + + open func textFieldShouldEndEditing(_ textField: UITextField) -> Bool { + return formViewController()?.textInputShouldEndEditing(textField, cell: self) ?? true + } + + open override func layoutSubviews() { + super.layoutSubviews() + guard let row = (row as? FieldRowConformance) else { return } + defer { + // As titleLabel is the textLabel, iOS may re-layout without updating constraints, for example: + // swiping, showing alert or actionsheet from the same section. + // thus we need forcing update to use customConstraints() + setNeedsUpdateConstraints() + updateConstraintsIfNeeded() + } + guard let titlePercentage = row.titlePercentage else { return } + var targetTitleWidth = bounds.size.width * titlePercentage + if let imageView = imageView, imageView.image != nil, let titleLabel = titleLabel { + var extraWidthToSubtract = titleLabel.frame.minX - imageView.frame.minX // Left-to-right interface layout + if UIView.userInterfaceLayoutDirection(for: self.semanticContentAttribute) == .rightToLeft { + extraWidthToSubtract = imageView.frame.maxX - titleLabel.frame.maxX + } + targetTitleWidth -= extraWidthToSubtract + } + calculatedTitlePercentage = targetTitleWidth / contentView.bounds.size.width + } +} diff --git a/Adamant/Helpers/EdgeInsetTextField.swift b/Adamant/Helpers/EdgeInsetTextField.swift new file mode 100644 index 000000000..feeaf62f1 --- /dev/null +++ b/Adamant/Helpers/EdgeInsetTextField.swift @@ -0,0 +1,9 @@ +import UIKit + +final class EdgeInsetTextField: UITextField { + var caretInset: CGFloat = .zero + + public override func caretRect(for position: UITextPosition) -> CGRect { + super.caretRect(for: position).offsetBy(dx: -caretInset, dy: 0) + } +} diff --git a/Adamant/Helpers/MainThreadAssembly.swift b/Adamant/Helpers/MainThreadAssembly.swift index b1e40cb11..8abf297bf 100644 --- a/Adamant/Helpers/MainThreadAssembly.swift +++ b/Adamant/Helpers/MainThreadAssembly.swift @@ -6,9 +6,9 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Swinject import CommonKit import Foundation +import Swinject @MainActor protocol MainThreadAssembly: Assembly, Sendable { @@ -18,7 +18,7 @@ protocol MainThreadAssembly: Assembly, Sendable { extension MainThreadAssembly { nonisolated func assemble(container: Container) { let sendable = Atomic(container) - + MainActor.assumeIsolatedSafe { assembleOnMainThread(container: sendable.value) } diff --git a/Adamant/Helpers/Markdown+Adamant.swift b/Adamant/Helpers/Markdown+Adamant.swift index d827a2244..b9b338142 100644 --- a/Adamant/Helpers/Markdown+Adamant.swift +++ b/Adamant/Helpers/Markdown+Adamant.swift @@ -6,30 +6,30 @@ // Copyright © 2022 Adamant. All rights reserved. // -import UIKit -import MarkdownKit import CommonKit +import MarkdownKit +import UIKit // MARK: Detect simple ADM address // - ex: U3716604363012166999 final class MarkdownSimpleAdm: MarkdownElement { private static let regex = "U([0-9]{6,20})" - + var regex: String { return MarkdownSimpleAdm.regex } - + func regularExpression() throws -> NSRegularExpression { return try NSRegularExpression(pattern: regex, options: .dotMatchesLineSeparators) } - + func match(_ match: NSTextCheckingResult, attributedString: NSMutableAttributedString) { - let attributesColor: [NSAttributedString.Key : Any] = [ + let attributesColor: [NSAttributedString.Key: Any] = [ NSAttributedString.Key.foregroundColor: UIColor.adamant.active, NSAttributedString.Key.underlineStyle: 0, NSAttributedString.Key.underlineColor: UIColor.clear ] - + let nsString = (attributedString.string as NSString) let address = nsString.substring(with: match.range) guard let url = URL(string: "adm:\(address)") else { return } @@ -44,11 +44,11 @@ final class MarkdownAdvancedAdm: MarkdownLink { private static let regex = "\\[[^\\(]*\\]\\(adm:[^\\s]+\\)" private static let onlyLinkRegex = "\\(adm:[^\\s]+\\)" private static let onlyAddressRegex = "U([0-9]{6,20})" - + override var regex: String { return MarkdownAdvancedAdm.regex } - + var attributes: [NSAttributedString.Key: AnyObject] { var attributes = [NSAttributedString.Key: AnyObject]() if let font = font { @@ -61,61 +61,71 @@ final class MarkdownAdvancedAdm: MarkdownLink { attributes[NSAttributedString.Key.underlineColor] = UIColor.clear return attributes } - + override func match(_ match: NSTextCheckingResult, attributedString: NSMutableAttributedString) { let nsString = (attributedString.string as NSString) let urlString = nsString.substring(with: match.range) - + guard let onlyLinkRegex = try? NSRegularExpression(pattern: MarkdownAdvancedAdm.onlyLinkRegex, options: .dotMatchesLineSeparators), - let onlyAddressRegex = try? NSRegularExpression(pattern: MarkdownAdvancedAdm.onlyAddressRegex, options: .dotMatchesLineSeparators) + let onlyAddressRegex = try? NSRegularExpression(pattern: MarkdownAdvancedAdm.onlyAddressRegex, options: .dotMatchesLineSeparators) else { return } - - guard let linkMatch = onlyLinkRegex.firstMatch(in: urlString, - options: .withoutAnchoringBounds, - range: NSRange( - location: 0, - length: urlString.count - )), - let addressMatch = onlyAddressRegex.firstMatch(in: urlString, - options: .withoutAnchoringBounds, - range: NSRange( - location: 0, - length: urlString.count - )) + + guard + let linkMatch = onlyLinkRegex.firstMatch( + in: urlString, + options: .withoutAnchoringBounds, + range: NSRange( + location: 0, + length: urlString.count + ) + ), + let addressMatch = onlyAddressRegex.firstMatch( + in: urlString, + options: .withoutAnchoringBounds, + range: NSRange( + location: 0, + length: urlString.count + ) + ) else { return } - + let urlLinkAbsoluteStart = match.range.location - let linkURLString = nsString + let linkURLString = + nsString .substring(with: NSRange(location: urlLinkAbsoluteStart + linkMatch.range.location + 1, length: linkMatch.range.length - 2)) - let addressString = nsString + let addressString = + nsString .substring(with: NSRange(location: urlLinkAbsoluteStart + addressMatch.range.location, length: addressMatch.range.length)) - - let nameString = nsString + + let nameString = + nsString .substring(with: NSRange(location: match.range.location, length: 2)) let separator = nameString != "[]" ? ":" : "" - + // deleting trailing markdown let trailingMarkdownRange = NSRange(location: urlLinkAbsoluteStart + linkMatch.range.location - 1, length: linkMatch.range.length + 1) attributedString.deleteCharacters(in: trailingMarkdownRange) - + // deleting leading markdown let leadingMarkdownRange = NSRange(location: match.range.location, length: 1) attributedString.deleteCharacters(in: leadingMarkdownRange) - + // insert address attributedString.insert(NSAttributedString(string: "\(separator)\(addressString)"), at: urlLinkAbsoluteStart + linkMatch.range.location - 2) - - let formatRange = NSRange(location: match.range.location, - length: (linkMatch.range.location - 2) + addressString.count + separator.count) - + + let formatRange = NSRange( + location: match.range.location, + length: (linkMatch.range.location - 2) + addressString.count + separator.count + ) + formatText(attributedString, range: formatRange, link: linkURLString) addAttributes(attributedString, range: formatRange, link: linkURLString) } - + override func addAttributes(_ attributedString: NSMutableAttributedString, range: NSRange, link: String) { attributedString.addAttributes(attributes, range: range) } @@ -124,12 +134,13 @@ final class MarkdownAdvancedAdm: MarkdownLink { // MARK: Detect link ADM address // - ex: https://anydomainOrIP?address=U9821606738809290000&label=John+Doe final class MarkdownLinkAdm: MarkdownLink { - private static let regex = "(?i)\\b((?:https?://|www\\d{0,3}[.]|[a-z0-9.\\-]+[.][a-z]{2,4}/)(?:[^\\s()<>]+(\\?address=U([0-9]{6,20}))[^\\s()<>]+)+(?:\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*\\)|[^\\s`!()\\[\\]{};:'\".,<>?«»“”‘’]))" - + private static let regex = + "(?i)\\b((?:https?://|www\\d{0,3}[.]|[a-z0-9.\\-]+[.][a-z]{2,4}/)(?:[^\\s()<>]+(\\?address=U([0-9]{6,20}))[^\\s()<>]+)+(?:\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*\\)|[^\\s`!()\\[\\]{};:'\".,<>?«»“”‘’]))" + override var regex: String { return MarkdownLinkAdm.regex } - + var attributes: [NSAttributedString.Key: AnyObject] { var attributes = [NSAttributedString.Key: AnyObject]() if let font = font { @@ -142,17 +153,17 @@ final class MarkdownLinkAdm: MarkdownLink { attributes[NSAttributedString.Key.underlineColor] = UIColor.clear return attributes } - + override func match(_ match: NSTextCheckingResult, attributedString: NSMutableAttributedString) { let nsString = (attributedString.string as NSString) let urlString = nsString.substring(with: match.range) - + guard let adm = urlString.getAdamantAddress(), - var urlComponents = URLComponents(string: "adm:\(adm.address)") + var urlComponents = URLComponents(string: "adm:\(adm.address)") else { return } - + var queryItems: [URLQueryItem] = [] if let name = adm.name { queryItems.append(URLQueryItem(name: "label", value: name)) @@ -162,16 +173,18 @@ final class MarkdownLinkAdm: MarkdownLink { } urlComponents.queryItems = queryItems guard let url = urlComponents.url else { return } - + // replace url with adm address attributedString.replaceCharacters(in: match.range, with: adm.address) - - let formatRange = NSRange(location: match.range.location, - length: adm.address.count) + + let formatRange = NSRange( + location: match.range.location, + length: adm.address.count + ) formatText(attributedString, range: formatRange, link: url.absoluteString) addAttributes(attributedString, range: formatRange, link: url.absoluteString) } - + override func addAttributes(_ attributedString: NSMutableAttributedString, range: NSRange, link: String) { attributedString.addAttributes(attributes, range: range) } @@ -181,16 +194,16 @@ final class MarkdownLinkAdm: MarkdownLink { final class MarkdownCodeAdamant: MarkdownCommonElement { private static let regex = "\\`[^\\`]*(?:\\`\\`[^\\`]*)*\\`" private let char = "`" - + var font: MarkdownFont? var color: MarkdownColor? var textHighlightColor: MarkdownColor? var textBackgroundColor: MarkdownColor? - + var regex: String { return MarkdownCodeAdamant.regex } - + init( font: MarkdownFont? = MarkdownCode.defaultFont, color: MarkdownColor? = nil, @@ -202,14 +215,14 @@ final class MarkdownCodeAdamant: MarkdownCommonElement { self.textHighlightColor = textHighlightColor self.textBackgroundColor = textBackgroundColor } - + func addAttributes(_ attributedString: NSMutableAttributedString, range: NSRange) { var matchString: String = attributedString.attributedSubstring(from: range).string matchString = matchString.replacingOccurrences(of: char, with: "") attributedString.replaceCharacters(in: range, with: matchString) - + var codeAttributes = attributes - + textHighlightColor.flatMap { codeAttributes[NSAttributedString.Key.foregroundColor] = $0 } @@ -217,11 +230,11 @@ final class MarkdownCodeAdamant: MarkdownCommonElement { codeAttributes[NSAttributedString.Key.backgroundColor] = $0 } font.flatMap { codeAttributes[NSAttributedString.Key.font] = $0 } - + let updatedRange = (attributedString.string as NSString).range(of: matchString) attributedString.addAttributes(codeAttributes, range: NSRange(location: range.location, length: updatedRange.length)) } - + func match(_ match: NSTextCheckingResult, attributedString: NSMutableAttributedString) { addAttributes(attributedString, range: match.range) } @@ -232,7 +245,7 @@ final class MarkdownCodeAdamant: MarkdownCommonElement { final class MarkdownFileRaw: MarkdownElement { private let emoji: String private let matchFont: UIFont - + init( emoji: String, font: UIFont @@ -240,34 +253,34 @@ final class MarkdownFileRaw: MarkdownElement { self.emoji = emoji self.matchFont = font } - + var regex: String { return "\(emoji)\\d{0,2}" } - + func regularExpression() throws -> NSRegularExpression { try NSRegularExpression(pattern: regex, options: .dotMatchesLineSeparators) } - + func match(_ match: NSTextCheckingResult, attributedString: NSMutableAttributedString) { - let attributesColor: [NSAttributedString.Key : Any] = [ + let attributesColor: [NSAttributedString.Key: Any] = [ .foregroundColor: UIColor.lightGray, .font: matchFont, .baselineOffset: -3.0 ] - + let nsString = (attributedString.string as NSString) let matchText = nsString.substring(with: match.range) - + let textWithoutEmoji = matchText.replacingOccurrences(of: emoji, with: "") let countRange = (matchText as NSString).range(of: textWithoutEmoji) let emojiRange = (matchText as NSString).range(of: emoji) - + let range = NSRange( location: match.range.location + emojiRange.length, length: countRange.length ) - + attributedString.addAttributes( attributesColor, range: range diff --git a/Adamant/Helpers/MessageCellWrapper.swift b/Adamant/Helpers/MessageCellWrapper.swift index 4e0c277e8..badd8f59d 100644 --- a/Adamant/Helpers/MessageCellWrapper.swift +++ b/Adamant/Helpers/MessageCellWrapper.swift @@ -6,31 +6,31 @@ // Copyright © 2023 Adamant. All rights reserved. // +import CommonKit import MessageKit -import UIKit import SnapKit -import CommonKit +import UIKit final class MessageCellWrapper: MessageReusableView { let wrappedView = View() - + override init(frame: CGRect) { super.init(frame: frame) configure() } - + required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) configure() } - + override func prepareForReuse() { wrappedView.prepareForReuse() } } -private extension MessageCellWrapper { - func configure() { +extension MessageCellWrapper { + fileprivate func configure() { addSubview(wrappedView) wrappedView.snp.makeConstraints { $0.directionalEdges.equalToSuperview() diff --git a/Adamant/Helpers/MyLittlePinpad+adamant.swift b/Adamant/Helpers/MyLittlePinpad+adamant.swift index 34c06bda6..59949af49 100644 --- a/Adamant/Helpers/MyLittlePinpad+adamant.swift +++ b/Adamant/Helpers/MyLittlePinpad+adamant.swift @@ -25,10 +25,10 @@ extension PinpadBiometryButtonType { switch self { case .hidden: return .none - + case .faceID: return .faceID - + case .touchID: return .touchID } @@ -40,10 +40,10 @@ extension BiometryType { switch self { case .none: return .hidden - + case .faceID: return .faceID - + case .touchID: return .touchID } @@ -53,31 +53,31 @@ extension BiometryType { extension PinpadViewController { static func adamantPinpad(biometryButton: PinpadBiometryButtonType) -> PinpadViewController { let pinpad = PinpadViewController.instantiateFromResourceNib() - + pinpad.bordersColor = UIColor.adamant.secondary pinpad.setColor(UIColor.adamant.primary, for: .normal) pinpad.buttonsHighlightedColor = UIColor.adamant.pinpadHighlightButton pinpad.buttonsFont = UIFont.adamantPrimary(ofSize: pinpad.buttonsFont.pointSize, weight: .light) - + pinpad.placeholdersSize = 15 - + if pinpad.view.frame.height > 600 { pinpad.buttonsSize = 75 pinpad.buttonsSpacing = 20 pinpad.placeholderViewHeight = 50 - } else {// iPhone 5 + } else { // iPhone 5 pinpad.buttonsSize = 70 pinpad.buttonsSpacing = 15 pinpad.placeholderViewHeight = 25 pinpad.bottomSpacing = 24 pinpad.pinpadToCancelSpacing = 14 } - + pinpad.placeholderActiveColor = UIColor.adamant.pinpadHighlightButton pinpad.biometryButtonType = biometryButton pinpad.cancelButton.setTitle(String.adamant.alert.cancel, for: .normal) pinpad.pinDigits = 6 - + return pinpad } } diff --git a/Adamant/Helpers/NSAttributedText+Adamant.swift b/Adamant/Helpers/NSAttributedText+Adamant.swift index 0d29c6e3c..9aa8104b5 100644 --- a/Adamant/Helpers/NSAttributedText+Adamant.swift +++ b/Adamant/Helpers/NSAttributedText+Adamant.swift @@ -11,14 +11,14 @@ import UIKit extension NSAttributedString { func resolveLinkColor(_ color: UIColor = UIColor.adamant.active) -> NSMutableAttributedString { let mutableText = NSMutableAttributedString(attributedString: self) - + mutableText.enumerateAttribute( .link, in: NSRange(location: 0, length: self.length), options: [] ) { (value, range, _) in guard value != nil else { return } - + mutableText.removeAttribute(.link, range: range) mutableText.addAttribute( .foregroundColor, @@ -26,7 +26,7 @@ extension NSAttributedString { range: range ) } - + return mutableText } } diff --git a/Adamant/Helpers/Node+UI.swift b/Adamant/Helpers/Node+UI.swift index d7b9f1be9..ccc2216ac 100644 --- a/Adamant/Helpers/Node+UI.swift +++ b/Adamant/Helpers/Node+UI.swift @@ -14,26 +14,27 @@ extension Node { case date case blocks } - + // swiftlint:disable switch_case_alignment func statusString(showVersion: Bool, heightType: HeightType?) -> String? { guard isEnabled else { return Strings.disabled } - - let statusTitle = switch connectionStatus { - case .allowed: - pingString - case let .synchronizing(isFinal): - isFinal - ? Strings.synchronizing - : Strings.updating - case .offline: - Strings.offline - case .notAllowed(let reason): - reason.text - case .none: - Strings.updating - } - + + let statusTitle = + switch connectionStatus { + case .allowed: + pingString + case let .synchronizing(isFinal): + isFinal + ? Strings.synchronizing + : Strings.updating + case .offline: + Strings.offline + case .notAllowed(let reason): + reason.text + case .none: + Strings.updating + } + let heightString: String? switch heightType { case .date: @@ -43,7 +44,7 @@ extension Node { case nil: heightString = nil } - + return [ statusTitle, showVersion ? versionString : nil, @@ -52,13 +53,13 @@ extension Node { .compactMap { $0 } .joined(separator: " ") } - + func indicatorString(isRest: Bool, isWs: Bool) -> String { let connections = [ isRest ? preferredOrigin.scheme.rawValue : nil, isWs ? "ws" : nil ].compactMap { $0 } - + return [ "●", connections.isEmpty @@ -68,10 +69,10 @@ extension Node { .compactMap { $0 } .joined(separator: " ") } - + var indicatorColor: UIColor { guard isEnabled else { return .adamant.inactive } - + switch connectionStatus { case .allowed: return .adamant.success @@ -85,14 +86,14 @@ extension Node { return .adamant.inactive } } - + var title: String { mainOrigin.asString() } - + var statusStringColor: UIColor { guard isEnabled else { return .adamant.textColor } - + return switch connectionStatus { case .none: .adamant.inactive @@ -100,10 +101,10 @@ extension Node { .adamant.textColor } } - + var titleColor: UIColor { guard isEnabled else { return .adamant.textColor } - + return switch connectionStatus { case .none: .adamant.inactive @@ -113,50 +114,50 @@ extension Node { } } -private extension Node { - enum Strings { +extension Node { + fileprivate enum Strings { static var ping: String { String.localized( "NodesList.NodeCell.Ping", comment: "NodesList.NodeCell: Node ping" ) } - + static var milliseconds: String { String.localized( "NodesList.NodeCell.Milliseconds", comment: "NodesList.NodeCell: Milliseconds" ) } - + static var synchronizing: String { String.localized( "NodesList.NodeCell.Synchronizing", comment: "NodesList.NodeCell: Node is synchronizing" ) } - + static var updating: String { String.localized( "NodesList.NodeCell.Updating", comment: "NodesList.NodeCell: Node is updating" ) } - + static var offline: String { String.localized( "NodesList.NodeCell.Offline", comment: "NodesList.NodeCell: Node is offline" ) } - + static var version: String { String.localized( "NodesList.NodeCell.Version", comment: "NodesList.NodeCell: Node version" ) } - + static var disabled: String { String.localized( "NodesList.NodeCell.Disabled", @@ -164,32 +165,32 @@ private extension Node { ) } } - - var pingString: String? { + + fileprivate var pingString: String? { guard let ping = ping else { return nil } return "\(Strings.ping): \(Int(ping * 1000)) \(Strings.milliseconds)" } - - var blocksHeightString: String? { + + fileprivate var blocksHeightString: String? { height.map { " ❐ \(getFormattedHeight(from: $0))" } } - - var dateHeightString: String? { + + fileprivate var dateHeightString: String? { height.map { " ❐ \(Date(timeIntervalSince1970: .init($0)).humanizedTime().string)" } } - - var versionString: String? { + + fileprivate var versionString: String? { version.map { "(v\($0.string))" } } - - var numberFormatter: NumberFormatter { + + fileprivate var numberFormatter: NumberFormatter { let numberFormatter = NumberFormatter() numberFormatter.numberStyle = .decimal numberFormatter.groupingSeparator = "," return numberFormatter } - - func getFormattedHeight(from height: Int) -> String { + + fileprivate func getFormattedHeight(from height: Int) -> String { numberFormatter.string(from: Decimal(height)) ?? String(height) } } diff --git a/Adamant/Helpers/NodeGroup+Constants.swift b/Adamant/Helpers/NodeGroup+Constants.swift index ac6fd0eef..3c489ae19 100644 --- a/Adamant/Helpers/NodeGroup+Constants.swift +++ b/Adamant/Helpers/NodeGroup+Constants.swift @@ -1,3 +1,4 @@ +import CommonKit // // NodeGroup+Constants.swift // Adamant @@ -6,7 +7,6 @@ // Copyright © 2023 Adamant. All rights reserved. // import Foundation -import CommonKit extension NodeGroup { var onScreenUpdateInterval: TimeInterval { @@ -26,7 +26,7 @@ extension NodeGroup { case .dash: return DashWalletService.healthCheckParameters.onScreenUpdateInterval case .ipfs: - return 10 // TODO: Fix the adamant-wallets script and the repo itself + return 10 // TODO: Fix the adamant-wallets script and the repo itself case .infoService: return AdmWalletService.healthCheckParameters.onScreenServiceUpdateInterval } @@ -100,7 +100,7 @@ extension NodeGroup { return AdmWalletService.healthCheckParameters.normalServiceUpdateInterval } } - + var minNodeVersion: Version? { let version: String? switch self { @@ -121,11 +121,11 @@ extension NodeGroup { case .ipfs, .infoService: version = nil } - guard let version = version else { return nil } - + guard let version = version else { return nil } + return .init(version) } - + var name: String { switch self { case .btc: @@ -136,7 +136,7 @@ extension NodeGroup { return KlyWalletService.tokenNetworkSymbol case .klyService: return KlyWalletService.tokenNetworkSymbol - + " " + .adamant.coinsNodesList.serviceNode + + " " + .adamant.coinsNodesList.serviceNode case .doge: return DogeWalletService.tokenNetworkSymbol case .dash: @@ -149,18 +149,16 @@ extension NodeGroup { return InfoService.name } } - + var heightType: Node.HeightType? { switch self { - case .btc, .eth, .klyNode, .klyService, .doge, .dash, .adm: + case .btc, .eth, .klyNode, .klyService, .doge, .dash, .adm, .ipfs: .blocks case .infoService: .date - case .ipfs: - .none } } - + var blockchainHealthCheckParams: BlockchainHealthCheckParams { .init( group: self, diff --git a/Adamant/Helpers/PasteInterceptingPasswordCell.swift b/Adamant/Helpers/PasteInterceptingPasswordCell.swift new file mode 100644 index 000000000..d685474e0 --- /dev/null +++ b/Adamant/Helpers/PasteInterceptingPasswordCell.swift @@ -0,0 +1,33 @@ +// +// PasteInterceptingPasswordCell.swift +// Adamant +// +// Created by Christian Benua on 23.02.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import Eureka +import UIKit + +final class PasteInterceptingPasswordCell: CustomFieldCell, CellType { + + required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + override func setup() { + super.setup() + textField.autocorrectionType = .no + textField.autocapitalizationType = .none + textField.keyboardType = .asciiCapable + textField.isSecureTextEntry = true + textField.textContentType = .password + if let textLabel = textLabel { + textField.setContentHuggingPriority(textLabel.contentHuggingPriority(for: .horizontal) - 1, for: .horizontal) + } + } +} diff --git a/Adamant/Helpers/PasteInterceptingPasswordRow.swift b/Adamant/Helpers/PasteInterceptingPasswordRow.swift new file mode 100644 index 000000000..a03592ee6 --- /dev/null +++ b/Adamant/Helpers/PasteInterceptingPasswordRow.swift @@ -0,0 +1,15 @@ +// +// PasteInterceptingPasswordRow.swift +// Adamant +// +// Created by Christian Benua on 23.02.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import Eureka + +final class PasteInterceptingPasswordRow: FieldRow, RowType { + required init(tag: String?) { + super.init(tag: tag) + } +} diff --git a/Adamant/Helpers/PasteInterceptingTextField.swift b/Adamant/Helpers/PasteInterceptingTextField.swift new file mode 100644 index 000000000..697ee57ef --- /dev/null +++ b/Adamant/Helpers/PasteInterceptingTextField.swift @@ -0,0 +1,62 @@ +// +// PasteInterceptingTextField.swift +// Adamant +// +// Created by Christian Benua on 23.02.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import UIKit + +final class PasteInterceptingTextField: UITextField, UITextFieldDelegate { + + private var isAfterPaste: Bool = false + var pasteInterceptor: ((String?) -> Void)? + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + private func setup() { + NotificationCenter.default.addObserver( + self, + selector: #selector(handlePasteNotification), + name: UITextField.textDidChangeNotification, + object: self + ) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func paste(_ sender: Any?) { + isAfterPaste = true + super.paste(sender) + } + + @objc func handlePasteNotification() { + if isAfterPaste { + pasteInterceptor?(text) + } + isAfterPaste = false + } + + func textField( + _ textField: UITextField, + shouldChangeCharactersIn range: NSRange, + replacementString string: String + ) -> Bool { + if isAfterPaste { + pasteInterceptor?(textField.text) + } + + isAfterPaste = false + return true + } +} diff --git a/Adamant/Helpers/QRCodeReader+adamant.swift b/Adamant/Helpers/QRCodeReader+adamant.swift index bcbee9eee..f74772e74 100644 --- a/Adamant/Helpers/QRCodeReader+adamant.swift +++ b/Adamant/Helpers/QRCodeReader+adamant.swift @@ -12,11 +12,11 @@ import QRCodeReader extension QRCodeReaderViewController { static func adamantQrCodeReader() -> QRCodeReaderViewController { let builder = QRCodeReaderViewControllerBuilder { - $0.reader = QRCodeReader(metadataObjectTypes: [.qr ], captureDevicePosition: .back) + $0.reader = QRCodeReader(metadataObjectTypes: [.qr], captureDevicePosition: .back) $0.cancelButtonTitle = String.adamant.alert.cancel $0.showSwitchCameraButton = false } - + return QRCodeReaderViewController(builder: builder) } } diff --git a/Adamant/Helpers/SafeDecimalRow.swift b/Adamant/Helpers/SafeDecimalRow.swift index 72c5171e0..12a9806b3 100644 --- a/Adamant/Helpers/SafeDecimalRow.swift +++ b/Adamant/Helpers/SafeDecimalRow.swift @@ -6,8 +6,8 @@ // Copyright © 2022 Adamant. All rights reserved. // -import UIKit import Eureka +import UIKit /// A decimal row without empty hint on MacOS final class SafeDecimalRow: _SafeDecimalRow, RowType { @@ -27,7 +27,7 @@ class _SafeDecimalRow: FieldRow { } } -final class SafeDecimalCell: _FieldCell, CellType { +final class SafeDecimalCell: CustomFieldCell, CellType { required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } @@ -42,3 +42,11 @@ final class SafeDecimalCell: _FieldCell, CellType { textField.setPopupKeyboardType(.decimalPad) } } + +extension CustomFieldCell { + /// Sets hugging priorities to make text field to take as much space as possible + func adjustHuggingPriority() { + textField.setContentHuggingPriority(.defaultLow, for: .horizontal) + titleLabel?.setContentHuggingPriority(.defaultHigh, for: .horizontal) + } +} diff --git a/Adamant/Helpers/SelfRemovableHostingController.swift b/Adamant/Helpers/SelfRemovableHostingController.swift index 8436a5e2b..758c78e10 100644 --- a/Adamant/Helpers/SelfRemovableHostingController.swift +++ b/Adamant/Helpers/SelfRemovableHostingController.swift @@ -11,16 +11,15 @@ import SwiftUI class SelfRemovableHostingController: UIHostingController { override func viewDidLoad() { super.viewDidLoad() - + navigationItem.rightBarButtonItem = UIBarButtonItem( barButtonSystemItem: .done, target: self, action: #selector(close) ) } - + @objc private func close() { dismiss(animated: true) } } - diff --git a/Adamant/Helpers/String+adamant.swift b/Adamant/Helpers/String+adamant.swift index 24db3e1ba..a9c5c1a8c 100644 --- a/Adamant/Helpers/String+adamant.swift +++ b/Adamant/Helpers/String+adamant.swift @@ -6,9 +6,9 @@ // Copyright © 2018 Adamant. All rights reserved. // +import CommonKit import Foundation import UIKit -import CommonKit struct AdamantAddress { let address: String @@ -23,13 +23,17 @@ extension String { let urlString = self.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed), let components = URLComponents(string: urlString), let queryItems = components.queryItems, - let address = queryItems.filter({$0.name == "address"}).first?.value else { + let address = queryItems.filter({ $0.name == "address" }).first?.value + else { return nil } - - let name = queryItems.filter({$0.name == "label"}).first?.value?.replacingOccurrences(of: "+", with: " ").replacingOccurrences(of: "%20", with: " ") - let amount = queryItems.filter({$0.name == "amount"}).first?.value - let message = queryItems.filter({$0.name == "message"}).first?.value?.replacingOccurrences(of: "+", with: " ").replacingOccurrences(of: "%20", with: " ") + + let name = queryItems.filter({ $0.name == "label" }).first?.value?.replacingOccurrences(of: "+", with: " ").replacingOccurrences(of: "%20", with: " ") + let amount = queryItems.filter({ $0.name == "amount" }).first?.value + let message = queryItems.filter({ $0.name == "message" }).first?.value?.replacingOccurrences(of: "+", with: " ").replacingOccurrences( + of: "%20", + with: " " + ) var amountDouble: Double? if let amount = amount { amountDouble = Double(amount) @@ -42,12 +46,12 @@ extension String { var name: String? var message: String? var amount: Double? - + let newUrl = self.replacingOccurrences(of: "//", with: "") - + if let uri = AdamantUriTools.decode(uri: newUrl) { switch uri { - case .address(address: let addr, params: let params): + case .address(address: let addr, let params): address = addr if let params = params { for param in params { @@ -63,7 +67,7 @@ extension String { } } } - case .addressLegacy(address: let addr, params: let params): + case .addressLegacy(address: let addr, let params): address = addr if let params = params { for param in params { @@ -86,39 +90,40 @@ extension String { switch AdamantUtilities.validateAdamantAddress(address: self) { case .valid, .system: address = self - + case .invalid: address = nil } } - + if let address = address { return AdamantAddress( address: address, - name: name, + name: name, amount: amount, message: message ) - } - + } + return nil } - + func addPrefixIfNeeded(prefix: String) -> String { let address = self let prefixLocal = address.prefix(prefix.count) - - let fixedAddress = prefixLocal != prefix - ? "\(prefix)\(address)" - : address - + + let fixedAddress = + prefixLocal != prefix + ? "\(prefix)\(address)" + : address + return fixedAddress } } -public extension NSMutableAttributedString { +extension NSMutableAttributedString { - func apply(font: UIFont, alignment: NSTextAlignment) { + public func apply(font: UIFont, alignment: NSTextAlignment) { let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.alignment = alignment @@ -126,9 +131,9 @@ public extension NSMutableAttributedString { self.setBaseFont(baseFont: font) self.addAttributes([.paragraphStyle: paragraphStyle, .foregroundColor: UIColor.adamant.textColor], range: stringRange) } - + /// Replaces the base font with the given font while preserving traits like bold and italic - func setBaseFont(baseFont: UIFont) { + public func setBaseFont(baseFont: UIFont) { let baseDescriptor = baseFont.fontDescriptor let wholeRange = NSRange(location: 0, length: length) beginEditing() diff --git a/Adamant/Helpers/String+localized.swift b/Adamant/Helpers/String+localized.swift index f35bb4aa6..bd57dda18 100644 --- a/Adamant/Helpers/String+localized.swift +++ b/Adamant/Helpers/String+localized.swift @@ -6,8 +6,8 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation extension String.adamant { enum alert { @@ -22,7 +22,11 @@ extension String.adamant { String.localized("Shared.Save", comment: "Shared alert 'Save' button. Used anywhere") } static var settings: String { - String.localized("Shared.Settings", comment: "Shared alert 'Settings' button. Used to go to system Settings app, on application settings page. Should be same as Settings application title.") + String.localized( + "Shared.Settings", + comment: + "Shared alert 'Settings' button. Used to go to system Settings app, on application settings page. Should be same as Settings application title." + ) } static var retry: String { String.localized("Shared.Retry", comment: "Shared alert 'Retry' button. Used anywhere") @@ -30,7 +34,7 @@ extension String.adamant { static var delete: String { String.localized("Shared.Delete", comment: "Shared alert 'Delete' button. Used anywhere") } - + // MARK: Titles and messages static var error: String { String.localized("Shared.Error", comment: "Shared alert 'Error' title. Used anywhere") @@ -39,12 +43,18 @@ extension String.adamant { String.localized("Shared.Done", comment: "Shared alert Done message. Used anywhere") } static var retryOrDeleteTitle: String { - String.localized("Chats.RetryOrDelete.Title", comment: "Alert 'Retry Or Delete' title. Used in caht for sending failed messages again or delete them") + String.localized( + "Chats.RetryOrDelete.Title", + comment: "Alert 'Retry Or Delete' title. Used in caht for sending failed messages again or delete them" + ) } static var retryOrDeleteBody: String { - String.localized("Chats.RetryOrDelete.Body", comment: "Alert 'Retry Or Delete' body message. Used in caht for sending failed messages again or delete them") + String.localized( + "Chats.RetryOrDelete.Body", + comment: "Alert 'Retry Or Delete' body message. Used in caht for sending failed messages again or delete them" + ) } - + // MARK: Notifications static var copiedToPasteboardNotification: String { String.localized("Shared.CopiedToPasteboard", comment: "Shared alert notification: message about item copied to pasteboard.") @@ -55,9 +65,10 @@ extension String.adamant { static var noInternetNotificationBoby: String { String.localized("Shared.NoInternet.Body", comment: "Shared alert notification: body message for no internet connection.") } - static var noInternetTransferBody: String { String.localized("Shared.Transfer.NoInternet.Body", comment: "Shared alert notification: body message for no internet connection.") + static var noInternetTransferBody: String { + String.localized("Shared.Transfer.NoInternet.Body", comment: "Shared alert notification: body message for no internet connection.") } - + static var emailErrorMessageTitle: String { String.localized("Error.Mail.Title", comment: "Error messge title for support email") } @@ -65,10 +76,14 @@ extension String.adamant { String.localized("Error.Mail.Body", comment: "Error messge body for support email") } static var emailErrorMessageBodyWithDescription: String { - String.localized("Error.Mail.Body.Detailed", comment: "Error messge body for support email, with detailed error description. Where first %@ - error's short message, second %@ - detailed description, third %@ - deviceInfo") + String.localized( + "Error.Mail.Body.Detailed", + comment: + "Error messge body for support email, with detailed error description. Where first %@ - error's short message, second %@ - detailed description, third %@ - deviceInfo" + ) } } - + enum reply { static var shortUnknownMessageError: String { String.localized("Reply.ShortUnknownMessageError", comment: "Short unknown message error") @@ -83,7 +98,7 @@ extension String.adamant { String.localized("Reply.pendingMessageError", comment: "Pending message reply error") } } - + enum partnerQR { static var includePartnerName: String { String.localized("PartnerQR.includePartnerName", comment: "Include partner name") diff --git a/Adamant/Helpers/SwipePanGestureRecognizer.swift b/Adamant/Helpers/SwipePanGestureRecognizer.swift index 718cd0aaf..42c796db4 100644 --- a/Adamant/Helpers/SwipePanGestureRecognizer.swift +++ b/Adamant/Helpers/SwipePanGestureRecognizer.swift @@ -14,19 +14,19 @@ final class SwipePanGestureRecognizer: UIPanGestureRecognizer, UIGestureRecogniz super.init(target: target, action: action) delegate = self } - + func gestureRecognizerShouldBegin( _ gestureRecognizer: UIGestureRecognizer ) -> Bool { guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else { return false } - + let velocity = panGesture.velocity(in: self.view) let isHorizontal = abs(velocity.x) > abs(velocity.y) return isHorizontal } - + func gestureRecognizer( _ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer diff --git a/Adamant/Helpers/TaskManager.swift b/Adamant/Helpers/TaskManager.swift index 2c60d56e9..0bfa637ad 100644 --- a/Adamant/Helpers/TaskManager.swift +++ b/Adamant/Helpers/TaskManager.swift @@ -10,15 +10,15 @@ import Foundation final class TaskManager { private var tasks = Set>() - + func insert(_ task: Task<(), Never>) { tasks.insert(task) } - + func clean() { tasks.forEach { $0.cancel() } } - + deinit { clean() } diff --git a/Adamant/Helpers/UIFont+adamant.swift b/Adamant/Helpers/UIFont+adamant.swift index 547051478..614b84133 100644 --- a/Adamant/Helpers/UIFont+adamant.swift +++ b/Adamant/Helpers/UIFont+adamant.swift @@ -10,36 +10,36 @@ import UIKit extension UIFont { static func adamantPrimary(ofSize size: CGFloat) -> UIFont { - return UIFont(name: "Exo 2", size: size) ?? .systemFont(ofSize: size) + return UIFont(name: "Exo 2", size: size) ?? .systemFont(ofSize: size) } - + static func adamantMono(ofSize size: CGFloat, weight: UIFont.Weight) -> UIFont { return .monospacedSystemFont(ofSize: size, weight: weight) } - + static func adamantPrimary(ofSize size: CGFloat, weight: UIFont.Weight) -> UIFont { let name: String - + switch weight { case UIFont.Weight.bold: name = "Exo 2 Bold" - + case UIFont.Weight.medium: name = "Exo 2 Medium" - + case UIFont.Weight.thin: name = "Exo 2 Thin" - + case UIFont.Weight.light: name = "Exo 2 Light" - + default: name = "Exo 2" } - + return UIFont(name: name, size: size) ?? .systemFont(ofSize: size, weight: weight) } - + static let adamantChatFileRawDefault = UIFont.systemFont(ofSize: 8) static let adamantChatDefault = UIFont.systemFont(ofSize: 17) static let adamantCodeDefault = UIFont.adamantMono(ofSize: 15, weight: .regular) diff --git a/Adamant/Helpers/UITableView+Adamant.swift b/Adamant/Helpers/UITableView+Adamant.swift index b386d06c8..712f993fa 100644 --- a/Adamant/Helpers/UITableView+Adamant.swift +++ b/Adamant/Helpers/UITableView+Adamant.swift @@ -13,11 +13,13 @@ extension UITableView { top: .zero, left: 15, bottom: .zero, - right: .zero) - + right: .zero + ) + static let defaultTransactionsSeparatorInset = UIEdgeInsets( top: .zero, left: 80, bottom: .zero, - right: .zero) + right: .zero + ) } diff --git a/Adamant/Helpers/UITextField+adamant.swift b/Adamant/Helpers/UITextField+adamant.swift index 5c67d49e6..6ec1bd2b1 100644 --- a/Adamant/Helpers/UITextField+adamant.swift +++ b/Adamant/Helpers/UITextField+adamant.swift @@ -6,20 +6,20 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit import CommonKit +import UIKit extension UITextField { private final class UITextField_AssociatedKeys: @unchecked Sendable { var clearButtonTint = "uitextfield_clearButtonTint" var originalImage = "uitextfield_originalImage" - + private init() {} - + static let shared: UITextField_AssociatedKeys = .init() } - + private var originalImage: UIImage? { get { if let cl = objc_getAssociatedObject(self, &UITextField_AssociatedKeys.shared.originalImage) as? Wrapper { @@ -31,7 +31,7 @@ extension UITextField { objc_setAssociatedObject(self, &UITextField_AssociatedKeys.shared.originalImage, Wrapper(newValue), .OBJC_ASSOCIATION_RETAIN) } } - + var clearButtonTint: UIColor? { get { if let cl = objc_getAssociatedObject(self, &UITextField_AssociatedKeys.shared.clearButtonTint) as? Wrapper { @@ -45,11 +45,11 @@ extension UITextField { applyClearButtonTint() } } - + private static let runOnce: Void = { Swizzle.for(UITextField.self, selector: #selector(UITextField.layoutSubviews), with: #selector(UITextField.uitextfield_layoutSubviews)) }() - + private func applyClearButtonTint() { if let button = UIView.find(of: UIButton.self, in: self), let color = clearButtonTint { if originalImage == nil { @@ -58,12 +58,12 @@ extension UITextField { button.setImage(originalImage?.tinted(with: color), for: .normal) } } - + @objc func uitextfield_layoutSubviews() { uitextfield_layoutSubviews() applyClearButtonTint() } - + func setPopupKeyboardType(_ type: UIKeyboardType) { guard !isMacOS else { return } keyboardType = type @@ -72,14 +72,14 @@ extension UITextField { class Wrapper { var underlying: T? - + init(_ underlying: T?) { self.underlying = underlying } } extension UIView { - + static func find(of type: T.Type, in view: UIView, includeSubviews: Bool = true) -> T? where T: UIView { if view.isKind(of: T.self) { return view as? T @@ -93,51 +93,53 @@ extension UIView { } return nil } - + } extension UIImage { - + func tinted(with color: UIColor) -> UIImage? { UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale) color.set() self.withRenderingMode(.alwaysTemplate).draw(in: CGRect(origin: CGPoint(x: 0, y: 0), size: self.size)) - + let result = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() - + return result } - + } class Swizzle { - + class func `for`(_ className: AnyClass, selector originalSelector: Selector, with newSelector: Selector) { if let method: Method = class_getInstanceMethod(className, originalSelector), - let swizzledMethod: Method = class_getInstanceMethod(className, newSelector) { - if (class_addMethod(className, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) { + let swizzledMethod: Method = class_getInstanceMethod(className, newSelector) + { + if class_addMethod(className, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)) { class_replaceMethod(className, newSelector, method_getImplementation(method), method_getTypeEncoding(method)) } else { method_exchangeImplementations(method, swizzledMethod) } } } - + } // MARK: Set line break extension UITextField { func setLineBreakMode() { - guard let oldStyle = self.defaultTextAttributes[ - .paragraphStyle, - default: NSParagraphStyle() - ] as? NSParagraphStyle, - let style = oldStyle.mutableCopy() as? NSMutableParagraphStyle + guard + let oldStyle = self.defaultTextAttributes[ + .paragraphStyle, + default: NSParagraphStyle() + ] as? NSParagraphStyle, + let style = oldStyle.mutableCopy() as? NSMutableParagraphStyle else { return } - + style.lineBreakMode = .byTruncatingMiddle self.defaultTextAttributes[.paragraphStyle] = style } @@ -154,7 +156,7 @@ extension UITextField { func enablePasswordToggle() { let button = makePasswordButton() - + let containerView = UIView() containerView.addSubview(button) button.snp.makeConstraints { make in @@ -163,11 +165,11 @@ extension UITextField { containerView.snp.makeConstraints { make in make.size.equalTo(UITextField.buttonContainerHeight) } - + rightView = containerView rightViewMode = .always } - + func makePasswordButton() -> UIButton { let button = UIButton(type: .custom) button.imageEdgeInsets = UITextField.buttonImageEdgeInsets @@ -180,7 +182,7 @@ extension UITextField { let imageName = isSecureTextEntry ? "eye_close" : "eye_open" button.setImage(.asset(named: imageName), for: .normal) } - + @objc private func togglePasswordView(_ sender: UIButton) { isSecureTextEntry.toggle() updatePasswordToggleImage(sender) diff --git a/Adamant/Helpers/UIViewController+email.swift b/Adamant/Helpers/UIViewController+email.swift index 5e0970b8e..bb7f23e80 100644 --- a/Adamant/Helpers/UIViewController+email.swift +++ b/Adamant/Helpers/UIViewController+email.swift @@ -6,9 +6,9 @@ // Copyright © 2022 Adamant. All rights reserved. // +import CommonKit import MessageUI import UIKit -import CommonKit extension UIViewController { func openEmailScreen( @@ -25,8 +25,8 @@ extension UIViewController { } } -private extension UIViewController { - func showEmailVC( +extension UIViewController { + fileprivate func showEmailVC( recipient: String, subject: String?, body: String?, @@ -34,12 +34,12 @@ private extension UIViewController { ) { let mailVC = MFMailComposeViewController() subject.map { mailVC.setSubject($0) } - + if let body = body { let html = body.replacingOccurrences(of: "\n", with: "
") mailVC.setMessageBody(html, isHTML: true) } - + mailVC.mailComposeDelegate = delegate mailVC.setToRecipients([recipient]) mailVC.modalPresentationStyle = .overFullScreen diff --git a/Adamant/Helpers/UserDefaultsManager.swift b/Adamant/Helpers/UserDefaultsManager.swift new file mode 100644 index 000000000..812af786d --- /dev/null +++ b/Adamant/Helpers/UserDefaultsManager.swift @@ -0,0 +1,17 @@ +// +// UserDefaultsManager.swift +// Adamant +// +// Created by Sergei Veretennikov on 22.03.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import CommonKit + +struct UserDefaultsManager { + @UserDefaultsStorage(.needsToShowNoActiveNodesAlert) static var needsToShowNoActiveNodesAlert: Bool? + + static func setInitialUserDefaults() { + needsToShowNoActiveNodesAlert = true + } +} diff --git a/Adamant/Helpers/Web3Swift+Adamant.swift b/Adamant/Helpers/Web3Swift+Adamant.swift index 1f8892965..89739577c 100644 --- a/Adamant/Helpers/Web3Swift+Adamant.swift +++ b/Adamant/Helpers/Web3Swift+Adamant.swift @@ -6,8 +6,8 @@ // Copyright © 2023 Adamant. All rights reserved. // -import web3swift import Web3Core +import web3swift // MARK: Web3Swift // Make requests more comfortable @@ -17,7 +17,7 @@ extension IEth { let request = APIRequest.getTransactionByHash(txHash) return try await APIRequest.sendRequest(with: provider, for: request).result } - + func transactionReceipt(_ txHash: String) async throws -> Web3Core.TransactionReceipt { let request = APIRequest.getTransactionReceipt(txHash) return try await APIRequest.sendRequest(with: provider, for: request).result diff --git a/Adamant/Models/ANSPayload.swift b/Adamant/Models/ANSPayload.swift index e38f0e359..c1d9507b1 100644 --- a/Adamant/Models/ANSPayload.swift +++ b/Adamant/Models/ANSPayload.swift @@ -17,7 +17,7 @@ extension ANSPayload { case apns case apnsSandbox = "apns-sandbox" } - + enum Action: String, Codable { case add case remove diff --git a/Adamant/Models/AdamantMessage.swift b/Adamant/Models/AdamantMessage.swift index dd616d499..d6a3af9a6 100644 --- a/Adamant/Models/AdamantMessage.swift +++ b/Adamant/Models/AdamantMessage.swift @@ -6,8 +6,8 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation /// Adamant message types /// @@ -22,17 +22,17 @@ enum AdamantMessage: Sendable { // MARK: - Fee extension AdamantMessage { static private let textFee = Decimal(sign: .plus, exponent: -3, significand: 1) - + var fee: Decimal { switch self { case .text(let message), .markdownText(let message): return AdamantMessage.feeFor(text: message) - + case .richMessage(let payload): return AdamantMessage.feeFor(text: payload.serialized()) } } - + private static func feeFor(text: String) -> Decimal { return Decimal(ceil(Double(text.count) / 255.0)) * AdamantMessage.textFee } @@ -44,7 +44,7 @@ extension AdamantMessage { switch self { case .text, .markdownText: return .message - + case .richMessage: return .richMessage } diff --git a/Adamant/Models/BTCRawTransaction.swift b/Adamant/Models/BTCRawTransaction.swift index 22fa94df3..c6afaf70a 100644 --- a/Adamant/Models/BTCRawTransaction.swift +++ b/Adamant/Models/BTCRawTransaction.swift @@ -12,120 +12,124 @@ import Foundation struct BTCRawTransaction { let txId: String let date: Date? - + let valueIn: Decimal let valueOut: Decimal let fee: Decimal - + let confirmations: Int? let blockHash: String? - + let inputs: [BTCInput] let outputs: [BTCOutput] let isDoubleSpend: Bool - - func asBtcTransaction(_ as:T.Type, for address: String, blockId: String? = nil) -> T { + + func asBtcTransaction(_ as: T.Type, for address: String, blockId: String? = nil) -> T { // MARK: Known values let confirmationsValue: String? let transactionStatus: TransactionStatus - + if let confirmations = confirmations { confirmationsValue = String(confirmations) transactionStatus = confirmations > 0 ? .success : .pending } else { confirmationsValue = nil - transactionStatus = .notInitiated + transactionStatus = .registered } - + // Transfers var myInputs = inputs.filter { $0.sender == address } var myOutputs = outputs.filter { $0.addresses.contains(address) } - + var totalInputsValue = myInputs.map { $0.value }.reduce(0, +) - fee var totalOutputsValue = myOutputs.map { $0.value }.reduce(0, +) - + if totalInputsValue == totalOutputsValue { totalInputsValue = 0 totalOutputsValue = 0 } - + if totalInputsValue > totalOutputsValue { while let out = myOutputs.first { totalInputsValue -= out.value totalOutputsValue -= out.value - + myOutputs.removeFirst() } } - + if totalInputsValue < totalOutputsValue { while let i = myInputs.first { totalInputsValue -= i.value totalOutputsValue -= i.value - + myInputs.removeFirst() } } - + let senders = Set(inputs.map { $0.sender }) let recipients = Set(outputs.compactMap { $0.addresses.first }) - + let sender: String let recipient: String - + if senders.count == 1 { sender = senders.first! } else { let filtered = senders.filter { $0 != address } - + if filtered.count == 1 { sender = filtered.first! } else { sender = String.adamant.dogeTransaction.senders(senders.count) } } - + if recipients.count == 1 { recipient = recipients.first! } else { let filtered = recipients.filter { $0 != address } - + if filtered.count == 1 { recipient = filtered.first! } else { recipient = String.adamant.dogeTransaction.recipients(recipients.count) } } - + // MARK: Inputs if myInputs.count > 0 { - let inputTransaction = T(txId: txId, - dateValue: date, - blockValue: blockId, - senderAddress: address, - recipientAddress: recipient, - amountValue: totalInputsValue, - feeValue: fee, - confirmationsValue: confirmationsValue, - isOutgoing: true, - transactionStatus: transactionStatus) - + let inputTransaction = T( + txId: txId, + dateValue: date, + blockValue: blockId, + senderAddress: address, + recipientAddress: recipient, + amountValue: totalInputsValue, + feeValue: fee, + confirmationsValue: confirmationsValue, + isOutgoing: true, + transactionStatus: transactionStatus + ) + return inputTransaction } - + // MARK: Outputs - let outputTransaction = T(txId: txId, - dateValue: date, - blockValue: blockId, - senderAddress: sender, - recipientAddress: address, - amountValue: totalOutputsValue, - feeValue: fee, - confirmationsValue: confirmationsValue, - isOutgoing: false, - transactionStatus: transactionStatus) - + let outputTransaction = T( + txId: txId, + dateValue: date, + blockValue: blockId, + senderAddress: sender, + recipientAddress: address, + amountValue: totalOutputsValue, + feeValue: fee, + confirmationsValue: confirmationsValue, + isOutgoing: false, + transactionStatus: transactionStatus + ) + return outputTransaction } } @@ -144,22 +148,22 @@ extension BTCRawTransaction: Decodable { case inputs = "vin" case outputs = "vout" } - + init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - + // MARK: Required txId = try container.decode(String.self, forKey: .txId) - + let possibleDoubleSpend = (try? container.decode(Bool.self, forKey: .possibleDoubleSpend)) ?? false - + // MARK: Optionals for new transactions if let timeInterval = try? container.decode(TimeInterval.self, forKey: .date) { date = Date(timeIntervalSince1970: timeInterval) } else { date = nil } - + guard !possibleDoubleSpend else { isDoubleSpend = true valueIn = 0 @@ -171,17 +175,18 @@ extension BTCRawTransaction: Decodable { outputs = [] return } - + confirmations = try? container.decode(Int.self, forKey: .confirmations) blockHash = try? container.decode(String.self, forKey: .blockHash) - + // MARK: Inputs & Outputs let rawInputs = try container.decode([BTCInput].self, forKey: .inputs) inputs = rawInputs.filter { !$0.sender.isEmpty } // Filter incomplete transactions without sender outputs = try container.decode([BTCOutput].self, forKey: .outputs) - + if let rawValueIn = try? container.decode(Decimal.self, forKey: .valueIn), - let rawValueOut = try? container.decode(Decimal.self, forKey: .valueOut) { + let rawValueOut = try? container.decode(Decimal.self, forKey: .valueOut) + { valueIn = rawValueIn valueOut = rawValueOut } else { @@ -198,13 +203,13 @@ extension BTCRawTransaction: Decodable { valueOut = outputs.map { $0.value }.reduce(0, +) } } - + if let raw = try? container.decode(Decimal.self, forKey: .fee) { fee = raw } else { fee = valueIn - valueOut } - + isDoubleSpend = false } } @@ -218,15 +223,15 @@ struct BTCInput: Decodable { case txId = "txid" case vOut = "vout" } - + let sender: String let value: Decimal let txId: String let vOut: Int - + init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - + // Incomplete inputs doesn't contains address. We will filter them out if let raw = try? container.decode(String.self, forKey: .sender) { self.sender = raw @@ -236,10 +241,10 @@ struct BTCInput: Decodable { } else { self.sender = "" } - + self.txId = try container.decode(String.self, forKey: .txId) self.vOut = try container.decode(Int.self, forKey: .vOut) - + if let raw = try? container.decode(Decimal.self, forKey: .value) { self.value = Decimal(sign: .plus, exponent: DogeWalletService.currencyExponent, significand: raw) } else { @@ -256,22 +261,22 @@ struct BTCOutput: Decodable { case spentTxId case spentIndex } - + enum SignatureCodingKeys: String, CodingKey { case addresses } - + let addresses: [String] var value: Decimal let spentTxId: String? let spentIndex: Int? - + init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - + let signatureContainer = try container.nestedContainer(keyedBy: SignatureCodingKeys.self, forKey: .signature) self.addresses = try signatureContainer.decode([String].self, forKey: .addresses) - + if let raw = try? container.decode(String.self, forKey: .value), let value = Decimal(string: raw) { self.value = value } else if let raw = try? container.decode(Decimal.self, forKey: .value) { @@ -279,13 +284,13 @@ struct BTCOutput: Decodable { } else { self.value = 0 } - + if let raw = try? container.decode(String.self, forKey: .valueSat), let value = Decimal(string: raw) { self.value = Decimal(sign: .plus, exponent: DogeWalletService.currencyExponent, significand: value) } else if let raw = try? container.decode(Decimal.self, forKey: .valueSat) { self.value = Decimal(sign: .plus, exponent: DogeWalletService.currencyExponent, significand: raw) } - + self.spentTxId = try? container.decode(String.self, forKey: .spentTxId) self.spentIndex = try? container.decode(Int.self, forKey: .spentIndex) } diff --git a/Adamant/Models/BaseBtcTransaction.swift b/Adamant/Models/BaseBtcTransaction.swift index a87783866..f06606a24 100644 --- a/Adamant/Models/BaseBtcTransaction.swift +++ b/Adamant/Models/BaseBtcTransaction.swift @@ -10,28 +10,39 @@ import Foundation class BaseBtcTransaction: TransactionDetails, @unchecked Sendable { var defaultCurrencySymbol: String? { return "" } - + let txId: String let dateValue: Date? let blockValue: String? - + let senderAddress: String let recipientAddress: String - + let amountValue: Decimal? let feeValue: Decimal? let confirmationsValue: String? - + let isOutgoing: Bool let transactionStatus: TransactionStatus? - + var blockHeight: UInt64? - + var nonceRaw: String? { nil } - - required init(txId: String, dateValue: Date?, blockValue: String?, senderAddress: String, recipientAddress: String, amountValue: Decimal, feeValue: Decimal?, confirmationsValue: String?, isOutgoing: Bool, transactionStatus: TransactionStatus?) { + + required init( + txId: String, + dateValue: Date?, + blockValue: String?, + senderAddress: String, + recipientAddress: String, + amountValue: Decimal, + feeValue: Decimal?, + confirmationsValue: String?, + isOutgoing: Bool, + transactionStatus: TransactionStatus? + ) { self.txId = txId self.dateValue = dateValue self.blockValue = blockValue diff --git a/Adamant/Models/CoreData/BaseAccount+CoreDataClass.swift b/Adamant/Models/CoreData/BaseAccount+CoreDataClass.swift index 39bd390b4..7070543ee 100644 --- a/Adamant/Models/CoreData/BaseAccount+CoreDataClass.swift +++ b/Adamant/Models/CoreData/BaseAccount+CoreDataClass.swift @@ -7,8 +7,8 @@ // // -import Foundation import CoreData +import Foundation @objc(BaseAccount) public class BaseAccount: NSManagedObject, @unchecked Sendable { diff --git a/Adamant/Models/CoreData/BaseAccount+CoreDataProperties.swift b/Adamant/Models/CoreData/BaseAccount+CoreDataProperties.swift index 349ead275..ac77f7383 100644 --- a/Adamant/Models/CoreData/BaseAccount+CoreDataProperties.swift +++ b/Adamant/Models/CoreData/BaseAccount+CoreDataProperties.swift @@ -7,8 +7,8 @@ // // -import Foundation import CoreData +import Foundation extension BaseAccount { diff --git a/Adamant/Models/CoreData/BaseTransaction+CoreDataClass.swift b/Adamant/Models/CoreData/BaseTransaction+CoreDataClass.swift index 13cc62c13..4b102064b 100644 --- a/Adamant/Models/CoreData/BaseTransaction+CoreDataClass.swift +++ b/Adamant/Models/CoreData/BaseTransaction+CoreDataClass.swift @@ -7,8 +7,8 @@ // // -import Foundation import CoreData +import Foundation @objc(BaseTransaction) public class BaseTransaction: CoinTransaction, @unchecked Sendable { diff --git a/Adamant/Models/CoreData/BaseTransaction+CoreDataProperties.swift b/Adamant/Models/CoreData/BaseTransaction+CoreDataProperties.swift index c2c2344b9..64d5921ae 100644 --- a/Adamant/Models/CoreData/BaseTransaction+CoreDataProperties.swift +++ b/Adamant/Models/CoreData/BaseTransaction+CoreDataProperties.swift @@ -7,8 +7,8 @@ // // -import Foundation import CoreData +import Foundation extension BaseTransaction { diff --git a/Adamant/Models/CoreData/ChatTransaction+CoreDataClass.swift b/Adamant/Models/CoreData/ChatTransaction+CoreDataClass.swift index 400559c15..47d6384d2 100644 --- a/Adamant/Models/CoreData/ChatTransaction+CoreDataClass.swift +++ b/Adamant/Models/CoreData/ChatTransaction+CoreDataClass.swift @@ -7,8 +7,8 @@ // // -import Foundation import CoreData +import Foundation @objc(ChatTransaction) public class ChatTransaction: BaseTransaction, @unchecked Sendable { @@ -16,21 +16,21 @@ public class ChatTransaction: BaseTransaction, @unchecked Sendable { get { return MessageStatus(rawValue: self.status) ?? .failed } set { self.status = newValue.rawValue } } - + func serializedMessage() -> String? { fatalError("You must implement serializedMessage in ChatTransaction classes") } - + var sentDate: Date? { date.map { $0 as Date } } - + override var transactionStatus: TransactionStatus? { get { return confirmations > 0 - ? .success - : .pending + ? .success + : .pending } - set { } + set {} } } diff --git a/Adamant/Models/CoreData/ChatTransaction+CoreDataProperties.swift b/Adamant/Models/CoreData/ChatTransaction+CoreDataProperties.swift index accc3697f..794c9a08f 100644 --- a/Adamant/Models/CoreData/ChatTransaction+CoreDataProperties.swift +++ b/Adamant/Models/CoreData/ChatTransaction+CoreDataProperties.swift @@ -7,8 +7,8 @@ // // -import Foundation import CoreData +import Foundation extension ChatTransaction { @@ -25,5 +25,9 @@ extension ChatTransaction { @NSManaged public var chatroom: Chatroom? @NSManaged public var lastIn: Chatroom? @NSManaged public var isFake: Bool + @NSManaged public var richMessageTransactions: Set? + func addToRichMessageTransactions(_ transaction: RichMessageTransaction) { + self.mutableSetValue(forKey: "richMessageTransactions").add(transaction) + } } diff --git a/Adamant/Models/CoreData/Chatroom+CoreDataClass.swift b/Adamant/Models/CoreData/Chatroom+CoreDataClass.swift index 6512a40ab..329ed6891 100644 --- a/Adamant/Models/CoreData/Chatroom+CoreDataClass.swift +++ b/Adamant/Models/CoreData/Chatroom+CoreDataClass.swift @@ -7,43 +7,48 @@ // // -import Foundation import CoreData +import Foundation @objc(Chatroom) public class Chatroom: NSManagedObject, @unchecked Sendable { static let entityName = "Chatroom" - - var hasUnread: Bool { - return hasUnreadMessages || (lastTransaction?.isUnread ?? false) - } - + func markAsReaded() { hasUnreadMessages = false - + if let trs = transactions as? Set { trs.filter { $0.isUnread }.forEach { $0.isUnread = false } } - lastTransaction?.isUnread = false } - + + func markMessageAsReaded(chatMessageId: String, stack: CoreDataStack) { + guard let trs = transactions as? Set, + let message = trs.first(where: { $0.chatMessageId == chatMessageId }) + else { + return + } + message.isUnread = false + + message.richMessageTransactions?.forEach { $0.isUnread = false } + + if let context = message.managedObjectContext, context.hasChanges { + try? context.save() + } + + updateLastTransaction() + } + func markAsUnread() { hasUnreadMessages = true - lastTransaction?.isUnread = true - } - - func getFirstUnread() -> ChatTransaction? { - if let trs = transactions as? Set { - return trs.filter { $0.isUnread }.map { $0 }.first - } - return nil } - + @MainActor func getName(addressBookService: AddressBookService) -> String? { guard let partner = partner else { return nil } let result: String? if let address = partner.address, - let name = addressBookService.getName(for: address) { + let name = addressBookService.getName(for: address) + { result = name } else if let title = title { result = title @@ -52,43 +57,38 @@ public class Chatroom: NSManagedObject, @unchecked Sendable { } else { result = partner.address } - + return result?.checkAndReplaceSystemWallets() } - + @MainActor func hasPartnerName(addressBookService: AddressBookService) -> Bool { guard let partner = partner else { return false } - + return partner.address.flatMap { addressBookService.getName(for: $0) } != nil - || title != nil - || partner.name != nil + || title != nil + || partner.name != nil } - + private let semaphore = DispatchSemaphore(value: 1) - + func updateLastTransaction() { semaphore.wait() defer { semaphore.signal() } - + if let transactions = transactions?.filtered( using: NSPredicate(format: "isHidden == false") ) as? Set { if let newest = transactions.sorted(by: { (lhs: ChatTransaction, rhs: ChatTransaction) in - guard let l = lhs.date as Date? else { - return true - } - - guard let r = rhs.date as Date? else { - return false - } - + guard let l = lhs.date as Date? else { return true } + guard let r = rhs.date as Date? else { return false } + switch l.compare(r) { case .orderedAscending: return true - + case .orderedDescending: return false - + // Rare case of identical date, compare IDs case .orderedSame: return lhs.transactionId < rhs.transactionId @@ -102,6 +102,8 @@ public class Chatroom: NSManagedObject, @unchecked Sendable { lastTransaction = nil updatedAt = nil } + + hasUnreadMessages = transactions.contains { $0.isUnread } } } } diff --git a/Adamant/Models/CoreData/Chatroom+CoreDataProperties.swift b/Adamant/Models/CoreData/Chatroom+CoreDataProperties.swift index 66aec3ef5..00fae71f0 100644 --- a/Adamant/Models/CoreData/Chatroom+CoreDataProperties.swift +++ b/Adamant/Models/CoreData/Chatroom+CoreDataProperties.swift @@ -7,8 +7,8 @@ // // -import Foundation import CoreData +import Foundation // TODO: remove desynchronization between hasUnreadMessages and lastTransaction.isUnread extension Chatroom { diff --git a/Adamant/Models/CoreData/CoinTransaction+CoreDataClass.swift b/Adamant/Models/CoreData/CoinTransaction+CoreDataClass.swift index 2b60df6bb..1b1d57e51 100644 --- a/Adamant/Models/CoreData/CoinTransaction+CoreDataClass.swift +++ b/Adamant/Models/CoreData/CoinTransaction+CoreDataClass.swift @@ -7,13 +7,13 @@ // // -import Foundation import CoreData +import Foundation @objc(CoinTransaction) public class CoinTransaction: NSManagedObject, @unchecked Sendable { static let entityCoinName = "CoinTransaction" - + var transactionStatus: TransactionStatus? { get { let data = Data(transactionStatusRaw.utf8) @@ -21,12 +21,12 @@ public class CoinTransaction: NSManagedObject, @unchecked Sendable { } set { guard let data = try? JSONEncoder().encode(newValue), - let raw = String(data: data, encoding: .utf8) + let raw = String(data: data, encoding: .utf8) else { transactionStatusRaw = "" return } - + transactionStatusRaw = raw } } diff --git a/Adamant/Models/CoreData/CoinTransaction+CoreDataProperties.swift b/Adamant/Models/CoreData/CoinTransaction+CoreDataProperties.swift index 7fdc74185..cbd57b6ff 100644 --- a/Adamant/Models/CoreData/CoinTransaction+CoreDataProperties.swift +++ b/Adamant/Models/CoreData/CoinTransaction+CoreDataProperties.swift @@ -7,8 +7,8 @@ // // -import Foundation import CoreData +import Foundation extension CoinTransaction { @@ -31,8 +31,9 @@ extension CoinTransaction { @NSManaged public var blockchainType: String @NSManaged public var transactionStatusRaw: String @NSManaged public var nonceRaw: String? + @NSManaged public var timestampMs: Int64 } -extension CoinTransaction : Identifiable { +extension CoinTransaction: Identifiable { } diff --git a/Adamant/Models/CoreData/CoinTransaction+TransactionDetails.swift b/Adamant/Models/CoreData/CoinTransaction+TransactionDetails.swift index 7ae29eccd..60a5a475f 100644 --- a/Adamant/Models/CoreData/CoinTransaction+TransactionDetails.swift +++ b/Adamant/Models/CoreData/CoinTransaction+TransactionDetails.swift @@ -14,28 +14,32 @@ extension CoinTransaction: TransactionDetails { var senderAddress: String { senderId ?? "" } - + var recipientAddress: String { recipientId ?? "" } - + var dateValue: Date? { date as? Date } - + var amountValue: Decimal? { amount?.decimalValue } - + var feeValue: Decimal? { fee?.decimalValue } - + var confirmationsValue: String? { return isConfirmed ? String(confirmations) : nil } - + var blockValue: String? { return isConfirmed ? blockId : nil } - + var txId: String { return transactionId } - + var blockHeight: UInt64? { return nil } + + var timeIntervalMillisecondsSince1970: Int64 { + date?.timeIntervalMillisecondsSince1970 ?? .zero + } } diff --git a/Adamant/Models/CoreData/CoreDataAccount+CoreDataClass.swift b/Adamant/Models/CoreData/CoreDataAccount+CoreDataClass.swift index 06328ef0a..39316c82c 100644 --- a/Adamant/Models/CoreData/CoreDataAccount+CoreDataClass.swift +++ b/Adamant/Models/CoreData/CoreDataAccount+CoreDataClass.swift @@ -7,8 +7,8 @@ // // -import Foundation import CoreData +import Foundation @objc(CoreDataAccount) public class CoreDataAccount: BaseAccount, @unchecked Sendable { diff --git a/Adamant/Models/CoreData/CoreDataAccount+CoreDataProperties.swift b/Adamant/Models/CoreData/CoreDataAccount+CoreDataProperties.swift index 4a60b59cd..c8292f11f 100644 --- a/Adamant/Models/CoreData/CoreDataAccount+CoreDataProperties.swift +++ b/Adamant/Models/CoreData/CoreDataAccount+CoreDataProperties.swift @@ -7,8 +7,8 @@ // // -import Foundation import CoreData +import Foundation extension CoreDataAccount { diff --git a/Adamant/Models/CoreData/DummyAccount+CoreDataClass.swift b/Adamant/Models/CoreData/DummyAccount+CoreDataClass.swift index 6e972c430..ddacada6a 100644 --- a/Adamant/Models/CoreData/DummyAccount+CoreDataClass.swift +++ b/Adamant/Models/CoreData/DummyAccount+CoreDataClass.swift @@ -7,8 +7,8 @@ // // -import Foundation import CoreData +import Foundation @objc(DummyAccount) public class DummyAccount: BaseAccount { diff --git a/Adamant/Models/CoreData/DummyAccount+CoreDataProperties.swift b/Adamant/Models/CoreData/DummyAccount+CoreDataProperties.swift index 05887eb88..eab47f0ad 100644 --- a/Adamant/Models/CoreData/DummyAccount+CoreDataProperties.swift +++ b/Adamant/Models/CoreData/DummyAccount+CoreDataProperties.swift @@ -7,8 +7,8 @@ // // -import Foundation import CoreData +import Foundation extension DummyAccount { diff --git a/Adamant/Models/CoreData/MessageTransaction+CoreDataClass.swift b/Adamant/Models/CoreData/MessageTransaction+CoreDataClass.swift index 4d4978621..cd664df2c 100644 --- a/Adamant/Models/CoreData/MessageTransaction+CoreDataClass.swift +++ b/Adamant/Models/CoreData/MessageTransaction+CoreDataClass.swift @@ -7,13 +7,13 @@ // // -import Foundation import CoreData +import Foundation @objc(MessageTransaction) public class MessageTransaction: ChatTransaction { static let entityName = "MessageTransaction" - + override func serializedMessage() -> String? { return message } diff --git a/Adamant/Models/CoreData/MessageTransaction+CoreDataProperties.swift b/Adamant/Models/CoreData/MessageTransaction+CoreDataProperties.swift index a8e8504c1..e9dc28a88 100644 --- a/Adamant/Models/CoreData/MessageTransaction+CoreDataProperties.swift +++ b/Adamant/Models/CoreData/MessageTransaction+CoreDataProperties.swift @@ -7,8 +7,8 @@ // // -import Foundation import CoreData +import Foundation extension MessageTransaction { @@ -19,7 +19,7 @@ extension MessageTransaction { @NSManaged public var isMarkdown: Bool @NSManaged public var message: String? @NSManaged public var reactionsData: Data? - + var reactions: Set? { get { guard let data = reactionsData else { @@ -28,7 +28,7 @@ extension MessageTransaction { return try? PropertyListDecoder().decode(Set.self, from: data) } - + set { guard let value = newValue else { reactionsData = nil diff --git a/Adamant/Models/CoreData/RichMessageTransaction+CoreDataClass.swift b/Adamant/Models/CoreData/RichMessageTransaction+CoreDataClass.swift index e79519d58..ecda0f092 100644 --- a/Adamant/Models/CoreData/RichMessageTransaction+CoreDataClass.swift +++ b/Adamant/Models/CoreData/RichMessageTransaction+CoreDataClass.swift @@ -7,18 +7,18 @@ // // -import Foundation -import CoreData import CommonKit +import CoreData +import Foundation @objc(RichMessageTransaction) public class RichMessageTransaction: ChatTransaction, @unchecked Sendable { static let entityName = "RichMessageTransaction" - + override func serializedMessage() -> String? { return richContentSerialized } - + override var transactionStatus: TransactionStatus? { get { let data = Data(transactionStatusRaw.utf8) @@ -26,16 +26,16 @@ public class RichMessageTransaction: ChatTransaction, @unchecked Sendable { } set { guard let data = try? JSONEncoder().encode(newValue), - let raw = String(data: data, encoding: .utf8) + let raw = String(data: data, encoding: .utf8) else { transactionStatusRaw = "" return } - + transactionStatusRaw = raw } } - + var transfer: RichMessageTransfer? { guard let richContent = richContent else { return nil } return .init(content: richContent) diff --git a/Adamant/Models/CoreData/RichMessageTransaction+CoreDataProperties.swift b/Adamant/Models/CoreData/RichMessageTransaction+CoreDataProperties.swift index bcecc8ffb..d87d0ace6 100644 --- a/Adamant/Models/CoreData/RichMessageTransaction+CoreDataProperties.swift +++ b/Adamant/Models/CoreData/RichMessageTransaction+CoreDataProperties.swift @@ -7,9 +7,9 @@ // // -import Foundation -import CoreData import CommonKit +import CoreData +import Foundation extension RichMessageTransaction { @@ -23,44 +23,48 @@ extension RichMessageTransaction { @NSManaged public var richType: String? @NSManaged public var transferStatusRaw: NSNumber? @NSManaged public var additionalType: RichAdditionalType - + @NSManaged public var chatTransaction: ChatTransaction? + func isTransferReply() -> Bool { return richContent?[RichContentKeys.reply.replyMessage] is [String: String] } - + func isFileReply() -> Bool { let replyMessage = richContent?[RichContentKeys.reply.replyMessage] as? [String: Any] return replyMessage?[RichContentKeys.file.files] is [[String: Any]] } - + func getRichValue(for key: String) -> String? { if let value = richContent?[key] as? String { return value } - + if let content = richContent?[RichContentKeys.reply.replyMessage] as? [String: Any], - let value = content[key] as? String { + let value = content[key] as? String + { return value } - + return nil } - + func getRichValue(for key: String) -> T? { if let value = richContent?[key] as? T { return value } - + if let content = richContent?[RichContentKeys.file.files] as? [String: Any], - let value = content[key] as? T { + let value = content[key] as? T + { return value } - + if let content = richContent?[RichContentKeys.reply.replyMessage] as? [String: Any], - let value = content[key] as? T { + let value = content[key] as? T + { return value } - + return nil } } diff --git a/Adamant/Models/CoreData/TransferTransaction+CoreDataClass.swift b/Adamant/Models/CoreData/TransferTransaction+CoreDataClass.swift index 4e60183eb..35ee95506 100644 --- a/Adamant/Models/CoreData/TransferTransaction+CoreDataClass.swift +++ b/Adamant/Models/CoreData/TransferTransaction+CoreDataClass.swift @@ -7,8 +7,8 @@ // // -import Foundation import CoreData +import Foundation @objc(TransferTransaction) public class TransferTransaction: ChatTransaction, @unchecked Sendable { diff --git a/Adamant/Models/CoreData/TransferTransaction+CoreDataProperties.swift b/Adamant/Models/CoreData/TransferTransaction+CoreDataProperties.swift index bbc9cfc2f..c8005367b 100644 --- a/Adamant/Models/CoreData/TransferTransaction+CoreDataProperties.swift +++ b/Adamant/Models/CoreData/TransferTransaction+CoreDataProperties.swift @@ -7,8 +7,8 @@ // // -import Foundation import CoreData +import Foundation extension TransferTransaction { @@ -20,7 +20,7 @@ extension TransferTransaction { @NSManaged public var replyToId: String? @NSManaged public var decodedReplyMessage: String? @NSManaged public var reactionsData: Data? - + var reactions: Set? { get { guard let data = reactionsData else { @@ -29,7 +29,7 @@ extension TransferTransaction { return try? PropertyListDecoder().decode(Set.self, from: data) } - + set { guard let value = newValue else { reactionsData = nil @@ -46,18 +46,18 @@ extension TransferTransaction: AdamantTransactionDetails { var partnerName: String? { partner?.name } - + var showToChat: Bool? { guard let partner = partner as? CoreDataAccount, - let chatroom = partner.chatroom, - !chatroom.isReadonly + let chatroom = partner.chatroom, + !chatroom.isReadonly else { return false } - + return true } - + var chatRoom: Chatroom? { let partner = partner as? CoreDataAccount return partner?.chatroom diff --git a/Adamant/Models/DashTransaction.swift b/Adamant/Models/DashTransaction.swift index d0ec40287..61f55102f 100644 --- a/Adamant/Models/DashTransaction.swift +++ b/Adamant/Models/DashTransaction.swift @@ -6,8 +6,8 @@ // Copyright © 2019 Adamant. All rights reserved. // -import Foundation import BitcoinKit +import Foundation final class DashTransaction: BaseBtcTransaction { override var defaultCurrencySymbol: String? { DashWalletService.currencySymbol } @@ -17,16 +17,16 @@ struct BtcBlock: Decodable { let hash: String let height: Int64 let time: Int64 - + enum CodingKeys: String, CodingKey { case hash case height case time } - + init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - + self.hash = try container.decode(String.self, forKey: .hash) self.height = try container.decode(Int64.self, forKey: .height) self.time = try container.decode(Int64.self, forKey: .time) @@ -40,7 +40,7 @@ struct DashUnspentTransaction: Decodable { let script: String let amount: UInt64 let height: UInt64 - + enum CodingKeys: String, CodingKey { case address case txid @@ -49,10 +49,10 @@ struct DashUnspentTransaction: Decodable { case amount = "satoshis" case height } - + func asUnspentTransaction(lockScript: Data) -> UnspentTransaction { let txHash = Data(hex: txid).map { Data($0.reversed()) } ?? Data() - + let unspentOutput = TransactionOutput(value: amount, lockingScript: lockScript) let unspentOutpoint = TransactionOutPoint(hash: txHash, index: outputIndex) let utxo = UnspentTransaction(output: unspentOutput, outpoint: unspentOutpoint) diff --git a/Adamant/Models/DogeTransaction.swift b/Adamant/Models/DogeTransaction.swift index dbe88fbe6..f23d46d86 100644 --- a/Adamant/Models/DogeTransaction.swift +++ b/Adamant/Models/DogeTransaction.swift @@ -6,19 +6,25 @@ // Copyright © 2019 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation extension String.adamant { struct dogeTransaction { static func recipients(_ recipients: Int) -> String { - return String.localizedStringWithFormat(.localized("Doge.TransactionDetails.RecipientsFormat", comment: "DogeTransaction: amount of recipients, if more than one."), recipients) + return String.localizedStringWithFormat( + .localized("Doge.TransactionDetails.RecipientsFormat", comment: "DogeTransaction: amount of recipients, if more than one."), + recipients + ) } - + static func senders(_ senders: Int) -> String { - return String.localizedStringWithFormat(.localized("Doge.TransactionDetails.SendersFormat", comment: "DogeTransaction: amount of senders, if more than one."), senders) + return String.localizedStringWithFormat( + .localized("Doge.TransactionDetails.SendersFormat", comment: "DogeTransaction: amount of senders, if more than one."), + senders + ) } - + private init() {} } } @@ -47,7 +53,7 @@ final class DogeTransaction: BaseBtcTransaction { "fees": 1, "firstSeenTs": 1554298214 } - + new transaction: { "txid": "60cd612335c9797ea67689b9cde4a41e20c20c1b96eb0731c59c5b0eab8bad31", @@ -60,11 +66,11 @@ new transaction: "valueIn": 284, "fees": 1 } - + */ /* Inputs - + { "txid": "3f4fa05bef67b1aacc0392fd5c3be3f94c991394166bc12ca73df28b63fe0aab", "vout": 0, diff --git a/Adamant/Models/DownloadPolicy.swift b/Adamant/Models/DownloadPolicy.swift index 0348f0eda..59dc4524e 100644 --- a/Adamant/Models/DownloadPolicy.swift +++ b/Adamant/Models/DownloadPolicy.swift @@ -6,29 +6,29 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation extension Notification.Name { - struct Storage { - public static let storageClear = Notification.Name("adamant.storage.clear") - public static let storageProprietiesUpdated = Notification.Name("adamant.storage.ProprietiesUpdated") - } - } + struct Storage { + public static let storageClear = Notification.Name("adamant.storage.clear") + public static let storageProprietiesUpdated = Notification.Name("adamant.storage.ProprietiesUpdated") + } +} - enum DownloadPolicy: String { - case everybody - case nobody - case contacts +enum DownloadPolicy: String { + case everybody + case nobody + case contacts - var title: String { - switch self { - case .everybody: - return .localized("Storage.DownloadPolicy.Everybody.Title") - case .nobody: - return .localized("Storage.DownloadPolicy.Nobody.Title") - case .contacts: - return .localized("Storage.DownloadPolicy.Contacts.Title") - } - } - } + var title: String { + switch self { + case .everybody: + return .localized("Storage.DownloadPolicy.Everybody.Title") + case .nobody: + return .localized("Storage.DownloadPolicy.Nobody.Title") + case .contacts: + return .localized("Storage.DownloadPolicy.Contacts.Title") + } + } +} diff --git a/Adamant/Models/EthAccount.swift b/Adamant/Models/EthAccount.swift index 691fdcfe5..a6aec79fe 100644 --- a/Adamant/Models/EthAccount.swift +++ b/Adamant/Models/EthAccount.swift @@ -7,9 +7,10 @@ // import Foundation +import Web3Core import web3swift + import struct BigInt.BigUInt -import Web3Core struct EthAccount { let wallet: BIP32Keystore diff --git a/Adamant/Models/EthTransaction.swift b/Adamant/Models/EthTransaction.swift index 96822c960..b544b3634 100644 --- a/Adamant/Models/EthTransaction.swift +++ b/Adamant/Models/EthTransaction.swift @@ -6,11 +6,12 @@ // Copyright © 2018 Adamant. All rights reserved. // +import CommonKit import Foundation +import Web3Core import web3swift + @preconcurrency import struct BigInt.BigUInt -import Web3Core -import CommonKit struct EthResponse { let status: Int @@ -25,16 +26,16 @@ extension EthResponse: Decodable { case message case result } - + init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - + if let raw = try? container.decode(String.self, forKey: .status), let status = Int(raw) { self.status = status } else { self.status = 0 } - + message = (try? container.decode(String.self, forKey: .message)) ?? "" result = (try? container.decode([EthTransaction].self, forKey: .result)) ?? [] } @@ -55,7 +56,7 @@ struct EthTransaction: @unchecked Sendable { let receiptStatus: TransactionReceipt.TXStatus let blockNumber: String? let currencySymbol: String - + var nonce: Int? var isOutgoing: Bool = false } @@ -75,17 +76,17 @@ extension EthTransaction: Decodable { case receiptStatus = "txreceipt_status" case blockNumber } - + init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - + hash = try container.decode(String.self, forKey: .hash) from = try container.decode(String.self, forKey: .from) to = try container.decode(String.self, forKey: .to) blockNumber = try? container.decode(String.self, forKey: .blockNumber) confirmations = try? container.decode(String.self, forKey: .confirmations) currencySymbol = EthWalletService.currencySymbol - + // Status if let statusRaw = try? container.decode(String.self, forKey: .receiptStatus) { if statusRaw == "1" { @@ -96,35 +97,35 @@ extension EthTransaction: Decodable { } else { self.receiptStatus = .notYetProcessed } - + // Date if let timeStampRaw = try? container.decode(String.self, forKey: .timeStamp), let timeStamp = Double(timeStampRaw) { self.date = Date(timeIntervalSince1970: timeStamp) } else { self.date = nil } - + // IsError if let isErrorRaw = try? container.decode(String.self, forKey: .isError) { self.isError = isErrorRaw == "1" } else { self.isError = false } - + // Value/amount if let raw = try? container.decode(String.self, forKey: .value), let value = Decimal(string: raw) { self.value = Decimal(sign: .plus, exponent: EthWalletService.currencyExponent, significand: value) } else { self.value = 0 } - + // Gas used if let raw = try? container.decode(String.self, forKey: .gasUsed), let gas = Decimal(string: raw) { self.gasUsed = gas } else { self.gasUsed = 0 } - + // Gas price if let raw = try? container.decode(String.self, forKey: .gasPrice), let gasPrice = Decimal(string: raw) { self.gasPrice = Decimal(sign: .plus, exponent: EthWalletService.currencyExponent, significand: gasPrice) @@ -137,7 +138,7 @@ extension EthTransaction: Decodable { // MARK: - TransactionDetails extension EthTransaction: TransactionDetails { var defaultCurrencySymbol: String? { return currencySymbol } - + var txId: String { return hash } var senderAddress: String { return from } var recipientAddress: String { return to } @@ -146,26 +147,26 @@ extension EthTransaction: TransactionDetails { var confirmationsValue: String? { return confirmations } var blockValue: String? { return blockNumber } var feeCurrencySymbol: String? { EthWalletService.currencySymbol } - + var feeValue: Decimal? { guard let gasUsed = gasUsed else { return nil } - + return gasPrice * gasUsed } - + var transactionStatus: TransactionStatus? { return receiptStatus.asTransactionStatus() } - + var blockHeight: UInt64? { return nil } - + var nonceRaw: String? { guard let nonce = nonce else { return nil } - + return String(nonce) } } @@ -183,19 +184,19 @@ extension CodableTransaction { hash: String? = nil, for token: ERC20Token? = nil ) -> EthTransaction { - + var recipient = to var txValue: BigUInt? = value - + var exponent = EthWalletService.currencyExponent if let naturalUnits = token?.naturalUnits { exponent = -1 * naturalUnits } - + if data.count > 0 { - let addressRaw = Data(data[16 ..< 36]).toHexString() - let erc20RawValue = Data(data[37 ..< 68]).toHexString() - + let addressRaw = Data(data[16..<36]).toHexString() + let erc20RawValue = Data(data[37..<68]).toHexString() + if let address = EthereumAddress("0x\(addressRaw)"), let v = BigUInt(erc20RawValue, radix: 16) { recipient = address txValue = v @@ -205,16 +206,16 @@ extension CodableTransaction { if receiptStatus == .notYetProcessed { txValue = nil } - + let feePrice: BigUInt if type == .eip1559 { feePrice = (maxFeePerGas ?? BigUInt(0)) + (maxPriorityFeePerGas ?? BigUInt(0)) } else { feePrice = gasPrice ?? BigUInt(0) } - + let gasPrice = gasPrice ?? feePrice - + return EthTransaction( date: date, hash: hash ?? txHash ?? "", @@ -256,7 +257,7 @@ extension CodableTransaction { "gasUsed":"21000", "confirmations":"32316" } - + */ // MARK: - Adamant ETH API transactions @@ -270,10 +271,10 @@ struct EthTransactionShort: @unchecked Sendable { let gasPrice: Decimal let value: Decimal let blockNumber: String - + let contract_to: String let contract_value: BigUInt - + func asEthTransaction(isOutgoing: Bool) -> EthTransaction { return EthTransaction( date: date, @@ -291,10 +292,10 @@ struct EthTransactionShort: @unchecked Sendable { isOutgoing: isOutgoing ) } - + func asERCTransaction(isOutgoing: Bool, token: ERC20Token) -> EthTransaction { let exponent = -1 * token.naturalUnits - + return EthTransaction( date: date, hash: hash, @@ -326,41 +327,41 @@ extension EthTransactionShort: Decodable { case contract_to case contract_value } - + init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - + from = try container.decode(String.self, forKey: .txfrom) to = try container.decode(String.self, forKey: .txto) - + // Hash let hashRaw = try container.decode(String.self, forKey: .txhash) hash = hashRaw.replacingOccurrences(of: "\\", with: "0") - + // Block let blockRaw = try container.decode(UInt64.self, forKey: .block) blockNumber = String(blockRaw) - + // Date let timestamp = try container.decode(TimeInterval.self, forKey: .time) date = Date(timeIntervalSince1970: timestamp) - + // Gas used gasUsed = try container.decode(Decimal.self, forKey: .gas) - + // Gas price let gasPriceRaw = try container.decode(Decimal.self, forKey: .gasprice) gasPrice = Decimal(sign: .plus, exponent: EthWalletService.currencyExponent, significand: gasPriceRaw) - + // Value let valueRaw = try container.decode(Decimal.self, forKey: .value) value = Decimal(sign: .plus, exponent: EthWalletService.currencyExponent, significand: valueRaw) - + contract_to = try container.decodeIfPresent(String.self, forKey: .contract_to) ?? "" - + let contractValueRaw = try container.decodeIfPresent(String.self, forKey: .contract_value) ?? "0" contract_value = BigUInt(contractValueRaw, radix: 16) ?? BigUInt.zero - + if !contract_to.isEmpty { let address = "0x" + contract_to.reversed()[..<40].reversed() to = address @@ -370,7 +371,7 @@ extension EthTransactionShort: Decodable { // MARK: Adamant node Sample JSON /* - + { "time": 1540676411, "txfrom": "0xcE25C5bbEB9f27ac942f914183279FDB31C999dC", @@ -383,8 +384,8 @@ extension EthTransactionShort: Decodable { "contract_to": "", "contract_value": "" } - + Note broken txhash contract_to & contract_value not requested from API - + */ diff --git a/Adamant/Models/IPFSNodeStatus.swift b/Adamant/Models/IPFSNodeStatus.swift index 2a1ed9813..009fcd14d 100644 --- a/Adamant/Models/IPFSNodeStatus.swift +++ b/Adamant/Models/IPFSNodeStatus.swift @@ -8,7 +8,8 @@ import Foundation -struct IPFSNodeStatus: Codable { +struct IPFSNodeStatus: Decodable { + let timestamp: UInt64 let version: String } diff --git a/Adamant/Models/LskAccount.swift b/Adamant/Models/LskAccount.swift index b73f8e8ee..e8ebf940f 100644 --- a/Adamant/Models/LskAccount.swift +++ b/Adamant/Models/LskAccount.swift @@ -6,8 +6,8 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation import BigInt +import Foundation import LiskKit struct LskAccount { diff --git a/Adamant/Models/NodeVersion.swift b/Adamant/Models/NodeVersion.swift index c2a32231b..618d4ca56 100644 --- a/Adamant/Models/NodeVersion.swift +++ b/Adamant/Models/NodeVersion.swift @@ -6,8 +6,8 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation struct NodeVersion: Codable { let success: Bool @@ -15,7 +15,7 @@ struct NodeVersion: Codable { let commit: String let version: String let nodeTimestamp: TimeInterval - + var nodeDate: Date { return AdamantUtilities.decodeAdamant(timestamp: nodeTimestamp) } diff --git a/Adamant/Models/ServerResponses/DogeGetTransactionsResponse.swift b/Adamant/Models/ServerResponses/DogeGetTransactionsResponse.swift index 30520a854..d759d16ac 100644 --- a/Adamant/Models/ServerResponses/DogeGetTransactionsResponse.swift +++ b/Adamant/Models/ServerResponses/DogeGetTransactionsResponse.swift @@ -12,7 +12,7 @@ final class DogeGetTransactionsResponse: Decodable, Sendable { let totalItems: Int let from: Int let to: Int - + let items: [BTCRawTransaction] } @@ -24,5 +24,5 @@ final class DogeGetTransactionsResponse: Decodable, Sendable { "to": 1, "items": [] } - + */ diff --git a/Adamant/Models/ServerResponses/ServerResponseWithTimestamp.swift b/Adamant/Models/ServerResponses/ServerResponseWithTimestamp.swift index 1665cf8b9..c1a1fba28 100644 --- a/Adamant/Models/ServerResponses/ServerResponseWithTimestamp.swift +++ b/Adamant/Models/ServerResponses/ServerResponseWithTimestamp.swift @@ -6,8 +6,8 @@ // Copyright © 2019 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation protocol ServerResponseWithTimestamp { var nodeTimestamp: TimeInterval { get } diff --git a/Adamant/Models/SimpleTransactionDetails.swift b/Adamant/Models/SimpleTransactionDetails.swift index 1d12ac044..33ea46aa0 100644 --- a/Adamant/Models/SimpleTransactionDetails.swift +++ b/Adamant/Models/SimpleTransactionDetails.swift @@ -10,38 +10,38 @@ import Foundation struct SimpleTransactionDetails: AdamantTransactionDetails, Hashable { var defaultCurrencySymbol: String? - + var txId: String - + var senderAddress: String - + var recipientAddress: String - + var dateValue: Date? - + var amountValue: Decimal? - + var feeValue: Decimal? - + var confirmationsValue: String? - + var blockValue: String? - + var isOutgoing: Bool - + var transactionStatus: TransactionStatus? - + var blockHeight: UInt64? { return nil } - + var partnerName: String? var comment: String? var showToChat: Bool? var chatRoom: Chatroom? - + var nonceRaw: String? - + init( defaultCurrencySymbol: String? = nil, txId: String, @@ -71,7 +71,7 @@ struct SimpleTransactionDetails: AdamantTransactionDetails, Hashable { self.partnerName = partnerName self.nonceRaw = nonceRaw } - + init(_ transaction: TransactionDetails) { self.defaultCurrencySymbol = transaction.defaultCurrencySymbol self.txId = transaction.txId @@ -86,7 +86,7 @@ struct SimpleTransactionDetails: AdamantTransactionDetails, Hashable { self.transactionStatus = transaction.transactionStatus self.nonceRaw = transaction.nonceRaw } - + init(_ transaction: TransferTransaction) { self.defaultCurrencySymbol = transaction.defaultCurrencySymbol self.txId = transaction.txId diff --git a/Adamant/Models/TransactionStatus.swift b/Adamant/Models/TransactionStatus.swift index 85d709cfb..f5fe5735e 100644 --- a/Adamant/Models/TransactionStatus.swift +++ b/Adamant/Models/TransactionStatus.swift @@ -19,7 +19,7 @@ enum InconsistentReason: Codable, Hashable { case recipientCryptoAddressMismatch(String) case senderCryptoAddressUnavailable(String) case recipientCryptoAddressUnavailable(String) - + var localized: String { switch self { case .time: @@ -33,13 +33,25 @@ enum InconsistentReason: Codable, Hashable { case .wrongAmount: return .localized("TransactionStatus.Inconsistent.WrongAmount", comment: "Transaction status: inconsistent wrong amount") case .senderCryptoAddressMismatch(let coin): - return String.localizedStringWithFormat(.localized("TransactionStatus.Inconsistent.SenderCryptoAddressMismatch", comment: "Transaction status: inconsistent wrong mismatch"), coin) + return String.localizedStringWithFormat( + .localized("TransactionStatus.Inconsistent.SenderCryptoAddressMismatch", comment: "Transaction status: inconsistent wrong mismatch"), + coin + ) case .recipientCryptoAddressMismatch(let coin): - return String.localizedStringWithFormat(.localized("TransactionStatus.Inconsistent.RecipientCryptoAddressMismatch", comment: "Transaction status: inconsistent wrong mismatch"), coin) + return String.localizedStringWithFormat( + .localized("TransactionStatus.Inconsistent.RecipientCryptoAddressMismatch", comment: "Transaction status: inconsistent wrong mismatch"), + coin + ) case .senderCryptoAddressUnavailable(let coin): - return String.localizedStringWithFormat(.localized("TransactionStatus.Inconsistent.SenderCryptoAddressUnavailable", comment: "Transaction status: inconsistent unable to retrieve"), coin) + return String.localizedStringWithFormat( + .localized("TransactionStatus.Inconsistent.SenderCryptoAddressUnavailable", comment: "Transaction status: inconsistent unable to retrieve"), + coin + ) case .recipientCryptoAddressUnavailable(let coin): - return String.localizedStringWithFormat(.localized("TransactionStatus.Inconsistent.RecipientCryptoAddressUnavailable", comment: "Transaction status: inconsistent unable to retrieve"), coin) + return String.localizedStringWithFormat( + .localized("TransactionStatus.Inconsistent.RecipientCryptoAddressUnavailable", comment: "Transaction status: inconsistent unable to retrieve"), + coin + ) } } } @@ -51,7 +63,7 @@ enum TransactionStatus: Codable, Equatable, Hashable { case failed case registered case inconsistent(InconsistentReason) - + var localized: String { switch self { case .notInitiated: @@ -73,7 +85,7 @@ extension TransactionStatus { if case .inconsistent = self { return true } - + return false } } diff --git a/Adamant/Modules/Account/AccountFactory.swift b/Adamant/Modules/Account/AccountFactory.swift index 2c8638580..b23d5211c 100644 --- a/Adamant/Modules/Account/AccountFactory.swift +++ b/Adamant/Modules/Account/AccountFactory.swift @@ -12,10 +12,10 @@ import UIKit @MainActor struct AccountFactory { let assembler: Assembler - + func makeViewController(screensFactory: ScreensFactory) -> UIViewController { AccountViewController( - visibleWalletsService: assembler.resolve(VisibleWalletsService.self)!, + walletStoreServiceProvider: assembler.resolve(WalletStoreServiceProviderProtocol.self)!, accountService: assembler.resolve(AccountService.self)!, dialogService: assembler.resolve(DialogService.self)!, screensFactory: screensFactory, @@ -26,7 +26,8 @@ struct AccountFactory { currencyInfoService: assembler.resolve(InfoServiceProtocol.self)!, languageService: assembler.resolve(LanguageStorageProtocol.self)!, walletServiceCompose: assembler.resolve(WalletServiceCompose.self)!, - apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)! + apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)!, + visibleWalletsService: assembler.resolve(VisibleWalletsService.self)! ) } } diff --git a/Adamant/Modules/Account/AccountFooterView.swift b/Adamant/Modules/Account/AccountFooterView.swift index c8d803f63..f35a5c00a 100644 --- a/Adamant/Modules/Account/AccountFooterView.swift +++ b/Adamant/Modules/Account/AccountFooterView.swift @@ -7,24 +7,24 @@ // import Foundation -import UIKit import SnapKit +import UIKit final class AccountFooterView: UIView { private let footerImageview = UIImageView(image: UIImage.asset(named: "avatar_bots")) - + override init(frame: CGRect) { super.init(frame: frame) setupView() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + private func setupView() { addSubview(footerImageview) - + footerImageview.snp.makeConstraints { make in make.size.equalTo(50) make.top.centerX.equalToSuperview() diff --git a/Adamant/Modules/Account/AccountHeaderView.swift b/Adamant/Modules/Account/AccountHeaderView.swift index b52c2ee9b..b41afe2ad 100644 --- a/Adamant/Modules/Account/AccountHeaderView.swift +++ b/Adamant/Modules/Account/AccountHeaderView.swift @@ -14,14 +14,14 @@ protocol AccountHeaderViewDelegate: AnyObject { } final class AccountHeaderView: UIView { - + // MARK: - IBOutlets @IBOutlet weak var avatarImageView: UIImageView! @IBOutlet weak var addressButton: UIButton! @IBOutlet weak var walletViewContainer: UIView! - + weak var delegate: AccountHeaderViewDelegate? - + @IBAction func addressButtonTapped(_ sender: UIButton) { delegate?.addressLabelTapped(from: sender) } diff --git a/Adamant/Modules/Account/AccountViewController/AccountViewController+Form.swift b/Adamant/Modules/Account/AccountViewController/AccountViewController+Form.swift index 869e318cd..3c1ee13cd 100644 --- a/Adamant/Modules/Account/AccountViewController/AccountViewController+Form.swift +++ b/Adamant/Modules/Account/AccountViewController/AccountViewController+Form.swift @@ -12,7 +12,7 @@ extension AccountViewController { // MARK: - Rows & Sections enum Sections { case wallet, application, delegates, actions, security - + var tag: String { switch self { case .wallet: return "wllt" @@ -22,10 +22,10 @@ extension AccountViewController { case .security: return "scrty" } } - + var localized: String { switch self { - case .wallet: return "Wallet" // Depends on selected wallet + case .wallet: return "Wallet" // Depends on selected wallet case .application: return .localized("AccountTab.Section.Application", comment: "Account tab: Application section title") case .actions: return .localized("AccountTab.Section.Actions", comment: "Account tab: Actions section title") case .delegates: return .localized("AccountTab.Section.Delegates", comment: "Account tab: Delegates section title") @@ -33,13 +33,13 @@ extension AccountViewController { } } } - + enum Rows { - case balance, sendTokens // Wallet - case security, nodes, coinsNodes, theme, currency, language, about, visibleWallets, contribute, storage // Application - case voteForDelegates, generateQr, generatePk, logout // Actions - case stayIn, biometry, notifications // Security - + case balance, sendTokens // Wallet + case security, nodes, coinsNodes, theme, currency, language, about, visibleWallets, contribute, storage // Application + case voteForDelegates, generateQr, generatePk, logout // Actions + case stayIn, biometry, notifications // Security + var tag: String { switch self { case .balance: return "blnc" @@ -63,7 +63,7 @@ extension AccountViewController { case .storage: return "storage" } } - + var localized: String { switch self { case .balance: return .localized("AccountTab.Row.Balance", comment: "Account tab: Balance row title") @@ -87,7 +87,7 @@ extension AccountViewController { case .storage: return .localized("StorageUsage.Title", comment: "Storage Usage: Title") } } - + var image: UIImage? { var image: UIImage? switch self { @@ -104,18 +104,18 @@ extension AccountViewController { case .generateQr: image = .asset(named: "row_QR.png") case .generatePk: image = .asset(named: "privateKey_row") case .stayIn: image = .asset(named: "row_security") - case .biometry: image = nil // Determined by localAuth service + case .biometry: image = nil // Determined by localAuth service case .notifications: image = .asset(named: "row_Notifications.png") case .visibleWallets: image = .asset(named: "row_balance") case .contribute: image = .asset(named: "row_contribute") case .language: image = .asset(named: "row_language") case .storage: image = .asset(named: "row_storage") } - + return image? .imageResized(to: .init(squareSize: 24)) .withTintColor(.adamant.tableRowIcons) } } - + } diff --git a/Adamant/Modules/Account/AccountViewController/AccountViewController+StayIn.swift b/Adamant/Modules/Account/AccountViewController/AccountViewController+StayIn.swift index 2f170945c..c0b002751 100644 --- a/Adamant/Modules/Account/AccountViewController/AccountViewController+StayIn.swift +++ b/Adamant/Modules/Account/AccountViewController/AccountViewController+StayIn.swift @@ -6,18 +6,18 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation +import CommonKit import Eureka +import Foundation import MyLittlePinpad -import CommonKit extension AccountViewController { func setStayLoggedIn(enabled: Bool) { guard accountService.hasStayInAccount != enabled else { return } - - if enabled { // Create pin and turn on Stay In + + if enabled { // Create pin and turn on Stay In pinpadRequest = .createPin let pinpad = PinpadViewController.adamantPinpad(biometryButton: .hidden) pinpad.commentLabel.text = String.adamant.pinpad.createPin @@ -27,7 +27,7 @@ extension AccountViewController { pinpad.backgroundView.backgroundColor = UIColor.adamant.backgroundColor setColors(for: pinpad) present(pinpad, animated: true, completion: nil) - } else { // Validate pin and turn off Stay In + } else { // Validate pin and turn off Stay In pinpadRequest = .turnOffPin let biometryButton: PinpadBiometryButtonType = accountService.useBiometry ? localAuth.biometryType.pinpadButtonType : .hidden let pinpad = PinpadViewController.adamantPinpad(biometryButton: biometryButton) @@ -39,64 +39,62 @@ extension AccountViewController { present(pinpad, animated: true, completion: nil) } } - + // MARK: Use biometry func setBiometry(enabled: Bool) { guard showLoggedInOptions, accountService.hasStayInAccount, accountService.useBiometry != enabled else { return } - - let reason = enabled ? String.adamant.security.biometryOnReason : String.adamant.security.biometryOffReason - localAuth.authorizeUser(reason: reason) { result in - Task { @MainActor [weak self] in - switch result { - case .success: - self?.dialogService.showSuccess(withMessage: String.adamant.alert.done) - self?.accountService.updateUseBiometry(enabled) - - case .cancel: - if let row: SwitchRow = self?.form.rowBy(tag: Rows.biometry.tag) { - row.value = self?.accountService.useBiometry - row.updateCell() - } - - case .fallback: - let pinpad = PinpadViewController.adamantPinpad(biometryButton: .hidden) - - if enabled { - pinpad.commentLabel.text = String.adamant.security.biometryOnReason - self?.pinpadRequest = .turnOnBiometry - } else { - pinpad.commentLabel.text = String.adamant.security.biometryOffReason - self?.pinpadRequest = .turnOffBiometry - } - - pinpad.commentLabel.isHidden = false - pinpad.delegate = self - pinpad.modalPresentationStyle = .overFullScreen - self?.setColors(for: pinpad) - self?.present(pinpad, animated: true, completion: nil) - - case .failed: - if let row: SwitchRow = self?.form.rowBy(tag: Rows.biometry.tag) { - if let value = self?.accountService.useBiometry { - row.value = value - } else { - row.value = false - } - - row.updateCell() - row.evaluateHidden() - } - - if let row = self?.form.rowBy(tag: Rows.notifications.tag) { - row.evaluateHidden() - } + + Task { @MainActor [weak self] in + guard let self else { return } + let reason = enabled ? String.adamant.security.biometryOnReason : String.adamant.security.biometryOffReason + let result = await self.localAuth.authorizeUser(reason: reason) + + switch result { + case .success: + self.dialogService.showSuccess(withMessage: String.adamant.alert.done) + self.accountService.updateUseBiometry(enabled) + + case .cancel: + if let row: SwitchRow = self.form.rowBy(tag: Rows.biometry.tag) { + row.value = self.accountService.useBiometry + row.updateCell() + } + + case .fallback: + let pinpad = PinpadViewController.adamantPinpad(biometryButton: .hidden) + + if enabled { + pinpad.commentLabel.text = String.adamant.security.biometryOnReason + self.pinpadRequest = .turnOnBiometry + } else { + pinpad.commentLabel.text = String.adamant.security.biometryOffReason + self.pinpadRequest = .turnOffBiometry + } + + pinpad.commentLabel.isHidden = false + pinpad.delegate = self + pinpad.modalPresentationStyle = .overFullScreen + self.setColors(for: pinpad) + self.present(pinpad, animated: true, completion: nil) + + case .failed: + if let row: SwitchRow = self.form.rowBy(tag: Rows.biometry.tag) { + row.value = self.accountService.useBiometry + row.updateCell() + row.evaluateHidden() + } + + if let row = self.form.rowBy(tag: Rows.notifications.tag) { + row.evaluateHidden() } + case .biometryLockout: + return } } } - + func setColors(for pinpad: PinpadViewController) { pinpad.backgroundView.backgroundColor = UIColor.adamant.backgroundColor pinpad.buttonsBackgroundColor = UIColor.adamant.backgroundColor @@ -116,14 +114,14 @@ extension AccountViewController: PinpadViewControllerDelegate { nonisolated func pinpad(_ pinpad: PinpadViewController, didEnterPin pin: String) { MainActor.assumeIsolatedSafe { switch pinpadRequest { - + // MARK: User has entered new pin first time. Request re-enter pin case .createPin?: pinpadRequest = .reenterPin(pin: pin) pinpad.commentLabel.text = String.adamant.pinpad.reenterPin pinpad.clearPin() return - + // MARK: User has reentered pin. Save pin. case .reenterPin(let pinToVerify)?: guard pin == pinToVerify else { @@ -131,30 +129,29 @@ extension AccountViewController: PinpadViewControllerDelegate { pinpad.clearPin() break } - - accountService.setStayLoggedIn(pin: pin) { [weak self] result in - Task { @MainActor in - switch result { - case .success: - self?.pinpadRequest = nil - if let row: SwitchRow = self?.form.rowBy(tag: Rows.biometry.tag) { - row.value = false - row.updateCell() - row.evaluateHidden() - } - - if let row = self?.form.rowBy(tag: Rows.notifications.tag) { - row.evaluateHidden() - } - - pinpad.dismiss(animated: true, completion: nil) - - case .failure(let error): - self?.dialogService.showRichError(error: error) + + let result = accountService.setStayLoggedIn(pin: pin) + Task { @MainActor in + switch result { + case .success: + self.pinpadRequest = nil + if let row: SwitchRow = self.form.rowBy(tag: Rows.biometry.tag) { + row.value = false + row.updateCell() + row.evaluateHidden() } + + if let row = self.form.rowBy(tag: Rows.notifications.tag) { + row.evaluateHidden() + } + + pinpad.dismiss(animated: true, completion: nil) + + case .failure(let error): + self.dialogService.showRichError(error: error) } } - + // MARK: Users want to turn off the pin. Validate and turn off. case .turnOffPin?: guard accountService.validatePin(pin) else { @@ -162,11 +159,11 @@ extension AccountViewController: PinpadViewControllerDelegate { pinpad.clearPin() break } - + accountService.dropSavedAccount() - + pinpad.dismiss(animated: true, completion: nil) - + // MARK: User wants to turn on biometry case .turnOnBiometry?: guard accountService.validatePin(pin) else { @@ -174,10 +171,10 @@ extension AccountViewController: PinpadViewControllerDelegate { pinpad.clearPin() break } - + accountService.updateUseBiometry(true) pinpad.dismiss(animated: true, completion: nil) - + // MARK: User wants to turn off biometry case .turnOffBiometry?: guard accountService.validatePin(pin) else { @@ -185,89 +182,85 @@ extension AccountViewController: PinpadViewControllerDelegate { pinpad.clearPin() break } - + accountService.updateUseBiometry(false) pinpad.dismiss(animated: true, completion: nil) - + default: pinpad.dismiss(animated: true, completion: nil) } } } - - nonisolated func pinpadDidTapBiometryButton(_ pinpad: PinpadViewController) { - MainActor.assumeIsolatedSafe { + + func pinpadDidTapBiometryButton(_ pinpad: PinpadViewController) { + Task { @MainActor in switch pinpadRequest { - // MARK: User wants to turn of StayIn with his face. Or finger. case .turnOffPin?: - localAuth.authorizeUser(reason: String.adamant.security.stayInTurnOff, completion: { [weak self] result in - switch result { - case .success: - self?.accountService.dropSavedAccount() - - DispatchQueue.main.async { - if let row: SwitchRow = self?.form.rowBy(tag: Rows.biometry.tag) { - row.value = false - row.updateCell() - row.evaluateHidden() - } - - if let row = self?.form.rowBy(tag: Rows.notifications.tag) { - row.evaluateHidden() - } - - pinpad.dismiss(animated: true, completion: nil) - } - - case .cancel: break - case .fallback: break - case .failed: break + let result = await localAuth.authorizeUser(reason: String.adamant.security.stayInTurnOff) + switch result { + case .success: + self.accountService.dropSavedAccount() + + if let row: SwitchRow = self.form.rowBy(tag: Rows.biometry.tag) { + row.value = false + row.updateCell() + row.evaluateHidden() } - }) - + + if let row = self.form.rowBy(tag: Rows.notifications.tag) { + row.evaluateHidden() + } + + pinpad.dismiss(animated: true, completion: nil) + + case .cancel: break + case .fallback: break + case .failed: break + case .biometryLockout: break + } default: return } } } - - nonisolated func pinpadDidCancel(_ pinpad: PinpadViewController) { + + func pinpadDidCancel(_ pinpad: PinpadViewController) { MainActor.assumeIsolatedSafe { switch pinpadRequest { - + // MARK: User canceled turning on StayIn case .createPin?, .reenterPin(pin: _)?: if let row: SwitchRow = form.rowBy(tag: Rows.stayIn.tag) { row.value = false row.updateCell() } - + // MARK: User canceled turning off StayIn case .turnOffPin?: if let row: SwitchRow = form.rowBy(tag: Rows.stayIn.tag) { row.value = true row.updateCell() } - + // MARK: User canceled Biometry On case .turnOnBiometry?: if let row: SwitchRow = form.rowBy(tag: Rows.biometry.tag) { row.value = false row.updateCell() } - + // MARK: User canceled Biometry Off case .turnOffBiometry?: if let row: SwitchRow = form.rowBy(tag: Rows.biometry.tag) { row.value = true row.updateCell() } - + default: break } - + pinpadRequest = nil pinpad.dismiss(animated: true, completion: nil) } diff --git a/Adamant/Modules/Account/AccountViewController/AccountViewController.swift b/Adamant/Modules/Account/AccountViewController/AccountViewController.swift index 3ec7ad710..e6b7a2ae4 100644 --- a/Adamant/Modules/Account/AccountViewController/AccountViewController.swift +++ b/Adamant/Modules/Account/AccountViewController/AccountViewController.swift @@ -6,14 +6,14 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit +import Combine +import CommonKit +@preconcurrency import CoreData import Eureka import FreakingSimpleRoundImageView -@preconcurrency import CoreData @preconcurrency import Parchment import SnapKit -import CommonKit -import Combine +import UIKit // MARK: - Localization extension String.adamant { @@ -21,92 +21,89 @@ extension String.adamant { static var title: String { String.localized("AccountTab.Title", comment: "Account page: scene title") } - + static let updatingBalance = "…" } } extension String.adamant.alert { - static var logoutMessageFormat: String { String.localized("AccountTab.ConfirmLogout.MessageFormat", comment: "Account tab: Confirm logout alert") + static var logoutMessageFormat: String { + String.localized("AccountTab.ConfirmLogout.MessageFormat", comment: "Account tab: Confirm logout alert") } - static var logoutButton: String { String.localized("AccountTab.ConfirmLogout.Logout", comment: "Account tab: Confirm logout alert: Logout (Ok) button") + static var logoutButton: String { + String.localized("AccountTab.ConfirmLogout.Logout", comment: "Account tab: Confirm logout alert: Logout (Ok) button") } } // MARK: AccountViewController final class AccountViewController: FormViewController { // MARK: - Dependencies - - private let visibleWalletsService: VisibleWalletsService + + private let walletStoreServiceProvider: WalletStoreServiceProviderProtocol private let screensFactory: ScreensFactory private let notificationsService: NotificationsService private let transfersProvider: TransfersProvider private let avatarService: AvatarService private let currencyInfoService: InfoServiceProtocol private let languageService: LanguageStorageProtocol - private let walletServiceCompose: WalletServiceCompose private let apiServiceCompose: ApiServiceComposeProtocol - + private let visibleWalletsService: VisibleWalletsService + private lazy var viewModel: AccountWalletsViewModel = .init(walletsStoreService: walletStoreServiceProvider) + let accountService: AccountService let dialogService: DialogService let localAuth: LocalAuthentication - + // MARK: - Properties - + let walletCellIdentifier = "wllt" private(set) var accountHeaderView: AccountHeaderView! - + private var transfersController: NSFetchedResultsController? private var pagingViewController: PagingViewController! - - private var walletViewControllers: [WalletViewController] = [] { - didSet { - makeWalletModels() - } - } - + private var notificationsSet: Set = [] - + // MARK: StayIn - + var showLoggedInOptions: Bool { return accountService.hasStayInAccount } - + var showBiometryOptions: Bool { switch localAuth.biometryType { case .none: return false - + case .touchID, .faceID: return showLoggedInOptions } } - + var pinpadRequest: SecurityViewController.PinpadRequest? - + private lazy var refreshControl: UIRefreshControl = { let refreshControl = UIRefreshControl() refreshControl.tintColor = .adamant.primary refreshControl.addTarget(self, action: #selector(self.handleRefresh(_:)), for: UIControl.Event.valueChanged) return refreshControl }() - - private var walletModels: [String: WalletItemModel] = [:] - - private var currentWalletIndex: Int = .zero - private var currentSelectedWalletItem: WalletItemModel? { - walletModels.first{ - $1.model.index == currentWalletIndex - }?.value + + private var walletViewControllers: [WalletViewController] = [] + + private var currentWalletCoinID: String = "" + private var currentSelectedWalletItem: WalletCollectionViewCell.Model? { + viewModel.state.wallets.first { wallet in + wallet.coinID == currentWalletCoinID + } } - + private var initiated = false - + // MARK: - Init - + init( - visibleWalletsService: VisibleWalletsService, + walletStoreServiceProvider: WalletStoreServiceProviderProtocol, accountService: AccountService, dialogService: DialogService, screensFactory: ScreensFactory, @@ -117,9 +114,10 @@ final class AccountViewController: FormViewController { currencyInfoService: InfoServiceProtocol, languageService: LanguageStorageProtocol, walletServiceCompose: WalletServiceCompose, - apiServiceCompose: ApiServiceComposeProtocol + apiServiceCompose: ApiServiceComposeProtocol, + visibleWalletsService: VisibleWalletsService ) { - self.visibleWalletsService = visibleWalletsService + self.walletStoreServiceProvider = walletStoreServiceProvider self.accountService = accountService self.dialogService = dialogService self.screensFactory = screensFactory @@ -129,120 +127,101 @@ final class AccountViewController: FormViewController { self.avatarService = avatarService self.currencyInfoService = currencyInfoService self.languageService = languageService - self.walletServiceCompose = walletServiceCompose self.apiServiceCompose = apiServiceCompose - + self.visibleWalletsService = visibleWalletsService + super.init(nibName: nil, bundle: nil) } - + required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: - Lifecycle - + override func viewDidLoad() { super.viewDidLoad() - + navigationOptions = .Disabled navigationController?.setNavigationBarHidden(true, animated: false) navigationController?.navigationBar.prefersLargeTitles = true navigationItem.largeTitleDisplayMode = .never - + // MARK: Status Bar let statusBarView = UIView(frame: UIApplication.shared.statusBarFrame) statusBarView.backgroundColor = UIColor.adamant.backgroundColor view.addSubview(statusBarView) - + // MARK: Transfers controller Task { let controller = await transfersProvider.unreadTransfersController() controller.delegate = self transfersController = controller - + do { try controller.performFetch() } catch { dialogService.showError(withMessage: "Error fetching transfers: report a bug", supportEmail: true, error: error) } } - + // MARK: Header&Footer guard let header = UINib(nibName: "AccountHeader", bundle: nil).instantiate(withOwner: nil, options: nil).first as? AccountHeaderView else { fatalError("Can't load AccountHeaderView") } - + accountHeaderView = header accountHeaderView.delegate = self - + updateAccountInfo() - + tableView.tableHeaderView = header - + tableView.refreshControl = self.refreshControl - + let footerView = AccountFooterView(frame: CGRect(x: .zero, y: .zero, width: self.view.frame.width, height: 100)) tableView.tableFooterView = footerView - + // MARK: Wallet pages setupWalletsVC() - + pagingViewController = PagingViewController() - pagingViewController.register(UINib(nibName: "WalletCollectionViewCell", bundle: nil), for: WalletItemModel.self) + pagingViewController.register(UINib(nibName: "WalletCollectionViewCell", bundle: nil), for: WalletCollectionViewCell.Model.self) pagingViewController.menuItemSize = .fixed(width: 110, height: 110) pagingViewController.indicatorColor = UIColor.adamant.primary pagingViewController.indicatorOptions = .visible(height: 2, zIndex: Int.max, spacing: UIEdgeInsets.zero, insets: UIEdgeInsets.zero) pagingViewController.dataSource = self pagingViewController.delegate = self - if walletViewControllers.count > 0 { - pagingViewController.select(index: currentWalletIndex) - } - + selectCurrentWallet() + accountHeaderView.walletViewContainer.addSubview(pagingViewController.view) pagingViewController.view.snp.makeConstraints { $0.directionalEdges.equalToSuperview() } - + addChild(pagingViewController) - + updatePagingItemHeight() - + pagingViewController.borderColor = UIColor.clear - - let callback: @MainActor (Notification) -> Void = { [weak self] data in - guard let account = data.userInfo?[AdamantUserInfoKey.WalletService.wallet] as? WalletAccount - else { - return + + viewModel.$state + .removeDuplicates() + .debounce(for: .nanoseconds(500_000_000), scheduler: DispatchQueue.main) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + self.pagingViewController.reloadMenu() } - - var model = self?.walletModels[account.unicId]?.model - model?.balance = account.balance - model?.isBalanceInitialized = account.isBalanceInitialized - model?.notifications = account.notifications - - self?.walletModels[account.unicId]?.model = model ?? .default - } - - for walletService in walletServiceCompose.getWallets() { - NotificationCenter.default.addObserver( - forName: walletService.core.walletUpdatedNotification, - object: nil, - queue: OperationQueue.main, - using: { notification in - MainActor.assumeIsolatedSafe { - callback(notification) - } - } - ) - } - + .store(in: ¬ificationsSet) + // MARK: Rows&Sections - + // MARK: Application let appSection = Section(Sections.application.localized) { $0.tag = Sections.application.tag } - + // Visible wallets let visibleWalletsRow = LabelRow { $0.tag = Rows.visibleWallets.tag @@ -255,9 +234,9 @@ final class AccountViewController: FormViewController { }.onCellSelection { [weak self] (_, _) in guard let self = self else { return } let vc = screensFactory.makeVisibleWallets() - + if let split = splitViewController { - let details = UINavigationController(rootViewController:vc) + let details = UINavigationController(rootViewController: vc) details.definesPresentationContext = true split.showDetailViewController(details, sender: self) } else if let nav = navigationController { @@ -266,12 +245,12 @@ final class AccountViewController: FormViewController { vc.modalPresentationStyle = .overFullScreen present(vc, animated: true, completion: nil) } - + deselectWalletViewControllers() } - + appSection.append(visibleWalletsRow) - + // Node list let nodesRow = LabelRow { $0.title = Rows.nodes.localized @@ -284,9 +263,9 @@ final class AccountViewController: FormViewController { }.onCellSelection { [weak self] (_, _) in guard let self = self else { return } let vc = screensFactory.makeNodesList() - + if let split = splitViewController { - let details = UINavigationController(rootViewController:vc) + let details = UINavigationController(rootViewController: vc) split.showDetailViewController(details, sender: self) } else if let nav = navigationController { nav.pushViewController(vc, animated: true) @@ -294,12 +273,12 @@ final class AccountViewController: FormViewController { vc.modalPresentationStyle = .overFullScreen present(vc, animated: true, completion: nil) } - + deselectWalletViewControllers() } - + appSection.append(nodesRow) - + // Coins nodes list let coinsNodesRow = LabelRow { $0.title = Rows.coinsNodes.localized @@ -312,9 +291,9 @@ final class AccountViewController: FormViewController { }.onCellSelection { [weak self] (_, _) in guard let self = self else { return } let vc = screensFactory.makeCoinsNodesList(context: .menu) - + if let split = splitViewController { - let details = UINavigationController(rootViewController:vc) + let details = UINavigationController(rootViewController: vc) split.showDetailViewController(details, sender: self) } else if let nav = navigationController { nav.pushViewController(vc, animated: true) @@ -322,12 +301,12 @@ final class AccountViewController: FormViewController { vc.modalPresentationStyle = .overFullScreen present(vc, animated: true, completion: nil) } - + deselectWalletViewControllers() } - + appSection.append(coinsNodesRow) - + // Language select let languageRow = ActionSheetRow { $0.title = Rows.language.localized @@ -335,7 +314,7 @@ final class AccountViewController: FormViewController { $0.cell.imageView?.image = Rows.language.image $0.options = Language.all $0.value = languageService.getLanguage() - + $0.displayValueFor = { language in return language?.name } @@ -347,9 +326,9 @@ final class AccountViewController: FormViewController { self?.languageService.setLanguage(value) self?.updateUI() } - + appSection.append(languageRow) - + // Currency select let currencyRow = ActionSheetRow { $0.title = Rows.currency.localized @@ -357,12 +336,12 @@ final class AccountViewController: FormViewController { $0.cell.imageView?.image = Rows.currency.image $0.options = [Currency.USD, Currency.EUR, Currency.RUB, Currency.CNY, Currency.JPY] $0.value = currencyInfoService.currentCurrency - + $0.displayValueFor = { currency in guard let currency = currency else { return nil } - + return "\(currency.rawValue) (\(currency.symbol))" } }.cellUpdate { (cell, row) in @@ -372,9 +351,9 @@ final class AccountViewController: FormViewController { guard let value = row.value else { return } self?.currencyInfoService.currentCurrency = value } - + appSection.append(currencyRow) - + // Contribute let contributeRow = LabelRow { $0.title = Rows.contribute.localized @@ -387,7 +366,7 @@ final class AccountViewController: FormViewController { }.onCellSelection { [weak self] (_, _) in guard let self = self else { return } let vc = screensFactory.makeContribute() - + if let split = splitViewController { let details = UINavigationController(rootViewController: vc) split.showDetailViewController(details, sender: self) @@ -397,12 +376,12 @@ final class AccountViewController: FormViewController { vc.modalPresentationStyle = .overFullScreen present(vc, animated: true, completion: nil) } - + deselectWalletViewControllers() } - + appSection.append(contributeRow) - + // Storage Usage let storageRow = LabelRow { $0.title = Rows.storage.localized @@ -415,7 +394,7 @@ final class AccountViewController: FormViewController { }.onCellSelection { [weak self] (_, _) in guard let self = self else { return } let vc = screensFactory.makeStorageUsage() - + if let split = splitViewController { let details = UINavigationController(rootViewController: vc) split.showDetailViewController(details, sender: self) @@ -425,12 +404,12 @@ final class AccountViewController: FormViewController { vc.modalPresentationStyle = .overFullScreen present(vc, animated: true, completion: nil) } - + deselectWalletViewControllers() } - + appSection.append(storageRow) - + // About let aboutRow = LabelRow { $0.title = Rows.about.localized @@ -443,9 +422,9 @@ final class AccountViewController: FormViewController { }.onCellSelection { [weak self] (_, _) in guard let self = self else { return } let vc = screensFactory.makeAbout() - + if let split = splitViewController { - let details = UINavigationController(rootViewController:vc) + let details = UINavigationController(rootViewController: vc) split.showDetailViewController(details, sender: self) } else if let nav = navigationController { nav.pushViewController(vc, animated: true) @@ -453,17 +432,17 @@ final class AccountViewController: FormViewController { vc.modalPresentationStyle = .overFullScreen present(vc, animated: true, completion: nil) } - + deselectWalletViewControllers() } - + appSection.append(aboutRow) - + // MARK: Actions let actionsSection = Section(Sections.actions.localized) { $0.tag = Sections.actions.tag } - + // Delegates let delegatesRow = LabelRow { $0.tag = Rows.voteForDelegates.tag @@ -476,9 +455,9 @@ final class AccountViewController: FormViewController { }.onCellSelection { [weak self] (_, _) in guard let self = self else { return } let vc = screensFactory.makeDelegatesList() - + if let split = splitViewController { - let details = UINavigationController(rootViewController:vc) + let details = UINavigationController(rootViewController: vc) details.definesPresentationContext = true split.showDetailViewController(details, sender: self) } else if let nav = navigationController { @@ -487,12 +466,12 @@ final class AccountViewController: FormViewController { vc.modalPresentationStyle = .overFullScreen present(vc, animated: true, completion: nil) } - + deselectWalletViewControllers() } - + actionsSection.append(delegatesRow) - + // Generate passphrase QR let generateQrRow = LabelRow { $0.title = Rows.generateQr.localized @@ -505,9 +484,9 @@ final class AccountViewController: FormViewController { }.onCellSelection { [weak self] (_, _) in guard let self = self else { return } let vc = screensFactory.makeQRGenerator() - + if let split = splitViewController { - let details = UINavigationController(rootViewController:vc) + let details = UINavigationController(rootViewController: vc) split.showDetailViewController(details, sender: self) } else if let nav = navigationController { nav.pushViewController(vc, animated: true) @@ -515,12 +494,12 @@ final class AccountViewController: FormViewController { vc.modalPresentationStyle = .overFullScreen present(vc, animated: true, completion: nil) } - + deselectWalletViewControllers() } - + actionsSection.append(generateQrRow) - + // Generatte private keys let generatePkRow = LabelRow { $0.title = Rows.generatePk.localized @@ -533,9 +512,9 @@ final class AccountViewController: FormViewController { }.onCellSelection { [weak self] (_, _) in guard let self = self else { return } let vc = screensFactory.makePKGenerator() - + if let split = splitViewController { - let details = UINavigationController(rootViewController:vc) + let details = UINavigationController(rootViewController: vc) split.showDetailViewController(details, sender: self) } else if let nav = navigationController { nav.pushViewController(vc, animated: true) @@ -543,12 +522,12 @@ final class AccountViewController: FormViewController { vc.modalPresentationStyle = .overFullScreen present(vc, animated: true, completion: nil) } - + deselectWalletViewControllers() } - + actionsSection.append(generatePkRow) - + // Logout let logoutRow = LabelRow { $0.title = Rows.logout.localized @@ -562,13 +541,18 @@ final class AccountViewController: FormViewController { guard let address = self?.accountService.account?.address else { return } - - let alert = UIAlertController(title: String.localizedStringWithFormat(String.adamant.alert.logoutMessageFormat, address), message: nil, preferredStyleSafe: .alert, source: nil) + + let alert = UIAlertController( + title: String.localizedStringWithFormat(String.adamant.alert.logoutMessageFormat, address), + message: nil, + preferredStyleSafe: .alert, + source: nil + ) let cancel = UIAlertAction(title: String.adamant.alert.cancel, style: .cancel) { _ in guard let indexPath = row.indexPath else { return } - + self?.tableView.deselectRow(at: indexPath, animated: true) } let logout = UIAlertAction( @@ -581,23 +565,23 @@ final class AccountViewController: FormViewController { vc.modalPresentationStyle = .overFullScreen self.dialogService.present(vc, animated: true, completion: nil) } - + alert.addAction(cancel) alert.addAction(logout) alert.modalPresentationStyle = .overFullScreen self?.present(alert, animated: true, completion: nil) } - + actionsSection.append(logoutRow) - + // MARK: Security section - + let securitySection = Section(Sections.security.localized) { $0.tag = Sections.security.tag } - + // Stay in - + let stayInRow = SwitchRow { $0.tag = Rows.stayIn.tag $0.title = Rows.stayIn.localized @@ -610,32 +594,35 @@ final class AccountViewController: FormViewController { guard let enabled = row.value else { return } - + self?.setStayLoggedIn(enabled: enabled) } - + securitySection.append(stayInRow) - + // Biometry let biometryRow = SwitchRow { [weak self] in guard let self = self else { return } $0.tag = Rows.biometry.tag $0.title = localAuth.biometryType.localized $0.value = accountService.useBiometry - + switch localAuth.biometryType { case .none: $0.cell.imageView?.image = nil case .touchID: $0.cell.imageView?.image = .asset(named: "row_touchid.png") case .faceID: $0.cell.imageView?.image = .asset(named: "row_faceid.png") } - - $0.hidden = Condition.function([], { [weak self] _ -> Bool in - guard let showBiometry = self?.showBiometryOptions else { - return true + + $0.hidden = Condition.function( + [], + { [weak self] _ -> Bool in + guard let showBiometry = self?.showBiometryOptions else { + return true + } + + return !showBiometry } - - return !showBiometry - }) + ) }.cellUpdate { [weak self] (cell, row) in cell.switchControl.onTintColor = UIColor.adamant.active row.title = self?.localAuth.biometryType.localized @@ -643,9 +630,9 @@ final class AccountViewController: FormViewController { let value = row.value ?? false self?.setBiometry(enabled: value) } - + securitySection.append(biometryRow) - + // Notifications let notificationsRow = LabelRow { [weak self] in $0.tag = Rows.notifications.tag @@ -653,14 +640,17 @@ final class AccountViewController: FormViewController { $0.cell.selectionStyle = .gray $0.value = self?.notificationsService.notificationsMode.localized $0.cell.imageView?.image = Rows.notifications.image - - $0.hidden = Condition.function([], { [weak self] _ -> Bool in - guard let showNotifications = self?.showLoggedInOptions else { - return true + + $0.hidden = Condition.function( + [], + { [weak self] _ -> Bool in + guard let showNotifications = self?.showLoggedInOptions else { + return true + } + + return !showNotifications } - - return !showNotifications - }) + ) }.cellUpdate { [weak self] (cell, row) in cell.accessoryType = .disclosureIndicator row.title = Rows.notifications.localized @@ -668,9 +658,9 @@ final class AccountViewController: FormViewController { }.onCellSelection { [weak self] (_, _) in guard let self = self else { return } let vc = screensFactory.makeNotifications() - + if let split = splitViewController { - let details = UINavigationController(rootViewController:vc) + let details = UINavigationController(rootViewController: vc) split.showDetailViewController(details, sender: self) } else if let nav = navigationController { nav.pushViewController(vc, animated: true) @@ -678,84 +668,84 @@ final class AccountViewController: FormViewController { vc.modalPresentationStyle = .overFullScreen present(vc, animated: true, completion: nil) } - + deselectWalletViewControllers() } - + securitySection.append(notificationsRow) - + // MARK: Appending sections form.append(securitySection) form.append(actionsSection) form.append(appSection) - + form.allRows.forEach { $0.baseCell.imageView?.tintColor = UIColor.adamant.tableRowIcons } - + // MARK: Notification Center addObservers() - + setColors() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.setNavigationBarHidden(true, animated: animated) - + if let indexPath = tableView.indexPathForSelectedRow { tableView.deselectRow(at: indexPath, animated: animated) } - + for vc in pagingViewController.pageViewController.children { vc.viewWillAppear(animated) } } - + override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) navigationController?.setNavigationBarHidden(false, animated: animated) } - + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - + if !initiated { initiated = true } } - + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - + if UIScreen.main.traitCollection.userInterfaceIdiom == .pad { tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 100, right: 0) } - + if UIScreen.main.traitCollection.userInterfaceIdiom == .pad, !initiated { layoutTableHeaderView() if !initiated { initiated = true } } - + pagingViewController?.indicatorColor = UIColor.adamant.primary } deinit { NotificationCenter.default.removeObserver(self) } - + // MARK: TableView configuration - + override func insertAnimation(forSections sections: [Section]) -> UITableView.RowAnimation { return .fade } - + override func deleteAnimation(forSections sections: [Section]) -> UITableView.RowAnimation { return .fade } - + // MARK: Other - + func addObservers() { NotificationCenter.default.addObserver( forName: Notification.Name.AdamantAccountService.userLoggedIn, @@ -764,7 +754,7 @@ final class AccountViewController: FormViewController { ) { [weak self] _ in MainActor.assumeIsolatedSafe { guard let self = self else { return } - + self.updateAccountInfo() self.tableView.setContentOffset( CGPoint( @@ -773,7 +763,7 @@ final class AccountViewController: FormViewController { ), animated: false ) - + self.pagingViewController.reloadData() self.tableView.reloadData() if let vc = self.pagingViewController.pageViewController.selectedViewController as? WalletViewController { @@ -781,7 +771,7 @@ final class AccountViewController: FormViewController { } } } - + NotificationCenter.default.addObserver( forName: Notification.Name.AdamantAccountService.userLoggedOut, object: nil, @@ -791,7 +781,7 @@ final class AccountViewController: FormViewController { self?.updateAccountInfo() } } - + NotificationCenter.default.addObserver( forName: Notification.Name.AdamantAccountService.accountDataUpdated, object: nil, @@ -801,7 +791,7 @@ final class AccountViewController: FormViewController { self?.updateAccountInfo() } } - + NotificationCenter.default.addObserver( forName: Notification.Name.AdamantAccountService.stayInChanged, object: nil, @@ -811,24 +801,24 @@ final class AccountViewController: FormViewController { guard let form = self?.form, let accountService = self?.accountService else { return } - + if let row: SwitchRow = form.rowBy(tag: Rows.stayIn.tag) { row.value = accountService.hasStayInAccount row.updateCell() } - + if let row: SwitchRow = form.rowBy(tag: Rows.biometry.tag) { row.value = accountService.hasStayInAccount && accountService.useBiometry row.evaluateHidden() row.updateCell() } - + if let row = form.rowBy(tag: Rows.notifications.tag) { row.evaluateHidden() } } } - + NotificationCenter.default.addObserver( forName: Notification.Name.AdamantNotificationService.notificationsModeChanged, object: nil, @@ -838,16 +828,16 @@ final class AccountViewController: FormViewController { guard let newMode = notification.userInfo?[AdamantUserInfoKey.NotificationsService.newNotificationsMode] as? NotificationsMode else { return } - + guard let row: LabelRow = self?.form.rowBy(tag: Rows.notifications.tag) else { return } - + row.value = newMode.localized row.updateCell() } } - + NotificationCenter.default.addObserver( forName: Notification.Name.WalletViewController.heightUpdated, object: nil, @@ -859,7 +849,7 @@ final class AccountViewController: FormViewController { let cvc = self?.pagingViewController.pageViewController.selectedViewController, vc.viewController == cvc else { return } - + if let initiated = self?.initiated { self?.updateHeaderSize(with: vc, animated: initiated) } else { @@ -867,93 +857,72 @@ final class AccountViewController: FormViewController { } } } - - NotificationCenter.default.addObserver( - forName: Notification.Name.AdamantVisibleWalletsService.visibleWallets, - object: nil, - queue: OperationQueue.main - ) { [weak self] _ in - MainActor.assumeIsolatedSafe { + + visibleWalletsService.statePublisher + .debounce(for: .milliseconds(50), scheduler: DispatchQueue.main) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in guard let self = self else { return } - self.setupWalletsVC() + self.viewModel.updateState() + self.updatePagingItemHeight() - + self.pagingViewController.reloadData() let collectionView = self.pagingViewController.collectionView collectionView.reloadData() self.tableView.reloadData() - } - } - - for vc in walletViewControllers { - guard let service = vc.service?.core else { return } - let notification = service.walletUpdatedNotification - - let callback: @Sendable (Notification) -> Void = { [weak self] _ in - MainActor.assumeIsolatedSafe { - guard let self = self else { return } - let collectionView = self.pagingViewController.collectionView - collectionView.reloadData() - } - } - NotificationCenter.default.addObserver( - forName: notification, - object: service, - queue: OperationQueue.main, - using: callback - ) - } + selectCurrentWallet() + } + .store(in: ¬ificationsSet) } - + private func updateUI() { let appSection = form.sectionBy(tag: Sections.application.tag) appSection?.header?.title = Sections.application.localized let walletSection = form.sectionBy(tag: Sections.wallet.tag) walletSection?.header?.title = Sections.wallet.localized - + let securitySection = form.sectionBy(tag: Sections.security.tag) securitySection?.header?.title = Sections.security.localized - + let actionsSection = form.sectionBy(tag: Sections.actions.tag) actionsSection?.header?.title = Sections.actions.localized - + tableView.reloadData() - + tabBarController?.viewControllers?.first?.tabBarItem.title = .adamant.tabItems.chats tabBarController?.viewControllers?.last?.tabBarItem.title = .adamant.tabItems.account - + if let splitVC = tabBarController?.viewControllers?.first as? UISplitViewController, - !splitVC.isCollapsed { + !splitVC.isCollapsed + { splitVC.showDetailViewController(WelcomeViewController(), sender: nil) } - + if let splitVC = tabBarController?.viewControllers?.last as? UISplitViewController, - !splitVC.isCollapsed { + !splitVC.isCollapsed + { splitVC.showDetailViewController(WelcomeViewController(), sender: nil) } } - + private func setupWalletsVC() { walletViewControllers.removeAll() - let availableServices = visibleWalletsService.sorted(includeInvisible: false) - availableServices.forEach { walletService in + for walletService in walletStoreServiceProvider.sorted(includeInvisible: false) { walletViewControllers.append(screensFactory.makeWalletVC(service: walletService)) } } - + private func updatePagingItemHeight() { - if walletViewControllers.count > 0 { - pagingViewController.menuItemSize = .fixed(width: 110, height: 114) - } else { - pagingViewController.menuItemSize = .fixed(width: 110, height: 0) - } - + let itemHeight: CGFloat = walletViewControllers.count > .zero ? 114 : .zero + pagingViewController.menuItemSize = .fixed(width: 110, height: itemHeight) + updateHeaderSize(with: pagingViewController.menuItemSize.height, animated: true) } - + private func setColors() { view.backgroundColor = .adamant.secondBackgroundColor pagingViewController.backgroundColor = .adamant.backgroundColor @@ -963,18 +932,18 @@ final class AccountViewController: FormViewController { tableView.backgroundColor = .clear accountHeaderView.backgroundColor = .adamant.backgroundColor } - + func updateAccountInfo() { let address: String - + if let account = accountService.account { address = account.address } else { address = "" } - + accountHeaderView.addressButton.setTitle(address, for: .normal) - + if let publickey = accountService.keypair?.publicKey { DispatchQueue.global().async { [avatarService] in let image = avatarService.avatar(for: publickey, size: 200) @@ -984,7 +953,7 @@ final class AccountViewController: FormViewController { } } } - + func layoutTableHeaderView() { guard let view = tableView.tableHeaderView else { return } var frame = view.frame @@ -994,38 +963,48 @@ final class AccountViewController: FormViewController { self.tableView.tableHeaderView = view } - + private func deselectWalletViewControllers() { for controller in walletViewControllers { guard let vc = controller.viewController as? WalletViewControllerBase else { continue } - + // ViewController can be not yet initialized if let tableView = vc.tableView, let indexPath = tableView.indexPathForSelectedRow { tableView.deselectRow(at: indexPath, animated: true) } } } - + @objc private func handleRefresh(_ refreshControl: UIRefreshControl) { - let unavailableNodes: [NodeGroup] = NodeGroup.allCases.filter { - apiServiceCompose.get($0)?.hasEnabledNode == false - } - + let unavailableNodes: Set = Set( + NodeGroup.allCases.filter { + !(apiServiceCompose.get($0)?.hasSupportedNode ?? true) + } + ) + if unavailableNodes.contains(where: { - $0.name == currentSelectedWalletItem?.model.currencyNetwork + $0.name == currentSelectedWalletItem?.currencyNetwork }) { dialogService.showWarning( withMessage: ApiServiceError.noEndpointsAvailable( - nodeGroupName: currentSelectedWalletItem?.model.currencyNetwork ?? "" + nodeGroupName: currentSelectedWalletItem?.currencyNetwork ?? "" ).localizedDescription ) } - - refreshControl.endRefreshing() - DispatchQueue.background.async { [accountService] in - accountService.reloadWallets() + + Task { @MainActor in + accountService.update() + refreshControl.endRefreshing() + } + } + + private func selectCurrentWallet() { + if let index = viewModel.state.wallets.firstIndex(where: { $0.coinID == currentWalletCoinID }) { + pagingViewController.select(index: index, animated: false) + } else if let firstWalletID = viewModel.state.wallets.first?.coinID { + currentWalletCoinID = firstWalletID } } } @@ -1036,19 +1015,22 @@ extension AccountViewController: AccountHeaderViewDelegate { guard let address = accountService.account?.address else { return } - + let encodedAddress = AdamantUriTools.encode(request: AdamantUri.address(address: address, params: nil)) - dialogService.presentShareAlertFor(stringForPasteboard: address, - stringForShare: encodedAddress, - stringForQR: encodedAddress, - types: [.copyToPasteboard, - .share, - .generateQr(encodedContent: encodedAddress, sharingTip: address, withLogo: true) - ], - excludedActivityTypes: ShareContentType.address.excludedActivityTypes, - animated: true, - from: from, - completion: nil) + dialogService.presentShareAlertFor( + stringForPasteboard: address, + stringForShare: encodedAddress, + stringForQR: encodedAddress, + types: [ + .copyToPasteboard, + .share, + .generateQr(encodedContent: encodedAddress, sharingTip: address, withLogo: true) + ], + excludedActivityTypes: ShareContentType.address.excludedActivityTypes, + animated: true, + from: from, + completion: nil + ) } } @@ -1078,18 +1060,18 @@ extension AccountViewController: NSFetchedResultsControllerDelegate { extension AccountViewController: PagingViewControllerDataSource, PagingViewControllerDelegate { nonisolated func numberOfViewControllers(in pagingViewController: PagingViewController) -> Int { MainActor.assertIsolated() - + return DispatchQueue.onMainThreadSyncSafe { walletViewControllers.count } } - + nonisolated func pagingViewController( _ pagingViewController: PagingViewController, viewControllerAt index: Int ) -> UIViewController { MainActor.assertIsolated() - + return DispatchQueue.onMainThreadSyncSafe { walletViewControllers[index].viewController } @@ -1097,16 +1079,12 @@ extension AccountViewController: PagingViewControllerDataSource, PagingViewContr nonisolated func pagingViewController(_: PagingViewController, pagingItemAt index: Int) -> PagingItem { MainActor.assertIsolated() - + return DispatchQueue.onMainThreadSyncSafe { - guard let service = walletViewControllers[index].service?.core else { - return WalletItemModel(model: .default) - } - - return walletModels[service.tokenUnicID] ?? WalletItemModel(model: .default) + return viewModel.state.wallets[safe: index] ?? WalletCollectionViewCell.Model.default } } - + nonisolated func pagingViewController( _ pagingViewController: PagingViewController, didScrollToItem pagingItem: PagingItem, @@ -1118,36 +1096,40 @@ extension AccountViewController: PagingViewControllerDataSource, PagingViewContr guard transitionSuccessful, let first = startingViewController as? WalletViewController, let second = destinationViewController as? WalletViewController, - first.height != second.height else { + first.height != second.height + else { return } updateHeaderSize(with: second, animated: true) } } - + nonisolated func pagingViewController( _ pagingViewController: PagingViewController, didSelectItem pagingItem: PagingItem ) { Task { @MainActor in - currentWalletIndex = pagingItem.identifier + currentWalletCoinID = + viewModel.state.wallets.first(where: { wallet in + wallet.index == pagingItem.identifier + })?.coinID ?? "" } } - + private func updateHeaderSize(with walletViewController: WalletViewController, animated: Bool) { guard case let .fixed(_, menuHeight) = pagingViewController.menuItemSize else { return } let pagingHeight = menuHeight + walletViewController.height - + updateHeaderSize(with: pagingHeight, animated: animated) } - + private func updateHeaderSize(with pagingHeight: CGFloat, animated: Bool) { var headerBounds = accountHeaderView.bounds headerBounds.size.height = accountHeaderView.walletViewContainer.frame.origin.y + pagingHeight - + if animated { UIView.animate(withDuration: 0.2) { self.accountHeaderView.bounds = headerBounds @@ -1165,12 +1147,12 @@ extension AccountViewController: WalletViewControllerDelegate { if let indexPath = tableView.indexPathForSelectedRow { tableView.deselectRow(at: indexPath, animated: true) } - + for controller in walletViewControllers { guard controller.viewController != viewController, let vc = controller.viewController as? WalletViewControllerBase else { continue } - + // Better check it if let tableView = vc.tableView, let indexPath = tableView.indexPathForSelectedRow { tableView.deselectRow(at: indexPath, animated: true) @@ -1178,38 +1160,3 @@ extension AccountViewController: WalletViewControllerDelegate { } } } - -private extension AccountViewController { - func makeWalletModels() { - for (index, wallet) in walletViewControllers.enumerated() { - guard let service = wallet.service?.core else { - continue - } - - var network: String? - if ERC20Token.supportedTokens.contains(where: { token in - return token.symbol == service.tokenSymbol - }) { - network = type(of: service).tokenNetworkSymbol - } - - var item = WalletItem( - index: index, - currencySymbol: service.tokenSymbol, - currencyImage: service.tokenLogo, - isBalanceInitialized: service.wallet?.isBalanceInitialized, - currencyNetwork: network ?? type(of: service).tokenNetworkSymbol - ) - - if let wallet = service.wallet { - item.balance = wallet.balance - item.notifications = wallet.notifications - } else { - item.balance = nil - } - - let model = WalletItemModel(model: item) - walletModels[service.tokenUnicID] = model - } - } -} diff --git a/Adamant/Modules/Account/AccountViewController/AccountWallets/AccountWalletsState.swift b/Adamant/Modules/Account/AccountViewController/AccountWallets/AccountWalletsState.swift new file mode 100644 index 000000000..2587edc35 --- /dev/null +++ b/Adamant/Modules/Account/AccountViewController/AccountWallets/AccountWalletsState.swift @@ -0,0 +1,13 @@ +// +// AccountWalletsState.swift +// Adamant +// +// Created by Dmitrij Meidus on 29.01.25. +// Copyright © 2025 Adamant. All rights reserved. +// + +struct AccountWalletsState: Equatable { + var wallets: [WalletCollectionViewCell.Model] + + static let `default` = Self(wallets: []) +} diff --git a/Adamant/Modules/Account/AccountViewController/AccountWallets/AccountWalletsViewModel.swift b/Adamant/Modules/Account/AccountViewController/AccountWallets/AccountWalletsViewModel.swift new file mode 100644 index 000000000..456258fec --- /dev/null +++ b/Adamant/Modules/Account/AccountViewController/AccountWallets/AccountWalletsViewModel.swift @@ -0,0 +1,75 @@ +// +// AccountWalletsViewModel.swift +// Adamant +// +// Created by Dmitrij Meidus on 29.01.25. +// Copyright © 2025 Adamant. All rights reserved. +// + +import Combine +import CommonKit +import Foundation + +@MainActor +final class AccountWalletsViewModel { + @ObservableValue var state: AccountWalletsState = .default + + private let walletsStoreService: WalletStoreServiceProviderProtocol + private var subscriptions = Set() + + init(walletsStoreService: WalletStoreServiceProviderProtocol) { + self.walletsStoreService = walletsStoreService + setup() + } +} + +extension AccountWalletsViewModel { + fileprivate func setup() { + addObservers() + } + + fileprivate func addObservers() { + for wallet in walletsStoreService.sorted(includeInvisible: false) { + updateInfo(for: wallet) + wallet.core.walletUpdatePublisher + .sink( + receiveValue: { [weak self] _ in + self?.updateInfo(for: wallet) + } + ) + .store(in: &subscriptions) + } + } + + fileprivate func updateInfo(for wallet: WalletService) { + let coreService = wallet.core + if let index = state.wallets.firstIndex(where: { $0.coinID == coreService.tokenUniqueID }) { + state.wallets[index].balance = coreService.wallet?.balance ?? 0 + state.wallets[index].isBalanceInitialized = coreService.wallet?.isBalanceInitialized ?? false + state.wallets[index].notificationBadgeCount = coreService.wallet?.notifications ?? 0 + } else { + let network = type(of: coreService).tokenNetworkSymbol + + let model = WalletCollectionViewCell.Model( + index: state.wallets.count, + coinID: coreService.tokenUniqueID, + currencySymbol: coreService.tokenSymbol, + currencyImage: coreService.tokenLogo, + currencyNetwork: network, + isBalanceInitialized: coreService.wallet?.isBalanceInitialized ?? false, + balance: coreService.wallet?.balance ?? 0, + notificationBadgeCount: coreService.wallet?.notifications ?? 0 + ) + + state.wallets.append(model) + } + } +} + +extension AccountWalletsViewModel { + func updateState() { + subscriptions.removeAll() + state.wallets.removeAll() + setup() + } +} diff --git a/Adamant/Modules/Account/WalletCollectionViewCell+Model.swift b/Adamant/Modules/Account/WalletCollectionViewCell+Model.swift new file mode 100644 index 000000000..8b5563ec3 --- /dev/null +++ b/Adamant/Modules/Account/WalletCollectionViewCell+Model.swift @@ -0,0 +1,64 @@ +// +// Model.swift +// Adamant +// +// Created by Dmitrij Meidus on 29.01.25. +// Copyright © 2025 Adamant. All rights reserved. +// + +import Parchment +import UIKit + +extension WalletCollectionViewCell { + struct Model { + let index: Int + let coinID: String + let currencySymbol: String + let currencyImage: UIImage + let currencyNetwork: String + var isBalanceInitialized: Bool + var balance: Decimal? + var notificationBadgeCount: Int + + static let `default` = Model( + index: 0, + coinID: "", + currencySymbol: "", + currencyImage: UIImage(), + currencyNetwork: "", + isBalanceInitialized: false, + balance: nil, + notificationBadgeCount: 0 + ) + } +} + +// MARK: PagingItem +extension WalletCollectionViewCell.Model: PagingItem { + var identifier: Int { index } + + func isBefore(item: PagingItem) -> Bool { + guard let other = item as? Self else { return false } + return self.index < other.index + } + + func isEqual(to item: PagingItem) -> Bool { + guard let other = item as? Self else { return false } + return self == other + } +} + +// MARK: Comparable +extension WalletCollectionViewCell.Model: Comparable { + static func < (lhs: Self, rhs: Self) -> Bool { + lhs.index < rhs.index + } + + static func == (lhs: Self, rhs: Self) -> Bool { + return lhs.index == rhs.index && lhs.coinID == rhs.coinID && lhs.currencySymbol == rhs.currencySymbol && lhs.currencyNetwork == rhs.currencyNetwork + && lhs.isBalanceInitialized == rhs.isBalanceInitialized && lhs.balance == rhs.balance && lhs.notificationBadgeCount == rhs.notificationBadgeCount + } +} + +// MARK: Hashable +extension WalletCollectionViewCell.Model: Hashable {} diff --git a/Adamant/Modules/Account/WalletCollectionViewCell.swift b/Adamant/Modules/Account/WalletCollectionViewCell.swift index c96b44244..14bae1526 100644 --- a/Adamant/Modules/Account/WalletCollectionViewCell.swift +++ b/Adamant/Modules/Account/WalletCollectionViewCell.swift @@ -6,62 +6,47 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit +import Combine +import CommonKit import FreakingSimpleRoundImageView import Parchment -import CommonKit -import Combine +import UIKit class WalletCollectionViewCell: PagingCell { @IBOutlet weak var currencyImageView: UIImageView! @IBOutlet weak var balanceLabel: UILabel! @IBOutlet weak var currencySymbolLabel: UILabel! @IBOutlet weak var accessoryContainerView: AccessoryContainerView! - - private var cancellables = Set() - - override func prepareForReuse() { - cancellables.removeAll() - } - + override func setPagingItem( _ pagingItem: PagingItem, selected: Bool, options: PagingOptions ) { - guard let item = pagingItem as? WalletItemModel else { + guard let item = pagingItem as? WalletCollectionViewCell.Model else { return } - update(item: item.model) - - cancellables.removeAll() - - item.$model - .removeDuplicates() - .sink { [weak self] item in - self?.update(item: item) - } - .store(in: &cancellables) + update(item: item) } } -private extension WalletCollectionViewCell { - func update(item: WalletItem) { +extension WalletCollectionViewCell { + fileprivate func update(item: WalletCollectionViewCell.Model) { currencyImageView.image = item.currencyImage - if item.currencyNetwork.isEmpty { + if item.currencyNetwork == item.currencySymbol { currencySymbolLabel.text = item.currencySymbol } else { let currencyFont = currencySymbolLabel.font ?? .systemFont(ofSize: 12) let networkFont = currencyFont.withSize(8) let currencyAttributes: [NSAttributedString.Key: Any] = [.font: currencyFont] let networkAttributes: [NSAttributedString.Key: Any] = [.font: networkFont] - + let defaultString = NSMutableAttributedString(string: item.currencySymbol, attributes: currencyAttributes) let underlineString = NSAttributedString(string: " \(item.currencyNetwork)", attributes: networkAttributes) defaultString.append(underlineString) currencySymbolLabel.attributedText = defaultString } - + if let balance = item.balance, item.isBalanceInitialized { if balance < 1 { balanceLabel.text = AdamantBalanceFormat.compact.format(balance) @@ -71,9 +56,9 @@ private extension WalletCollectionViewCell { } else { balanceLabel.text = String.adamant.account.updatingBalance } - - if item.notifications > 0 { - accessoryContainerView.setAccessory(AccessoryType.label(text: String(item.notifications)), at: .topRight) + + if item.notificationBadgeCount > 0 { + accessoryContainerView.setAccessory(AccessoryType.label(text: String(item.notificationBadgeCount)), at: .topRight) } else { accessoryContainerView.setAccessory(nil, at: .topRight) } diff --git a/Adamant/Modules/Account/WalletPagingItem.swift b/Adamant/Modules/Account/WalletPagingItem.swift deleted file mode 100644 index 9cb04ec9a..000000000 --- a/Adamant/Modules/Account/WalletPagingItem.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// WalletPagingItem.swift -// Adamant -// -// Created by Anokhov Pavel on 10.08.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import UIKit -import Parchment -import CommonKit -import Combine - -struct WalletItem: Equatable { - var index: Int - var currencySymbol: String - var currencyImage: UIImage - var currencyNetwork: String - var isBalanceInitialized: Bool - var balance: Decimal? - var notifications: Int = 0 - - init( - index: Int, - currencySymbol symbol: String, - currencyImage image: UIImage, - isBalanceInitialized: Bool?, - currencyNetwork network: String = .empty, - balance: Decimal? = nil - ) { - self.index = index - self.isBalanceInitialized = isBalanceInitialized ?? false - self.currencySymbol = symbol - self.currencyImage = image - self.currencyNetwork = network - self.balance = balance - } - - static let `default` = Self( - index: .zero, - currencySymbol: .empty, - currencyImage: .init(), - isBalanceInitialized: nil - ) -} - -final class WalletItemModel: ObservableObject, PagingItem, Hashable, Comparable, @unchecked Sendable { - @Published var model: WalletItem = .default - - var identifier: Int { - model.index - } - - init(model: WalletItem) { - self.model = model - } - - // MARK: Hashable, Comparable - - func hash(into hasher: inout Hasher) { - hasher.combine(model.index) - hasher.combine(model.currencySymbol) - } - - static func < (lhs: WalletItemModel, rhs: WalletItemModel) -> Bool { - lhs.model.index < rhs.model.index - } - - static func == (lhs: WalletItemModel, rhs: WalletItemModel) -> Bool { - lhs.model.index == rhs.model.index && - lhs.model.currencySymbol == rhs.model.currencySymbol - } -} diff --git a/Adamant/Modules/Chat/ChatFactory.swift b/Adamant/Modules/Chat/ChatFactory.swift index 32debf2c8..e9f377a11 100644 --- a/Adamant/Modules/Chat/ChatFactory.swift +++ b/Adamant/Modules/Chat/ChatFactory.swift @@ -6,14 +6,14 @@ // Copyright © 2022 Adamant. All rights reserved. // -import UIKit -import MessageKit -import InputBarAccessoryView import Combine -import Swinject -import FilesStorageKit -import FilesPickerKit import CommonKit +import FilesPickerKit +import FilesStorageKit +import InputBarAccessoryView +import MessageKit +import Swinject +import UIKit @MainActor struct ChatFactory { @@ -25,7 +25,7 @@ struct ChatFactory { let accountProvider: AccountsProvider let richTransactionStatusService: TransactionsStatusServiceComposeProtocol let addressBookService: AddressBookService - let visibleWalletService: VisibleWalletsService + let walletsStoreService: WalletStoreServiceProtocol let avatarService: AvatarService let emojiService: EmojiService let walletServiceCompose: WalletServiceCompose @@ -36,7 +36,9 @@ struct ChatFactory { let apiServiceCompose: ApiServiceComposeProtocol let reachabilityMonitor: ReachabilityMonitor let filesPickerKit: FilesPickerProtocol - + let coreDataRealationMapper: CoreDataRealationMapperProtocol + let visibleWalletsService: VisibleWalletsService + init(assembler: Assembler) { chatsProvider = assembler.resolve(ChatsProvider.self)! dialogService = assembler.resolve(DialogService.self)! @@ -45,7 +47,7 @@ struct ChatFactory { accountProvider = assembler.resolve(AccountsProvider.self)! richTransactionStatusService = assembler.resolve(TransactionsStatusServiceComposeProtocol.self)! addressBookService = assembler.resolve(AddressBookService.self)! - visibleWalletService = assembler.resolve(VisibleWalletsService.self)! + walletsStoreService = assembler.resolve(WalletStoreServiceProtocol.self)! avatarService = assembler.resolve(AvatarService.self)! emojiService = assembler.resolve(EmojiService.self)! walletServiceCompose = assembler.resolve(WalletServiceCompose.self)! @@ -56,23 +58,26 @@ struct ChatFactory { apiServiceCompose = assembler.resolve(ApiServiceComposeProtocol.self)! reachabilityMonitor = assembler.resolve(ReachabilityMonitor.self)! filesPickerKit = assembler.resolve(FilesPickerProtocol.self)! + coreDataRealationMapper = assembler.resolve(CoreDataRealationMapperProtocol.self)! + visibleWalletsService = assembler.resolve(VisibleWalletsService.self)! } - + func makeViewController(screensFactory: ScreensFactory) -> ChatViewController { let viewModel = makeViewModel() let delegates = makeDelegates(viewModel: viewModel) let dialogManager = ChatDialogManager( viewModel: viewModel, dialogService: dialogService, - emojiService: emojiService + emojiService: emojiService, + accountService: accountService ) - + let wallets = walletServiceCompose.getWallets() - + let walletService = wallets.first { wallet in return wallet.core is AdmWalletService } - + let viewController = ChatViewController( viewModel: viewModel, walletServiceCompose: walletServiceCompose, @@ -85,7 +90,7 @@ struct ChatFactory { screensFactory: screensFactory ) ) - + viewController.setupDelegates(delegates) delegates.cell.setupDelegate( collection: viewController.messagesCollectionView, @@ -95,29 +100,32 @@ struct ChatFactory { } } -private extension ChatFactory { - struct Delegates { +extension ChatFactory { + fileprivate struct Delegates { let dataSource: MessagesDataSource let layout: MessagesLayoutDelegate let display: MessagesDisplayDelegate let inputBar: InputBarAccessoryViewDelegate let cell: ChatCellManager - + var asArray: [AnyObject] { [dataSource, layout, display, inputBar, cell] } } - - func makeViewModel() -> ChatViewModel { + + fileprivate func makeViewModel() -> ChatViewModel { .init( chatsProvider: chatsProvider, markdownParser: .init(font: UIFont.systemFont(ofSize: UIFont.systemFontSize)), transfersProvider: transferProvider, - chatMessagesListFactory: .init(chatMessageFactory: .init( - walletServiceCompose: walletServiceCompose - )), + chatMessagesListFactory: .init( + chatMessageFactory: .init( + walletServiceCompose: walletServiceCompose + ), + coreDataRelationMapper: coreDataRealationMapper + ), addressBookService: addressBookService, - visibleWalletService: visibleWalletService, + walletsStoreService: walletsStoreService, accountService: accountService, accountProvider: accountProvider, richTransactionStatusService: richTransactionStatusService, @@ -135,11 +143,12 @@ private extension ChatFactory { filesStorageProprieties: filesStorageProprieties, apiServiceCompose: apiServiceCompose, reachabilityMonitor: reachabilityMonitor, - filesPicker: filesPickerKit + filesPicker: filesPickerKit, + visibleWalletsService: visibleWalletsService ) } - - func makeDelegates(viewModel: ChatViewModel) -> Delegates { + + fileprivate func makeDelegates(viewModel: ChatViewModel) -> Delegates { .init( dataSource: ChatDataSourceManager(viewModel: viewModel), layout: ChatLayoutManager(viewModel: viewModel), @@ -148,19 +157,19 @@ private extension ChatFactory { cell: ChatCellManager(viewModel: viewModel) ) } - - func makeSendTransactionAction( + + fileprivate func makeSendTransactionAction( viewModel: ChatViewModel, screensFactory: ScreensFactory ) -> ChatViewController.SendTransaction { { [screensFactory, viewModel] parentVC, messageId in guard let vc = screensFactory.makeComplexTransfer() as? ComplexTransferViewController else { return } - + vc.partner = viewModel.chatroom?.partner vc.transferDelegate = parentVC vc.replyToMessageId = messageId - + let navigator = UINavigationController(rootViewController: vc) navigator.modalPresentationStyle = .overFullScreen parentVC.present(navigator, animated: true, completion: nil) @@ -168,8 +177,8 @@ private extension ChatFactory { } } -private extension ChatViewController { - func setupDelegates(_ delegates: ChatFactory.Delegates) { +extension ChatViewController { + fileprivate func setupDelegates(_ delegates: ChatFactory.Delegates) { messagesCollectionView.messagesDataSource = delegates.dataSource messagesCollectionView.messagesLayoutDelegate = delegates.layout messagesCollectionView.messagesDisplayDelegate = delegates.display @@ -178,15 +187,15 @@ private extension ChatViewController { } } -private extension ChatCellManager { - func setupDelegate(collection: MessagesCollectionView, dataSource: MessagesDataSource) { +extension ChatCellManager { + fileprivate func setupDelegate(collection: MessagesCollectionView, dataSource: MessagesDataSource) { getMessageId = { [weak collection, weak dataSource] cell in guard let collection = collection, let indexPath = collection.indexPath(for: cell), let message = dataSource?.messageForItem(at: indexPath, in: collection) else { return nil } - + return message.messageId } } diff --git a/Adamant/Modules/Chat/ChatLocalization.swift b/Adamant/Modules/Chat/ChatLocalization.swift index 22f7c8281..075206c59 100644 --- a/Adamant/Modules/Chat/ChatLocalization.swift +++ b/Adamant/Modules/Chat/ChatLocalization.swift @@ -6,8 +6,8 @@ // Copyright © 2022 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation // MARK: - Localization extension String.adamant { @@ -72,6 +72,18 @@ extension String.adamant { static var freeTokens: String { String.localized("ChatScene.FreeTokensAlert.FreeTokens", comment: "Chat: 'Free Tokens' button") } + static var freeTokensTitleChat: String { + String.localized("ChatScene.FreeTokensAlert.Title.Chat", comment: "Chat: 'Free Tokens' title") + } + static var freeTokensTitleBook: String { + String.localized("ChatScene.FreeTokensAlert.Title.Book", comment: "Chat: 'Free Tokens' title") + } + static var freeTokensTitleNotification: String { + String.localized("ChatScene.FreeTokensAlert.Title.Notification", comment: "Chat: 'Free Tokens' title") + } + static var freeTokensBuyADM: String { + String.localized("ChatScene.FreeTokensAlert.BuyADM", comment: "Chat: 'Free Tokens' action") + } static var freeTokensMessage: String { String.localized("ChatScene.FreeTokensAlert.Message", comment: "Chat: 'Free Tokens' message") } @@ -82,7 +94,10 @@ extension String.adamant { String.localized("ChatScene.Received", comment: "Chat: 'Received funds' bubble title") } static var messageWasDeleted: String { - String.localized("ChatScene.Error.messageWasDeleted", comment: "Chat: Error scrolling to message, this message has been deleted and is no longer accessible") + String.localized( + "ChatScene.Error.messageWasDeleted", + comment: "Chat: Error scrolling to message, this message has been deleted and is no longer accessible" + ) } static var messageIsTooBig: String { String.localized("ChatScene.Error.messageIsTooBig", comment: "Chat: Error message is too big") @@ -90,5 +105,23 @@ extension String.adamant { static var unknownTitle: String { String.localized("Chat.unknown.title", comment: "Chat unknown") } + static var noActiveNodesTitle: String { + String.localized("Chat.Alert.Title.NoActiveNodes", comment: "No active nodes title") + } + static var noActiveNodes: String { + String.localized("Chat.Alert.NoActiveNodes", comment: "No active nodes") + } + static var timestampIsInTheFutureTitle: String { + String.localized("Chat.Alert.Title.TimestampIsInTheFuture", comment: "Timestamp is in the future title") + } + static var timestampIsInTheFuture: String { + String.localized("Chat.Alert.TimestampIsInTheFuture", comment: "Timestamp is in the future text") + } + static var timeSettings: String { + String.localized("Chat.Alert.TimeSettings", comment: "Timestamp is in the future text") + } + static var reviewNodesList: String { + String.localized("Chat.Alert.ReviewNodesList", comment: "Review Nodes List") + } } } diff --git a/Adamant/Modules/Chat/View/ChatSelectTextViewFactory.swift b/Adamant/Modules/Chat/View/ChatSelectTextViewFactory.swift index 1c462a392..7306bea4a 100644 --- a/Adamant/Modules/Chat/View/ChatSelectTextViewFactory.swift +++ b/Adamant/Modules/Chat/View/ChatSelectTextViewFactory.swift @@ -7,15 +7,15 @@ // import Foundation +import SwiftUI import Swinject import UIKit -import SwiftUI struct ChatSelectTextViewFactory { @MainActor func makeViewController(text: String) -> UIViewController { let view = SelectTextView(text: text) - + return UIHostingController( rootView: view ) diff --git a/Adamant/Modules/Chat/View/ChatViewController+NavigationControllerDelegate.swift b/Adamant/Modules/Chat/View/ChatViewController+NavigationControllerDelegate.swift new file mode 100644 index 000000000..5c1fe609c --- /dev/null +++ b/Adamant/Modules/Chat/View/ChatViewController+NavigationControllerDelegate.swift @@ -0,0 +1,17 @@ +// +// ChatViewController+NavigationControllerDelegate.swift +// Adamant +// +// Created by Sergei Veretennikov on 07.03.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import UIKit + +extension ChatViewController: UINavigationControllerDelegate { + func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { + if viewController === self { + viewModel.checkForADMNodesAvailability() + } + } +} diff --git a/Adamant/Modules/Chat/View/ChatViewController.swift b/Adamant/Modules/Chat/View/ChatViewController.swift index b309fc4d0..690de5884 100644 --- a/Adamant/Modules/Chat/View/ChatViewController.swift +++ b/Adamant/Modules/Chat/View/ChatViewController.swift @@ -6,16 +6,16 @@ // Copyright © 2022 Adamant. All rights reserved. // -import MessageKit -import InputBarAccessoryView import Combine -import UIKit -import SnapKit import CommonKit +import FilesPickerKit import FilesStorageKit +import InputBarAccessoryView +import MessageKit import PhotosUI -import FilesPickerKit import QuickLook +import SnapKit +import UIKit @MainActor final class ChatViewController: MessagesViewController { @@ -24,29 +24,33 @@ final class ChatViewController: MessagesViewController { _ parentVC: UIViewController & ComplexTransferViewControllerDelegate, _ replyToMessageId: String? ) -> Void - + // MARK: Dependencies - + private let storedObjects: [AnyObject] private let walletServiceCompose: WalletServiceCompose private let admWalletService: WalletService? private let screensFactory: ScreensFactory private let chatSwipeManager: ChatSwipeManager - + let viewModel: ChatViewModel - + // MARK: Properties - + private var subscriptions = Set() - private var topMessageId: String? private var bottomMessageId: String? private var messagesLoaded = false private var isScrollPositionNearlyTheBottom = true private var viewAppeared = false - + private var scrollToUnreadBottomConstraint: Constraint? + private var isScrollDownButtonHidden = true + private var previousUnreadCount: Int = 0 + private var scrollToPositionType: UICollectionView.ScrollPosition = .top + private lazy var inputBar = ChatInputBar() private lazy var loadingView = LoadingView() private lazy var scrollDownButton = makeScrollDownButton() + private lazy var scrollToUnreadReactButton = makeScrollToUnreadReactButton() private lazy var chatMessagesCollectionView = makeChatMessagesCollectionView() private lazy var replyView = ReplyView() private lazy var filesToolbarView = FilesToolbarView() @@ -56,21 +60,21 @@ final class ChatViewController: MessagesViewController { textColor: .adamant.textColor, numberOfLines: 1 ) - + private var sendTransaction: SendTransaction - + // swiftlint:disable unused_setter_value override var messageInputBar: InputBarAccessoryView { get { inputBar } set { assertionFailure("Do not set messageInputBar") } } - + // swiftlint:disable unused_setter_value override var messagesCollectionView: MessagesCollectionView { get { chatMessagesCollectionView } set { assertionFailure("Do not set messagesCollectionView") } } - + private lazy var updatingIndicatorView: UpdatingIndicatorView = { let view = UpdatingIndicatorView(title: "", titleType: .small) view.snp.makeConstraints { make in @@ -79,12 +83,12 @@ final class ChatViewController: MessagesViewController { } return view }() - + private lazy var chatKeyboardManager: ChatKeyboardManager = { let data = ChatKeyboardManager(scrollView: messagesCollectionView) return data }() - + init( viewModel: ChatViewModel, walletServiceCompose: WalletServiceCompose, @@ -102,24 +106,24 @@ final class ChatViewController: MessagesViewController { self.sendTransaction = sendTransaction self.chatSwipeManager = chatSwipeManager super.init(nibName: nil, bundle: nil) - + inputBar.onAttachmentButtonTap = { [weak self] in self?.viewModel.presentActionMenu() } - + inputBar.onImagePasted = { [weak self] image in self?.viewModel.handlePastedImage(image) } - + viewModel.indexPathsForVisibleItems = { [weak self] in self?.messagesCollectionView.indexPathsForVisibleItems ?? .init() } } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .adamant.backgroundColor @@ -137,57 +141,62 @@ final class ChatViewController: MessagesViewController { viewModel.loadFirstMessagesIfNeeded() chatSwipeManager.configure(chatView: view) } - + override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() updateIsScrollPositionNearlyTheBottom() - updateScrollDownButtonVisibility() } - + override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() chatMessagesCollectionView.setFullBottomInset( view.bounds.height - inputContainerView.frame.minY ) } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + if navigationController?.delegate !== self { + navigationController?.delegate = self + } viewModel.updatePartnerName() + updateScrollDownButtonVisibility() + + // Needs to check the current state of the chats update to present or hide spinner on appear instantly + viewModel.checkUpdateState() } - + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) defer { viewAppeared = true } inputBar.isUserInteractionEnabled = true chatMessagesCollectionView.fixedBottomOffset = nil - + updateUnreadMessages() if !viewAppeared { viewModel.presentKeyboardOnStartIfNeeded() } - + guard isMacOS, !viewAppeared else { return } focusInputBarWithoutAnimation() } - + override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + updateUnreadMessages() inputBar.isUserInteractionEnabled = false inputBar.inputTextView.resignFirstResponder() } - + override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) - viewModel.preserveFiles() viewModel.preserveMessage(inputBar.text) - viewModel.preserveReplayMessage() viewModel.saveChatOffset( isScrollPositionNearlyTheBottom - ? nil - : chatMessagesCollectionView.bottomOffset + ? nil + : chatMessagesCollectionView.bottomOffset ) } - + override func collectionView( _ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, @@ -195,38 +204,38 @@ final class ChatViewController: MessagesViewController { ) { // TODO: refactor for architecture if let index = viewModel.needToAnimateCellIndex, - indexPath.section == index { + indexPath.section == index + { cell.isSelected = true cell.isSelected = false viewModel.needToAnimateCellIndex = nil } - super.collectionView(collectionView, willDisplay: cell, forItemAt: indexPath) } - + override func scrollViewDidEndDecelerating(_: UIScrollView) { scrollDidStop() } - + override func scrollViewDidEndDragging(_: UIScrollView, willDecelerate: Bool) { guard !willDecelerate else { return } scrollDidStop() } - + override func scrollViewDidScroll(_ scrollView: UIScrollView) { super.scrollViewDidScroll(scrollView) + updateUnreadMessages() updateIsScrollPositionNearlyTheBottom() updateScrollDownButtonVisibility() - + if scrollView.isTracking || scrollView.isDragging || scrollView.isDecelerating { updateDateHeaderIfNeeded() } - guard viewAppeared, scrollView.contentOffset.y <= viewModel.minOffsetForStartLoadNewMessages else { return } - + viewModel.loadMoreMessagesIfNeeded() } } @@ -285,17 +294,17 @@ extension ChatViewController { // MARK: Observers -private extension ChatViewController { - func scrollDidStop() { +extension ChatViewController { + fileprivate func scrollDidStop() { viewModel.startHideDateTimer() } - - func setupObservers() { + + fileprivate func setupObservers() { NotificationCenter.default .notifications(named: UITextView.textDidChangeNotification, object: inputBar.inputTextView) .sink { @MainActor [weak self] _ in self?.inputTextUpdated() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: UIApplication.didBecomeActiveNotification) .sink { @MainActor [weak self] _ in @@ -304,28 +313,48 @@ private extension ChatViewController { self.viewModel.updatePreviewFor(indexes: indexes) } .store(in: &subscriptions) - + + viewModel.didTapAdmNodesList + .sink { [weak self] in + self?.didTapReviewAdmNodes() + } + .store(in: &subscriptions) + + viewModel.didTapShowTimeSettings + .sink { [weak self] in + self?.didTapShowTimeSettings() + } + .store(in: &subscriptions) + viewModel.$messages .removeDuplicates() - .sink { [weak self] _ in self?.updateMessages() } + .sink { [weak self] _ in + self?.updateMessages() + } .store(in: &subscriptions) - + + viewModel.messagesUpdated + .sink { [weak self] _ in + self?.updateMessagesPosition() + } + .store(in: &subscriptions) + viewModel.$fullscreenLoading .removeDuplicates() .sink { [weak self] _ in self?.updateFullscreenLoadingView() } .store(in: &subscriptions) - + viewModel.$inputText .removeDuplicates() .assign(to: \.text, on: inputBar) .store(in: &subscriptions) - + viewModel.presentKeyboard .sink { [weak self] in self?.messageInputBar.inputTextView.becomeFirstResponder() } .store(in: &subscriptions) - + viewModel.$isSendingAvailable .removeDuplicates() .sink(receiveValue: { [weak self] value in @@ -337,41 +366,41 @@ private extension ChatViewController { } }) .store(in: &subscriptions) - + viewModel.$fee .removeDuplicates() .assign(to: \.fee, on: inputBar) .store(in: &subscriptions) - + viewModel.didTapTransfer .sink { [weak self] in self?.didTapTransfer(id: $0) } .store(in: &subscriptions) - + viewModel.$partnerName .sink { [weak self] in self?.updatingIndicatorView.updateTitle(title: $0) } .store(in: &subscriptions) - + viewModel.$partnerImage .sink { [weak self] in self?.updatingIndicatorView.updateImage(image: $0) } .store(in: &subscriptions) - + viewModel.closeScreen .sink { [weak self] in self?.close() } .store(in: &subscriptions) - + viewModel.$isAttachmentButtonAvailable .removeDuplicates() .assign(to: \.isAttachmentButtonEnabled, on: inputBar) .store(in: &subscriptions) - + viewModel.didTapAdmChat .sink { [weak self] in self?.didTapAdmChat(with: $0, message: $1) } .store(in: &subscriptions) - + viewModel.didTapAdmSend .sink { [weak self] in self?.didTapAdmSend(to: $0) } .store(in: &subscriptions) - + viewModel.$isHeaderLoading .removeDuplicates() .sink { [weak self] in @@ -383,92 +412,86 @@ private extension ChatViewController { } } .store(in: &subscriptions) - + viewModel.$replyMessage .sink { [weak self] in self?.processSwipeMessage($0) } .store(in: &subscriptions) - + viewModel.$filesPicked .sink { [weak self] in self?.processFileToolbarView($0) } .store(in: &subscriptions) - - viewModel.$scrollToMessage + + viewModel.$scrollToIdAndPosition .sink { [weak self] in - guard let toId = $0, - let fromId = $1 + guard let idAndPosition = $0 else { return } - - if self?.isScrollPositionNearlyTheBottom != true { - self?.viewModel.appendTempOffset(fromId, toId: toId) - } - self?.scrollToPosition(.messageId(toId), animated: true) + + self?.scrollToPositionType = idAndPosition.position + self?.scrollToPosition(.messageId(idAndPosition.id), animated: false, setExtraOffset: self?.scrollToPositionType == .top) + self?.viewModel.messageIdToShow = nil } .store(in: &subscriptions) - + viewModel.enableScroll .sink { [weak self] in self?.enableScroll($0) } .store(in: &subscriptions) - + viewModel.$isNeedToAnimateScroll .sink { [weak self] in self?.animateScroll(isStarted: $0) } .store(in: &subscriptions) - + viewModel.$dateHeader .removeDuplicates() .sink { [weak self] in self?.dateHeaderLabel.text = $0 } .store(in: &subscriptions) - + viewModel.$dateHeaderHidden .removeDuplicates() .sink { [weak self] in self?.dateHeaderLabel.isHidden = $0 } .store(in: &subscriptions) - - viewModel.updateChatRead - .sink { [weak self] in self?.checkIsChatWasRead() } - .store(in: &subscriptions) - + viewModel.commitVibro .sink { UIImpactFeedbackGenerator(style: .light).impactOccurred() } .store(in: &subscriptions) - + viewModel.layoutIfNeeded .sink { [weak self] in self?.view.layoutIfNeeded() } .store(in: &subscriptions) - + viewModel.didTapPartnerQR .sink { [weak self] in self?.didTapPartenerQR(partner: $0) } .store(in: &subscriptions) - + viewModel.presentSendTokensVC .sink { [weak self] in guard let self = self else { return } - + sendTransaction(self, self.viewModel.replyMessage?.id) self.viewModel.clearReplyMessage() self.viewModel.clearPickedFiles() } .store(in: &subscriptions) - + viewModel.presentMediaPickerVC .sink { [weak self] in self?.presentMediaPicker() } .store(in: &subscriptions) - + viewModel.presentDocumentPickerVC .sink { [weak self] in self?.presentDocumentPicker() } .store(in: &subscriptions) - + viewModel.presentDocumentViewerVC .sink { [weak self] (files, index) in self?.presentDocumentViewer(files: files, selectedIndex: index) } .store(in: &subscriptions) - + viewModel.presentDropView - .sink { [weak self] in self?.presentDropView($0) } + .sink { [weak self] in self?.presentDropView($0) } .store(in: &subscriptions) viewModel.didTapSelectText @@ -476,55 +499,105 @@ private extension ChatViewController { self?.didTapSelectText(text: text) } .store(in: &subscriptions) + + viewModel.$unreadMesaggesIndexes + .removeDuplicates() + .sink { [weak self] _ in + self?.updateUnreadMessages() + } + .store(in: &subscriptions) + + viewModel.$unreadMessagesIds + .removeDuplicates() + .sink { [weak self] _ in + self?.updateScrollDownButtonVisibility() + } + .store(in: &subscriptions) + + viewModel.$messagesWithUnredReactionsIds + .removeDuplicates() + .sink { [weak self] _ in + self?.updateScrollToUnreadButtonVisibility() + } + .store(in: &subscriptions) + + viewModel.showBuyAndSell + .sink { [weak self] in + self?.presentBuyAndSell() + } + .store(in: &subscriptions) } } // MARK: Configuration -private extension ChatViewController { - func configureDropFiles() { +extension ChatViewController { + fileprivate func configureDropFiles() { chatDropView.alpha = .zero view.addSubview(chatDropView) chatDropView.snp.makeConstraints { $0.directionalEdges.equalTo(view.safeAreaLayoutGuide).inset(5) } - + view.addInteraction(UIDropInteraction(delegate: viewModel.dropInteractionService)) } - - func configureLayout() { + + fileprivate func configureLayout() { view.addSubview(scrollDownButton) - scrollDownButton.snp.makeConstraints { [unowned inputBar] in + view.addSubview(scrollToUnreadReactButton) + scrollDownButton.snp.makeConstraints { $0.trailing.equalToSuperview().inset(scrollDownButtonInset) $0.bottom.equalTo(inputBar.snp.top).offset(-scrollDownButtonInset) - $0.size.equalTo(30) + $0.size.equalTo(scrollButtonHeight) + } + scrollToUnreadReactButton.snp.makeConstraints { + $0.centerX.equalTo(scrollDownButton.snp.centerX) + self.scrollToUnreadBottomConstraint = $0.bottom.equalTo(scrollDownButton.snp.bottom).constraint + $0.size.equalTo(scrollButtonHeight + 6) } - + view.addSubview(loadingView) loadingView.snp.makeConstraints { $0.directionalEdges.equalToSuperview() } } - - func configureHeader() { + + fileprivate func updateScrollToUnreadButtonPosition() { + let offset = (scrollDownButton.alpha == 0) ? 0 : -(scrollToUnreadInset + scrollButtonHeight) + scrollToUnreadBottomConstraint?.update(offset: offset) + if messagesLoaded { + self.view.layoutIfNeeded() + } + } + + fileprivate func updateUnreadMessages() { + guard let unreadIndexes = viewModel.unreadMesaggesIndexes, !unreadIndexes.isEmpty else { return } + let visibleIndexPaths = messagesCollectionView.indexPathsForVisibleItems + + for indexPath in visibleIndexPaths where unreadIndexes.contains(indexPath.section) { + viewModel.markMessageAsRead(index: indexPath.section) + } + } + + fileprivate func configureHeader() { navigationItem.titleView = updatingIndicatorView navigationItem.largeTitleDisplayMode = .never - + configureHeaderRightButton() - + let tapGesture = UITapGestureRecognizer( target: self, action: #selector(shortTapAction) ) - + let longPressGesture = UILongPressGestureRecognizer( target: self, action: #selector(longTapAction(_:)) ) - + navigationItem.titleView?.addGestureRecognizer(tapGesture) navigationItem.titleView?.addGestureRecognizer(longPressGesture) - + view.addSubview(dateHeaderLabel) dateHeaderLabel.backgroundColor = .adamant.chatSenderBackground dateHeaderLabel.textInsets = .init(top: 4, left: 7, bottom: 4, right: 7) @@ -535,8 +608,8 @@ private extension ChatViewController { make.centerX.equalToSuperview() } } - - func configureHeaderRightButton() { + + fileprivate func configureHeaderRightButton() { navigationItem.rightBarButtonItem = .init( title: "•••", style: .plain, @@ -544,36 +617,36 @@ private extension ChatViewController { action: #selector(showMenu) ) } - - func configureReplyView() { + + fileprivate func configureReplyView() { replyView.snp.makeConstraints { make in make.height.equalTo(40) } - + replyView.closeAction = { [weak self] in self?.viewModel.replyMessage = nil } } - - func configureFilesToolbarView() { + + fileprivate func configureFilesToolbarView() { filesToolbarView.snp.makeConstraints { make in make.height.equalTo(filesToolbarViewHeight) } - + filesToolbarView.closeAction = { [weak self] in self?.viewModel.updateFiles(nil) } - + filesToolbarView.updatedDataAction = { [weak self] data in self?.viewModel.updateFiles(data) } - + filesToolbarView.openFileAction = { [weak self] data in self?.presentDocumentViewer(file: data) } } - - func configureGestures() { + + fileprivate func configureGestures() { /// Replaces the delegate of the pan gesture recognizer used in the input bar control of MessageKit. /// This gesture controls the position of the input bar when the keyboard is open and the user swipes it to dismiss. /// Due to incorrect checks in MessageKit, we manually set the delegate and assign it to our custom chatKeyboardManager object. @@ -582,7 +655,7 @@ private extension ChatViewController { gesture.delegate = chatKeyboardManager chatKeyboardManager.panGesture = gesture } - + /// Resolves the conflict between horizontal swipe gestures and vertical scrolling in the MessageKit's UICollectionView. /// The gestureRecognizerShouldBegin method checks the velocity of the pan gesture and allows it to begin only if the horizontal velocity is greater than the vertical velocity. /// This ensures smooth and uninterrupted vertical scrolling while still allowing horizontal swipe gestures to be recognized. @@ -592,12 +665,12 @@ private extension ChatViewController { messagesCollectionView.clipsToBounds = false } - func presentMediaPicker() { + fileprivate func presentMediaPicker() { guard !isMacOS else { return presentDocumentPicker() } messageInputBar.inputTextView.resignFirstResponder() - + viewModel.mediaPickerDelegate.preSelectedFiles = viewModel.filesPicked ?? [] - + let assetIds = viewModel.filesPicked?.compactMap { $0.assetId } ?? [] var phPickerConfig = PHPickerConfiguration(photoLibrary: .shared()) @@ -605,15 +678,16 @@ private extension ChatViewController { phPickerConfig.filter = PHPickerFilter.any(of: [.images, .videos, .livePhotos]) phPickerConfig.preselectedAssetIdentifiers = assetIds phPickerConfig.selection = .ordered - + let phPickerVC = PHPickerViewController(configuration: phPickerConfig) phPickerVC.delegate = viewModel.mediaPickerDelegate + phPickerVC.view.tintColor = .systemBlue present(phPickerVC, animated: true) } - - func presentDocumentPicker() { + + fileprivate func presentDocumentPicker() { messageInputBar.inputTextView.resignFirstResponder() - + let documentPicker = UIDocumentPickerViewController( forOpeningContentTypes: [.data, .content], asCopy: false @@ -622,55 +696,59 @@ private extension ChatViewController { documentPicker.delegate = viewModel.documentPickerDelegate present(documentPicker, animated: true) } - - func presentDocumentViewer(files: [FileResult], selectedIndex: Int) { + + fileprivate func presentDocumentViewer(files: [FileResult], selectedIndex: Int) { viewModel.documentViewerService.openFile( files: files ) - + let quickVC = QLPreviewController() quickVC.delegate = viewModel.documentViewerService quickVC.dataSource = viewModel.documentViewerService quickVC.modalPresentationStyle = .fullScreen quickVC.currentPreviewItemIndex = selectedIndex - + if let splitViewController = splitViewController { splitViewController.present(quickVC, animated: true) } else { present(quickVC, animated: true) } } - - func presentDocumentViewer(file: FileResult) { + + fileprivate func presentDocumentViewer(file: FileResult) { viewModel.documentViewerService.openFile(files: [file]) - + let quickVC = QLPreviewController() quickVC.delegate = viewModel.documentViewerService quickVC.dataSource = viewModel.documentViewerService quickVC.modalPresentationStyle = .fullScreen - + if let splitViewController = splitViewController { splitViewController.present(quickVC, animated: true) } else { present(quickVC, animated: true) } } - - func presentDropView(_ value: Bool) { + + fileprivate func presentDropView(_ value: Bool) { UIView.animate(withDuration: 0.25) { self.chatDropView.alpha = value ? 1.0 : .zero } } + fileprivate func presentBuyAndSell() { + let buyAndSellVC = screensFactory.makeBuyAndSell() + navigationController?.pushViewController(buyAndSellVC, animated: true) + } } // MARK: Tap on title view -private extension ChatViewController { - @objc func shortTapAction() { +extension ChatViewController { + @objc fileprivate func shortTapAction() { viewModel.openPartnerQR() } - - @objc func longTapAction(_ gestureRecognizer: UILongPressGestureRecognizer) { + + @objc fileprivate func longTapAction(_ gestureRecognizer: UILongPressGestureRecognizer) { guard gestureRecognizer.state == .began else { return } viewModel.renamePartner() } @@ -678,56 +756,90 @@ private extension ChatViewController { // MARK: Content updating -private extension ChatViewController { - func updateIsScrollPositionNearlyTheBottom() { - let oldValue = isScrollPositionNearlyTheBottom +extension ChatViewController { + fileprivate func updateIsScrollPositionNearlyTheBottom() { isScrollPositionNearlyTheBottom = chatMessagesCollectionView.bottomOffset < 150 - - guard oldValue != isScrollPositionNearlyTheBottom else { return } - checkIsChatWasRead() - } - - func updateMessages() { - defer { checkIsChatWasRead() } - chatMessagesCollectionView.reloadData(newIds: viewModel.messages.map { $0.id }) + } + + fileprivate func updateMessages() { + chatMessagesCollectionView.reloadData(newIds: viewModel.messages.map { $0.id }, isOnBottom: isScrollPositionNearlyTheBottom) scrollDownOnNewMessageIfNeeded(previousBottomMessageId: bottomMessageId) bottomMessageId = viewModel.messages.last?.messageId - + } + + fileprivate func updateMessagesPosition() { guard !messagesLoaded, !viewModel.messages.isEmpty else { return } - viewModel.startPosition.map { scrollToPosition($0) } messagesLoaded = true + if viewModel.messageIdToShow == nil { + if let unreadMessage = viewModel.unreadMessagesIds?.first { + scrollToPosition(.messageId(unreadMessage), setExtraOffset: true) + } else if let position = viewModel.startPosition { + scrollToPosition(position) + } + } } - - func updateFullscreenLoadingView() { + + fileprivate func updateFullscreenLoadingView() { loadingView.isHidden = !viewModel.fullscreenLoading - + if viewModel.fullscreenLoading { loadingView.startAnimating() } else { loadingView.stopAnimating() } } - - func updateScrollDownButtonVisibility() { - scrollDownButton.isHidden = isScrollPositionNearlyTheBottom + + fileprivate func updateScrollDownButtonVisibility() { + let topCount = viewModel.unreadMessagesIds?.count ?? 0 + self.scrollDownButton.updateCounter(topCount) + guard isScrollDownButtonHidden != isScrollPositionNearlyTheBottom else { return } + isScrollDownButtonHidden = isScrollPositionNearlyTheBottom + let buttonUpdate = { + self.scrollDownButton.alpha = self.isScrollPositionNearlyTheBottom ? 0 : 1 + self.updateScrollToUnreadButtonPosition() + } + if messagesLoaded { + UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut) { + buttonUpdate() + } + } else { + buttonUpdate() + } + } + + fileprivate func updateScrollToUnreadButtonVisibility() { + let count = viewModel.messagesWithUnredReactionsIds?.count ?? 0 + scrollToUnreadReactButton.updateCounter(count) + + guard (previousUnreadCount == 0 && count > 0) || (previousUnreadCount > 0 && count == 0) else { + previousUnreadCount = count + return + } + previousUnreadCount = count + let updateAlpha = { self.scrollToUnreadReactButton.alpha = (count == 0) ? 0 : 1 } + if messagesLoaded { + UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseInOut, animations: updateAlpha) + } else { + updateAlpha() + } } - - func updateDateHeaderIfNeeded() { + + fileprivate func updateDateHeaderIfNeeded() { guard viewAppeared else { return } - + let targetY: CGFloat = targetYOffset + view.safeAreaInsets.top let visibleIndexPaths = messagesCollectionView.indexPathsForVisibleItems - + for indexPath in visibleIndexPaths { guard let cell = messagesCollectionView.cellForItem(at: indexPath) else { continue } - + let cellRect = messagesCollectionView.convert(cell.frame, to: self.view) - + guard cellRect.minY <= targetY && cellRect.maxY >= targetY else { continue } - + viewModel.checkTopMessage(indexPath: indexPath) break } @@ -736,27 +848,38 @@ private extension ChatViewController { // MARK: Making entities -private extension ChatViewController { - func makeScrollDownButton() -> ChatScrollButton { +extension ChatViewController { + fileprivate func makeScrollDownButton() -> ChatScrollButton { let button = ChatScrollButton(position: .down) button.action = { [weak self] in - guard let id = self?.viewModel.getTempOffset(visibleIndex: self?.messagesCollectionView.indexPathsForVisibleItems.last?.section) - else { - self?.viewModel.animateScrollIfNeeded( - to: self?.viewModel.messages.count ?? 0, - visibleIndex: self?.messagesCollectionView.indexPathsForVisibleItems.last?.section - ) - - self?.messagesCollectionView.scrollToBottom(animated: true) - return + guard let self else { return } + if viewModel.shouldScrollToBottom { + self.messagesCollectionView.scrollToBottom(animated: true) + } else if let id = viewModel.unreadMessagesIds?.first { + scrollToPositionType = .top + viewModel.scroll(to: id) + viewModel.shouldScrollToBottom = true } - self?.scrollToPosition(.messageId(id), animated: true) } - + button.alpha = 0 + return button + } + + fileprivate func makeScrollToUnreadReactButton() -> ChatScrollButton { + let button = ChatScrollButton(position: .reaction) + button.action = { [weak self] in + guard let self, + let unreadId = self.viewModel.messagesWithUnredReactionsIds?.last + else { return } + + scrollToPositionType = .bottom + viewModel.scroll(to: unreadId) + } + button.alpha = 0 return button } - - func makeChatMessagesCollectionView() -> ChatMessagesCollectionView { + + fileprivate func makeChatMessagesCollectionView() -> ChatMessagesCollectionView { let collection = ChatMessagesCollectionView() collection.refreshControl = ChatRefreshMock() collection.register(ChatTransactionCell.self) @@ -767,23 +890,26 @@ private extension ChatViewController { SpinnerCell.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader ) - + collection.register( + NewMessagesCell.self, + forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter + ) return collection } } // MARK: Other -private extension ChatViewController { - func focusInputBarWithoutAnimation() { +extension ChatViewController { + fileprivate func focusInputBarWithoutAnimation() { // "becomeFirstResponder()" causes content animation on start without this fix Task { try await Task.sleep(interval: .zero) messageInputBar.inputTextView.becomeFirstResponder() } } - - func enableScroll(_ isEnabled: Bool) { + + fileprivate func enableScroll(_ isEnabled: Bool) { if isEnabled { chatMessagesCollectionView.isScrollEnabled = true } else { @@ -791,8 +917,8 @@ private extension ChatViewController { chatMessagesCollectionView.isScrollEnabled = false } } - - func dismissTransferViewController( + + fileprivate func dismissTransferViewController( andPresent viewController: UIViewController?, didFinishWithTransfer: TransactionDetails? ) { @@ -804,82 +930,120 @@ private extension ChatViewController { guard let detailsViewController = viewController else { return } navigationController?.pushViewController(detailsViewController, animated: true) } - - func checkIsChatWasRead() { - guard isScrollPositionNearlyTheBottom, messagesLoaded else { return } - viewModel.entireChatWasRead() - } - + @MainActor - func scrollToPosition(_ position: ChatStartPosition, animated: Bool = false) { + fileprivate func scrollToPosition(_ position: ChatStartPosition, animated: Bool = false, setExtraOffset: Bool = false) { chatMessagesCollectionView.fixedBottomOffset = nil - + switch position { case let .offset(offset): chatMessagesCollectionView.setBottomOffset(offset, safely: viewAppeared) case let .messageId(id, scrollToBottomIfNotFound): - var index = viewModel.messages.firstIndex(where: { $0.messageId == id}) + var index = viewModel.messages.firstIndex(where: { $0.messageId == id }) var needToAnimateCell = true - + if scrollToBottomIfNotFound, - index == nil { + index == nil + { index = viewModel.messages.count - 1 needToAnimateCell = false } - + guard let index = index else { break } - + messagesCollectionView.scrollToItem( at: .init(item: .zero, section: index), - at: [.centeredVertically, .centeredHorizontally], + at: scrollToPositionType, animated: animated ) - - viewModel.needToAnimateCellIndex = needToAnimateCell - ? index - : nil - + + if setExtraOffset { + if checkIfNeedExtraOffsetForUnreadMessages() { + setExtraOffsetForNewMessages() + } + } + + viewModel.needToAnimateCellIndex = + needToAnimateCell + ? index + : nil + guard animated else { break } - + viewModel.animateScrollIfNeeded( to: index, visibleIndex: messagesCollectionView.indexPathsForVisibleItems.last?.section ) } - + guard !viewAppeared else { return } chatMessagesCollectionView.fixedBottomOffset = chatMessagesCollectionView.bottomOffset } - - func scrollDownOnNewMessageIfNeeded(previousBottomMessageId: String?) { + + fileprivate func setExtraOffsetForNewMessages() { + //extra scroll to 120 to see newMessage line and 2 strings from previus message + let newOffsetY = max( + messagesCollectionView.contentOffset.y - 120, + 0 + ) + let newOffset = CGPoint(x: messagesCollectionView.contentOffset.x, y: newOffsetY) + + messagesCollectionView.setContentOffset(newOffset, animated: false) + } + + fileprivate func checkIfNeedExtraOffsetForUnreadMessages() -> Bool { + guard let unreadCount = viewModel.unredMessageCount() else { return false } + + let totalSections = messagesCollectionView.numberOfSections + let visibleHeight = messagesCollectionView.bounds.height + var totalUnreadHeight: CGFloat = 0 + + for i in 0.. visibleHeight { + return true + } + } + return false + } + + fileprivate func scrollDownOnNewMessageIfNeeded(previousBottomMessageId: String?) { let messages = viewModel.messages - + guard let previousBottomMessageId = previousBottomMessageId, let index = messages.firstIndex(where: { $0.id == previousBottomMessageId }), index < messages.count - 1, isScrollPositionNearlyTheBottom || messages.last?.sender.senderId == viewModel.sender.senderId - && messages.last?.status == .pending + && messages.last?.status == .pending else { return } - + messagesCollectionView.scrollToBottom(animated: true) } - - @objc func showMenu(_ sender: UIBarButtonItem) { + + @objc fileprivate func showMenu(_ sender: UIBarButtonItem) { viewModel.dialog.send(.menu(sender: sender)) } - - func inputTextUpdated() { + + fileprivate func inputTextUpdated() { viewModel.inputText = inputBar.text } - - func processSwipeMessage(_ message: MessageModel?) { + + fileprivate func processSwipeMessage(_ message: MessageModel?) { guard let message = message else { closeReplyView() return } - + if !messageInputBar.topStackView.subviews.contains(replyView) { if messageInputBar.topStackView.arrangedSubviews.isEmpty { UIView.transition( @@ -891,36 +1055,37 @@ private extension ChatViewController { self.replyView, at: .zero ) - }) + } + ) } else { messageInputBar.topStackView.insertArrangedSubview( replyView, at: .zero ) } - + if viewAppeared { messageInputBar.inputTextView.becomeFirstResponder() } } - + replyView.update(with: message) } - - func closeReplyView() { + + fileprivate func closeReplyView() { replyView.removeFromSuperview() messageInputBar.invalidateIntrinsicContentSize() } - - func processFileToolbarView(_ data: [FileResult]?) { + + fileprivate func processFileToolbarView(_ data: [FileResult]?) { guard let data = data, !data.isEmpty else { inputBar.isForcedSendEnabled = false closeFileToolbarView() return } - + inputBar.isForcedSendEnabled = true - + if !messageInputBar.topStackView.subviews.contains(filesToolbarView) { UIView.transition( with: messageInputBar.topStackView, @@ -931,27 +1096,28 @@ private extension ChatViewController { self.filesToolbarView, at: self.messageInputBar.topStackView.arrangedSubviews.count ) - }) + } + ) if viewAppeared { messageInputBar.inputTextView.becomeFirstResponder() } } - + filesToolbarView.update(data) } - - func closeFileToolbarView() { + + fileprivate func closeFileToolbarView() { filesToolbarView.removeFromSuperview() messageInputBar.invalidateIntrinsicContentSize() } - - func didTapTransfer(id: String) { + + fileprivate func didTapTransfer(id: String) { guard let transaction = viewModel.chatTransactions.first( where: { $0.chatMessageId == id } ) else { return } - + switch transaction { case let transaction as TransferTransaction: didTapTransferTransaction(transaction) @@ -961,29 +1127,43 @@ private extension ChatViewController { return } } - - func didTapTransferTransaction(_ transaction: TransferTransaction) { + + fileprivate func didTapReviewAdmNodes() { + let vc = screensFactory.makeNodesList() + navigationController?.pushViewController(vc, animated: true) + } + + fileprivate func didTapShowTimeSettings() { + let settingsURL = isMacOS ? "x-apple.systempreferences:com.apple.preference.datetime" : "App-prefs:root=General&path=DATE_AND_TIME" + if let appSettings = URL(string: settingsURL), + UIApplication.shared.canOpenURL(appSettings) + { + UIApplication.shared.open(appSettings) + } + } + + fileprivate func didTapTransferTransaction(_ transaction: TransferTransaction) { let vc = screensFactory.makeAdmTransactionDetails(transaction: transaction) navigationController?.pushViewController(vc, animated: true) } - - func didTapPartenerQR(partner: CoreDataAccount) { + + fileprivate func didTapPartenerQR(partner: CoreDataAccount) { let vc = screensFactory.makePartnerQR(partner: partner) navigationController?.pushViewController(vc, animated: true) } - - func didTapSelectText(text: String) { + + fileprivate func didTapSelectText(text: String) { let vc = screensFactory.makeChatSelectTextView(text: text) present(vc, animated: true) } - - func didTapRichMessageTransaction(_ transaction: RichMessageTransaction) { + + fileprivate func didTapRichMessageTransaction(_ transaction: RichMessageTransaction) { guard let type = transaction.richType, let provider = walletServiceCompose.getWallet(by: type), let vc = screensFactory.makeDetailsVC(service: provider, transaction: transaction) else { return } - + switch transaction.transactionStatus { case .failed: guard transaction.getRichValue(for: RichContentKeys.transfer.hash) != nil @@ -991,46 +1171,47 @@ private extension ChatViewController { viewModel.dialog.send(.alert(.adamant.sharedErrors.inconsistentTransaction)) return } - + navigationController?.pushViewController(vc, animated: true) case .notInitiated, .pending, .success, .none, .inconsistent, .registered: navigationController?.pushViewController(vc, animated: true) } } - - @objc func onEnterClick() { + + @objc fileprivate func onEnterClick() { if messageInputBar.inputTextView.isFirstResponder { messageInputBar.didSelectSendButton() } else { messageInputBar.inputTextView.becomeFirstResponder() } } - - func getMessageIdByIndexPath(_ indexPath: IndexPath) -> String? { + + fileprivate func getMessageIdByIndexPath(_ indexPath: IndexPath) -> String? { getMessageByIndexPath(indexPath)?.messageId } - - func getMessageByIndexPath(_ indexPath: IndexPath) -> MessageType? { + + fileprivate func getMessageByIndexPath(_ indexPath: IndexPath) -> MessageType? { messagesCollectionView.messagesDataSource?.messageForItem( at: indexPath, in: messagesCollectionView ) } - - func animateScroll(isStarted: Bool) { + + fileprivate func animateScroll(isStarted: Bool) { UIView.animate(withDuration: 0.1) { self.messagesCollectionView.alpha = isStarted ? 0.2 : 1.0 } } - + // TODO: Use coordinator - - func close() { - let navVC = tabBarController? + + fileprivate func close() { + let navVC = + tabBarController? .selectedViewController? .children .first as? UINavigationController - + if let navVC = navVC { navVC.popToRootViewController(animated: true) } else { @@ -1041,31 +1222,32 @@ private extension ChatViewController { // MARK: Markdown -private extension ChatViewController { - func didTapAdmChat(with chatroom: Chatroom, message: String?) { +extension ChatViewController { + fileprivate func didTapAdmChat(with chatroom: Chatroom, message: String?) { var chatlistVC: ChatListViewController? - + if let nav = splitViewController?.viewControllers.first as? UINavigationController, - let vc = nav.viewControllers.first as? ChatListViewController { + let vc = nav.viewControllers.first as? ChatListViewController + { chatlistVC = vc } - + if let vc = navigationController?.viewControllers.first as? ChatListViewController { chatlistVC = vc } - + guard let chatlistVC = chatlistVC else { return } - + let vc = chatlistVC.chatViewController(for: chatroom) if let message = message { vc.messageInputBar.inputTextView.text = message vc.viewModel.inputText = message } - + self.navigationController?.pushViewController(vc, animated: true) } - - func didTapAdmSend(to adm: AdamantAddress) { + + fileprivate func didTapAdmSend(to adm: AdamantAddress) { guard let admWalletService = admWalletService else { return } let vc = screensFactory.makeTransferVC(service: admWalletService) vc.recipientAddress = adm.address @@ -1080,25 +1262,27 @@ private extension ChatViewController { extension ChatViewController { override func scrollViewDidEndScrollingAnimation(_: UIScrollView) { animateScroll(isStarted: false) - + guard let index = viewModel.needToAnimateCellIndex else { return } - + let isVisible = messagesCollectionView.indexPathsForVisibleItems.contains { $0.section == index } - + guard isVisible else { return } - + // TODO: refactor for architecture let cell = messagesCollectionView.cellForItem(at: .init(item: .zero, section: index)) cell?.isSelected = true cell?.isSelected = false - + viewModel.needToAnimateCellIndex = nil } } +private let scrollToUnreadInset: CGFloat = 10 private let scrollDownButtonInset: CGFloat = 20 private let messagePadding: CGFloat = 12 private let filesToolbarViewHeight: CGFloat = 140 private let targetYOffset: CGFloat = 20 +private let scrollButtonHeight: CGFloat = 30 diff --git a/Adamant/Modules/Chat/View/Helpers/AdamantCellAnimation.swift b/Adamant/Modules/Chat/View/Helpers/AdamantCellAnimation.swift index c8669afa7..bc7f0fae5 100644 --- a/Adamant/Modules/Chat/View/Helpers/AdamantCellAnimation.swift +++ b/Adamant/Modules/Chat/View/Helpers/AdamantCellAnimation.swift @@ -6,19 +6,19 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit import CommonKit +import UIKit extension UIView { func animateIsSelected(_ value: Bool, originalColor: UIColor?) { guard value else { return } backgroundColor = .adamant.active.withAlphaComponent(0.2) - + UIView.animate(withDuration: 1.0) { self.backgroundColor = originalColor } } - + func addShadow( shadowColor: UIColor = UIColor.black, shadowOffset: CGSize = .zero, diff --git a/Adamant/Modules/Chat/View/Helpers/ChatDropView.swift b/Adamant/Modules/Chat/View/Helpers/ChatDropView.swift index eb67a11df..1d709b3b4 100644 --- a/Adamant/Modules/Chat/View/Helpers/ChatDropView.swift +++ b/Adamant/Modules/Chat/View/Helpers/ChatDropView.swift @@ -6,43 +6,43 @@ // Copyright © 2024 Adamant. All rights reserved. // -import UIKit import SnapKit +import UIKit final class ChatDropView: UIView { private lazy var imageView = UIImageView(image: .asset(named: "uploadIcon")) private lazy var titleLabel = UILabel(font: titleFont, textColor: .lightGray) - + override init(frame: CGRect) { super.init(frame: frame) configure() } - + required init?(coder: NSCoder) { super.init(coder: coder) configure() } } -private extension ChatDropView { - func configure() { +extension ChatDropView { + fileprivate func configure() { layer.cornerRadius = 5 layer.borderWidth = 2.0 layer.borderColor = UIColor.adamant.active.cgColor backgroundColor = .systemBackground - + titleLabel.text = dropTitle imageView.tintColor = .lightGray - + addSubview(imageView) addSubview(titleLabel) - + imageView.snp.makeConstraints { make in make.centerX.equalToSuperview() make.centerY.equalToSuperview().offset(-15) make.size.equalTo(60) } - + titleLabel.snp.makeConstraints { make in make.centerX.equalToSuperview() make.top.equalTo(imageView.snp.bottom).offset(10) diff --git a/Adamant/Modules/Chat/View/Helpers/ChatFile.swift b/Adamant/Modules/Chat/View/Helpers/ChatFile.swift index 1ead696c0..ea34d4a9f 100644 --- a/Adamant/Modules/Chat/View/Helpers/ChatFile.swift +++ b/Adamant/Modules/Chat/View/Helpers/ChatFile.swift @@ -12,7 +12,7 @@ import UIKit struct DownloadStatus: Hashable { var isPreviewDownloading: Bool var isOriginalDownloading: Bool - + static let `default` = Self( isPreviewDownloading: false, isOriginalDownloading: false @@ -30,17 +30,17 @@ struct ChatFile: Equatable, Hashable, @unchecked Sendable { var isFromCurrentSender: Bool var fileType: FileType var progress: Int? - + var isBusy: Bool { isDownloading - || isUploading + || isUploading } - + var isDownloading: Bool { downloadStatus.isOriginalDownloading - || downloadStatus.isPreviewDownloading + || downloadStatus.isPreviewDownloading } - + static var `default`: Self { Self( file: .init([:]), diff --git a/Adamant/Modules/Chat/View/Helpers/ChatReactionsView.swift b/Adamant/Modules/Chat/View/Helpers/ChatReactionsView.swift index 1e9eeca8a..928d07db2 100644 --- a/Adamant/Modules/Chat/View/Helpers/ChatReactionsView.swift +++ b/Adamant/Modules/Chat/View/Helpers/ChatReactionsView.swift @@ -6,9 +6,9 @@ // Copyright © 2023 Adamant. All rights reserved. // -import SwiftUI import CommonKit import ElegantEmojiPicker +import SwiftUI protocol ChatReactionsViewDelegate: AnyObject { func didSelectEmoji(_ emoji: String) @@ -20,10 +20,10 @@ struct ChatReactionsView: View { private let defaultEmojis = ["😂", "🤔", "😁", "👍", "👌", "🤝"] private let selectedEmoji: String? private let messageId: String - + var didSelectEmoji: ((_ emoji: String, _ messageId: String) -> Void)? var didSelectMore: (() -> Void)? - + init( emojis: [String]?, selectedEmoji: String?, @@ -33,7 +33,7 @@ struct ChatReactionsView: View { self.selectedEmoji = selectedEmoji self.messageId = messageId } - + var body: some View { HStack(spacing: 10) { ScrollView(.horizontal, showsIndicators: false) { @@ -46,8 +46,8 @@ struct ChatReactionsView: View { .frame(width: 40, height: 40) .background( selectedEmoji == emoji - ? Color.init(uiColor: .gray.withAlphaComponent(0.75)) - : .clear + ? Color.init(uiColor: .gray.withAlphaComponent(0.75)) + : .clear ) .clipShape(Circle()) .onTapGesture { @@ -57,7 +57,7 @@ struct ChatReactionsView: View { } } .padding([.top, .bottom, .leading], 5) - + Button { didSelectMore?() } label: { @@ -79,7 +79,7 @@ struct ChatReactionsView: View { struct ChatReactionButton: View { let emoji: String - + var body: some View { Text(emoji) .font(.title) diff --git a/Adamant/Modules/Chat/View/Helpers/CircularProgressView.swift b/Adamant/Modules/Chat/View/Helpers/CircularProgressView.swift index f9859965f..503617ce2 100644 --- a/Adamant/Modules/Chat/View/Helpers/CircularProgressView.swift +++ b/Adamant/Modules/Chat/View/Helpers/CircularProgressView.swift @@ -15,7 +15,7 @@ final class CircularProgressState: ObservableObject { let progressColor: UIColor @Published var progress: Double = 0 @Published var hidden: Bool = false - + init( lineWidth: CGFloat = 6, backgroundColor: UIColor = .lightGray, @@ -33,11 +33,11 @@ final class CircularProgressState: ObservableObject { struct CircularProgressView: View { @StateObject private var state: CircularProgressState - + init(state: @escaping () -> CircularProgressState) { _state = .init(wrappedValue: state()) } - + var body: some View { ZStack { Circle() diff --git a/Adamant/Modules/Chat/View/Helpers/FileMessageStatus.swift b/Adamant/Modules/Chat/View/Helpers/FileMessageStatus.swift index 697e6a294..f6c5add5c 100644 --- a/Adamant/Modules/Chat/View/Helpers/FileMessageStatus.swift +++ b/Adamant/Modules/Chat/View/Helpers/FileMessageStatus.swift @@ -6,15 +6,15 @@ // Copyright © 2024 Adamant. All rights reserved. // -import UIKit import CommonKit +import UIKit enum FileMessageStatus: Equatable { case busy case needToDownload(failed: Bool) case failed case success - + var image: UIImage { switch self { case .busy: return .asset(named: "status_pending") ?? .init() @@ -27,7 +27,7 @@ enum FileMessageStatus: Equatable { return .asset(named: "download-circular") ?? .init() } } - + var imageTintColor: UIColor { switch self { case .busy, .needToDownload, .success: return .adamant.primary diff --git a/Adamant/Modules/Chat/View/Managers/ChatAction.swift b/Adamant/Modules/Chat/View/Managers/ChatAction.swift index b4fef84a4..b941b73fc 100644 --- a/Adamant/Modules/Chat/View/Managers/ChatAction.swift +++ b/Adamant/Modules/Chat/View/Managers/ChatAction.swift @@ -6,8 +6,8 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit import CommonKit +import UIKit enum ChatAction { case forceUpdateTransactionStatus(id: String) @@ -15,12 +15,13 @@ enum ChatAction { case reply(id: String) case scrollTo(message: ChatMessageReplyCell.Model) case copy(text: String) - case copyInPart(text:String) + case copyInPart(text: String) case report(id: String) case remove(id: String) case react(id: String, emoji: String) case presentMenu(arg: ChatContextMenuArguments) case openFile(messageId: String, file: ChatFile) + case cancelUploading(messageId: String, file: ChatFile) case autoDownloadContentIfNeeded(messageId: String, files: [ChatFile]) case forceDownloadAllFiles(messageId: String, files: [ChatFile]) } diff --git a/Adamant/Modules/Chat/View/Managers/ChatCellManager.swift b/Adamant/Modules/Chat/View/Managers/ChatCellManager.swift index e70c8fc55..e86b5d297 100644 --- a/Adamant/Modules/Chat/View/Managers/ChatCellManager.swift +++ b/Adamant/Modules/Chat/View/Managers/ChatCellManager.swift @@ -13,17 +13,17 @@ import MessageKit final class ChatCellManager: MessageCellDelegate { private let viewModel: ChatViewModel var getMessageId: ((MessageCollectionViewCell) -> String?)? - + init(viewModel: ChatViewModel) { self.viewModel = viewModel } - + nonisolated func didSelectURL(_ url: URL) { MainActor.assumeIsolatedSafe { viewModel.didSelectURL(url) } } - + nonisolated func didTapMessage(in cell: MessageCollectionViewCell) { MainActor.assumeIsolatedSafe { guard @@ -31,7 +31,7 @@ final class ChatCellManager: MessageCellDelegate { let message = viewModel.messages.first(where: { $0.id == id }), message.status == .failed else { return } - + viewModel.dialog.send(.failedMessageAlert(id: id, sender: .view(cell))) } } diff --git a/Adamant/Modules/Chat/View/Managers/ChatDataSourceManager.swift b/Adamant/Modules/Chat/View/Managers/ChatDataSourceManager.swift index 5d6fd69e1..deabd6511 100644 --- a/Adamant/Modules/Chat/View/Managers/ChatDataSourceManager.swift +++ b/Adamant/Modules/Chat/View/Managers/ChatDataSourceManager.swift @@ -6,43 +6,43 @@ // Copyright © 2022 Adamant. All rights reserved. // -@preconcurrency import MessageKit -import UIKit import Combine import CommonKit +@preconcurrency import MessageKit +import UIKit @MainActor final class ChatDataSourceManager: MessagesDataSource { private let viewModel: ChatViewModel - + nonisolated var currentSender: SenderType { MainActor.assumeIsolatedSafe { viewModel.sender } } - + init(viewModel: ChatViewModel) { self.viewModel = viewModel } - + nonisolated func numberOfSections(in messagesCollectionView: MessagesCollectionView) -> Int { MainActor.assumeIsolatedSafe { viewModel.messages.count } } - + nonisolated func messageForItem( at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView ) -> MessageType { MainActor.assumeIsolatedSafe { viewModel.messages[indexPath.section] } } - + nonisolated func messageTopLabelAttributedText( for message: MessageType, at _: IndexPath ) -> NSAttributedString? { MainActor.assumeIsolatedSafe { guard message.fullModel.status == .failed else { return nil } - + return .init( string: .adamant.chat.failToSend, attributes: [ @@ -52,21 +52,21 @@ final class ChatDataSourceManager: MessagesDataSource { ) } } - + nonisolated func messageBottomLabelAttributedText( for message: MessageType, at _: IndexPath ) -> NSAttributedString? { message.fullModel.bottomString?.string } - + nonisolated func cellTopLabelAttributedText( for message: MessageType, at indexPath: IndexPath ) -> NSAttributedString? { message.fullModel.dateHeader?.string } - + nonisolated func textCell( for message: MessageType, at indexPath: IndexPath, @@ -78,15 +78,15 @@ final class ChatDataSourceManager: MessagesDataSource { ChatMessageCell.self, for: indexPath ) - + let publisher: any Observable = viewModel.$messages.compactMap { let message = $0[safe: indexPath.section] guard case let .message(model) = message?.fullModel.content else { return nil } - + return model.value } - + cell.actionHandler = { [weak self] in self?.handleAction($0) } cell.chatMessagesListViewModel = viewModel.chatMessagesListViewModel cell.model = model.value @@ -94,21 +94,21 @@ final class ChatDataSourceManager: MessagesDataSource { cell.setSubscription(publisher: publisher, collection: messagesCollectionView) return cell } - + if case let .reply(model) = message.fullModel.content { let cell = messagesCollectionView.dequeueReusableCell( ChatMessageReplyCell.self, for: indexPath ) - + let publisher: any Observable = viewModel.$messages.compactMap { let message = $0[safe: indexPath.section] guard case let .reply(model) = message?.fullModel.content else { return nil } - + return model.value } - + cell.actionHandler = { [weak self] in self?.handleAction($0) } cell.chatMessagesListViewModel = viewModel.chatMessagesListViewModel cell.model = model.value @@ -116,11 +116,11 @@ final class ChatDataSourceManager: MessagesDataSource { cell.setSubscription(publisher: publisher, collection: messagesCollectionView) return cell } - + return UICollectionViewCell() } } - + nonisolated func customCell( for message: MessageType, at indexPath: IndexPath, @@ -132,15 +132,15 @@ final class ChatDataSourceManager: MessagesDataSource { ChatTransactionCell.self, for: indexPath ) - + let publisher: any Observable = viewModel.$messages.compactMap { let message = $0[safe: indexPath.section] guard case let .transaction(model) = message?.fullModel.content else { return nil } - + return model.value } - + cell.actionHandler = { [weak self] in self?.handleAction($0) } cell.chatMessagesListViewModel = viewModel.chatMessagesListViewModel cell.model = model.value @@ -148,21 +148,21 @@ final class ChatDataSourceManager: MessagesDataSource { cell.configure(with: message, at: indexPath, and: messagesCollectionView) return cell } - + if case let .file(model) = message.fullModel.content { let cell = messagesCollectionView.dequeueReusableCell( ChatMediaCell.self, for: indexPath ) - + let publisher: any Observable = viewModel.$messages.compactMap { let message = $0[safe: indexPath.section] guard case let .file(model) = message?.fullModel.content else { return nil } - + return model.value } - + cell.actionHandler = { [weak self] in self?.handleAction($0) } cell.chatMessagesListViewModel = viewModel.chatMessagesListViewModel cell.model = model.value @@ -170,14 +170,14 @@ final class ChatDataSourceManager: MessagesDataSource { cell.configure(with: message, at: indexPath, and: messagesCollectionView) return cell } - + return UICollectionViewCell() } } } -private extension ChatDataSourceManager { - func handleAction(_ action: ChatAction) { +extension ChatDataSourceManager { + fileprivate func handleAction(_ action: ChatAction) { switch action { case let .openTransactionDetails(id): viewModel.didTapTransfer.send(id) @@ -186,7 +186,7 @@ private extension ChatDataSourceManager { case let .reply(id): viewModel.replyMessageIfNeeded(id: id) case let .scrollTo(message): - viewModel.scroll(to: message) + viewModel.scroll(to: message.replyId) case let .copy(text): viewModel.copyMessageAction(text) case let .remove(id): @@ -197,10 +197,12 @@ private extension ChatDataSourceManager { viewModel.reactAction(id, emoji: emoji) case let .presentMenu(arg): viewModel.presentMenu(arg: arg) - case .copyInPart(text: let text): + case .copyInPart(let text): viewModel.copyTextInPartAction(text) case let .openFile(messageId, file): viewModel.openFile(messageId: messageId, file: file) + case let .cancelUploading(messageId, file): + viewModel.cancelFileUploading(messageId: messageId, file: file) case let .autoDownloadContentIfNeeded(messageId, files): viewModel.autoDownloadContentIfNeeded( messageId: messageId, diff --git a/Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift b/Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift index 2d3fcc368..6e0ae5f94 100644 --- a/Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift +++ b/Adamant/Modules/Chat/View/Managers/ChatDialogManager.swift @@ -6,53 +6,58 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit +import AdvancedContextMenuKit import Combine -import SafariServices import CommonKit -import AdvancedContextMenuKit -import SwiftUI import ElegantEmojiPicker +import SafariServices +import SwiftUI +import UIKit @MainActor final class ChatDialogManager { private let viewModel: ChatViewModel private let dialogService: DialogService private let emojiService: EmojiService? - + private let accountService: AccountService + private var subscription: AnyCancellable? private lazy var contextMenu = AdvancedContextMenuManager() - + typealias DidSelectEmojiAction = ((_ emoji: String, _ messageId: String) -> Void)? typealias ContextMenuAction = ((_ messageId: String) -> Void)? - + init( viewModel: ChatViewModel, dialogService: DialogService, - emojiService: EmojiService + emojiService: EmojiService, + accountService: AccountService ) { self.viewModel = viewModel self.dialogService = dialogService self.emojiService = emojiService + self.accountService = accountService subscription = viewModel.dialog.sink { [weak self] in self?.showDialog($0) } } } -private extension ChatDialogManager { - var address: String? { +extension ChatDialogManager { + fileprivate var address: String? { viewModel.chatroom?.partner?.address } - - var encodedAddress: String? { + + fileprivate var encodedAddress: String? { guard let partner = viewModel.chatroom?.partner, let address = address else { return nil } - - return AdamantUriTools.encode(request: AdamantUri.address( - address: address, - params: partner.name.map { [.label($0)] } - )) - } - - func showDialog(_ dialog: ChatDialog) { + + return AdamantUriTools.encode( + request: AdamantUri.address( + address: address, + params: partner.name.map { [.label($0)] } + ) + ) + } + + fileprivate func showDialog(_ dialog: ChatDialog) { switch dialog { case let .toast(message): dialogService.showToastMessage(message) @@ -72,6 +77,10 @@ private extension ChatDialogManager { showMenu(sender: sender) case .freeTokenAlert: showFreeTokenAlert() + case .noActiveNodesAlert: + showNoActiveNodesAlert() + case .timestampIsInTheFuture: + showTimestampAlert() case let .removeMessageAlert(id): showRemoveMessageAlert(id: id) case let .reportMessageAlert(id): @@ -110,8 +119,8 @@ private extension ChatDialogManager { showActionMenu() } } - - func showAlert(message: String) { + + fileprivate func showAlert(message: String) { dialogService.showAlert( title: nil, message: message, @@ -120,11 +129,11 @@ private extension ChatDialogManager { from: nil ) } - - func showMenu(sender: UIBarButtonItem) { + + fileprivate func showMenu(sender: UIBarButtonItem) { guard let partner = viewModel.chatroom?.partner else { return } guard !partner.isSystem else { return showSystemPartnerMenu(sender: sender) } - + dialogService.showAlert( title: nil, message: nil, @@ -138,12 +147,12 @@ private extension ChatDialogManager { from: .barButtonItem(sender) ) } - - func showActionMenu() { + + fileprivate func showActionMenu() { let didSelect: ((ShareType) -> Void)? = { [weak self] type in self?.viewModel.didSelectMenuAction(type) } - + dialogService.presentShareAlertFor( string: .empty, types: [ @@ -158,18 +167,18 @@ private extension ChatDialogManager { didSelect: didSelect ) } - - func showSystemPartnerMenu(sender: UIBarButtonItem) { + + fileprivate func showSystemPartnerMenu(sender: UIBarButtonItem) { guard let address = address else { return } - + let didSelect: ((ShareType) -> Void)? = { [weak self] type in guard case .partnerQR = type, - let partner = self?.viewModel.chatroom?.partner + let partner = self?.viewModel.chatroom?.partner else { return } - + self?.viewModel.didTapPartnerQR.send(partner) } - + dialogService.presentShareAlertFor( string: address, types: [ @@ -184,22 +193,60 @@ private extension ChatDialogManager { didSelect: didSelect ) } - - func showFreeTokenAlert() { + + fileprivate func showNoActiveNodesAlert() { let alert = UIAlertController( - title: "", - message: String.adamant.chat.freeTokensMessage, + title: .adamant.chat.noActiveNodesTitle, + message: .adamant.chat.noActiveNodes, preferredStyleSafe: .alert, source: nil ) - - alert.addAction(makeFreeTokensAlertAction()) - alert.addAction(makeCancelAction()) + + alert.addAction( + .init( + title: .adamant.chat.reviewNodesList, + style: .destructive, + handler: { [weak self] _ in + self?.viewModel.didTapAdmNodesList.send(()) + } + ) + ) + let cancelButton = UIAlertAction(title: .adamant.alert.cancel, style: .default) + alert.addAction(cancelButton) alert.modalPresentationStyle = .overFullScreen dialogService.present(alert, animated: true, completion: nil) } - - func showRemoveMessageAlert(id: String) { + + fileprivate func showTimestampAlert() { + dialogService.showAlert( + title: .adamant.chat.timestampIsInTheFutureTitle, + message: .adamant.chat.timestampIsInTheFuture, + style: .alert, + actions: [ + .init( + title: .adamant.chat.timeSettings, + style: .destructive, + handler: { [weak self] _ in + self?.viewModel.didTapShowTimeSettings.send(()) + } + ), + UIAlertAction(title: .adamant.alert.cancel, style: .default) + ], + from: nil + ) + } + + fileprivate func showFreeTokenAlert() { + dialogService.showFreeTokenAlert( + url: accountService.account?.address, + type: .message, + showVC: { [weak self] in + self?.viewModel.showBuyAndSell.send() + } + ) + } + + fileprivate func showRemoveMessageAlert(id: String) { dialogService.showAlert( title: .adamant.chat.removeMessage, message: nil, @@ -215,8 +262,8 @@ private extension ChatDialogManager { from: nil ) } - - func showReportMessageAlert(id: String) { + + fileprivate func showReportMessageAlert(id: String) { dialogService.showAlert( title: .adamant.chat.reportMessage, message: nil, @@ -235,8 +282,8 @@ private extension ChatDialogManager { from: nil ) } - - func showFailedMessageAlert(id: String, sender: UIAlertController.SourceView?) { + + fileprivate func showFailedMessageAlert(id: String, sender: UIAlertController.SourceView?) { dialogService.showAlert( title: .adamant.alert.retryOrDeleteTitle, message: .adamant.alert.retryOrDeleteBody, @@ -249,19 +296,34 @@ private extension ChatDialogManager { from: nil ) } - - func showRenameAlert() { - guard let alert = makeRenameAlert() else { return } + + fileprivate func showRenameAlert() { + guard let address = address else { return } + + let alert = dialogService.makeRenameAlert( + titleFormat: String(format: .adamant.chat.actionsBody, address), + initialText: viewModel.partnerName, + isEnoughMoney: accountService.account?.isEnoughMoneyForTransaction ?? false, + url: accountService.account?.address, + showVC: { [weak self] in + self?.viewModel.showBuyAndSell.send() + }, + onRename: handleRename(newName:) + ) + dialogService.present(alert, animated: true) { [weak self] in self?.dialogService.selectAllTextFields(in: alert) } } + fileprivate func handleRename(newName: String) { + viewModel.setNewName(newName) + } } // MARK: Alert actions -private extension ChatDialogManager { - func makeBlockAction() -> UIAlertAction { +extension ChatDialogManager { + fileprivate func makeBlockAction() -> UIAlertAction { .init( title: .adamant.chat.block, style: .destructive @@ -286,8 +348,8 @@ private extension ChatDialogManager { ) } } - - func makeRenameAction() -> UIAlertAction { + + fileprivate func makeRenameAction() -> UIAlertAction { .init( title: .adamant.chat.rename, style: .default @@ -295,42 +357,8 @@ private extension ChatDialogManager { self?.showRenameAlert() } } - - func makeRenameAlert() -> UIAlertController? { - guard let address = address else { return nil } - - let alert = UIAlertController( - title: .init(format: .adamant.chat.actionsBody, address), - message: nil, - preferredStyleSafe: .alert, - source: nil - ) - - alert.addTextField { [weak viewModel] textField in - textField.placeholder = .adamant.chat.name - textField.autocapitalizationType = .words - textField.text = viewModel?.partnerName - } - - let renameAction = UIAlertAction( - title: .adamant.chat.rename, - style: .default - ) { [weak viewModel] _ in - guard - let textField = alert.textFields?.first, - let newName = textField.text - else { return } - - viewModel?.setNewName(newName) - } - - alert.addAction(renameAction) - alert.addAction(makeCancelAction()) - alert.modalPresentationStyle = .overFullScreen - return alert - } - - func makeShareAction(sender: UIBarButtonItem) -> UIAlertAction { + + fileprivate func makeShareAction(sender: UIBarButtonItem) -> UIAlertAction { .init( title: ShareType.share.localized, style: .default @@ -339,15 +367,15 @@ private extension ChatDialogManager { let self = self, let address = self.address else { return } - + let didSelect: ((ShareType) -> Void)? = { [weak self] type in guard case .partnerQR = type, - let partner = self?.viewModel.chatroom?.partner + let partner = self?.viewModel.chatroom?.partner else { return } - + self?.viewModel.didTapPartnerQR.send(partner) } - + self.dialogService.presentShareAlertFor( string: address, types: [ @@ -363,26 +391,13 @@ private extension ChatDialogManager { ) } } - - func makeFreeTokensAlertAction() -> UIAlertAction { - .init( - title: String.adamant.chat.freeTokens, - style: .default - ) { [weak self] _ in - guard let self = self, let url = self.viewModel.freeTokensURL else { return } - let safari = SFSafariViewController(url: url) - safari.preferredControlTintColor = UIColor.adamant.primary - safari.modalPresentationStyle = .overFullScreen - self.dialogService.present(safari, animated: true, completion: nil) - } - } - - func showAdmMenuAction(_ adm: AdamantAddress, partnerAddress: String) { + + fileprivate func showAdmMenuAction(_ adm: AdamantAddress, partnerAddress: String) { let shareTypes: [AddressChatShareType] = adm.address == partnerAddress ? [.send] : [.chat, .send] let name = adm.name ?? adm.address - + let kvsName = viewModel.getKvsName(for: adm.address) - + self.dialogService.presentShareAlertFor( adm: adm.address, name: kvsName ?? name, @@ -397,13 +412,13 @@ private extension ChatDialogManager { self.dialogService.showToastMessage(String.adamant.newChat.specifyValidAddressMessage) return } - + self.viewModel.process(adm: adm, action: action) } } } - - func showDummyAlert(for address: String) { + + fileprivate func showDummyAlert(for address: String) { dialogService.presentDummyChatAlert( for: address, from: nil, @@ -411,8 +426,8 @@ private extension ChatDialogManager { sendCompletion: nil ) } - - func showUrl(_ url: URL) { + + fileprivate func showUrl(_ url: URL) { if url.absoluteString.starts(with: "http") { let safari = SFSafariViewController(url: url) safari.preferredControlTintColor = UIColor.adamant.primary @@ -432,46 +447,46 @@ private extension ChatDialogManager { } } } - - func makeRetryAction(id: String) -> UIAlertAction { + + fileprivate func makeRetryAction(id: String) -> UIAlertAction { .init(title: .adamant.alert.retry, style: .default) { [weak viewModel] _ in viewModel?.retrySendMessage(id: id) } } - - func makeCancelSendingAction(id: String) -> UIAlertAction { + + fileprivate func makeCancelSendingAction(id: String) -> UIAlertAction { .init(title: .adamant.alert.delete, style: .default) { [weak viewModel] _ in viewModel?.cancelMessage(id: id) } } - - func setProgress(_ show: Bool) { + + fileprivate func setProgress(_ show: Bool) { if show { dialogService.showProgress(withMessage: nil, userInteractionEnable: false) } else { dialogService.dismissProgress() } } - - func makeCancelAction() -> UIAlertAction { + + fileprivate func makeCancelAction() -> UIAlertAction { .init(title: .adamant.alert.cancel, style: .cancel, handler: nil) } - - func makeCancelAction() -> AdamantAlertAction { + + fileprivate func makeCancelAction() -> AdamantAlertAction { .init(title: .adamant.alert.cancel, style: .cancel, handler: nil) } } // MARK: Context Menu -private extension ChatDialogManager { - func dismissMenu() { +extension ChatDialogManager { + fileprivate func dismissMenu() { Task { await contextMenu.dismiss() } } - - func presentMenu( + + fileprivate func presentMenu( presentReactions: Bool, arg: ChatContextMenuArguments, didSelectEmojiDelegate: ElegantEmojiPickerDelegate?, @@ -481,32 +496,34 @@ private extension ChatDialogManager { ) { contextMenu.didPresentMenuAction = didPresentMenuAction contextMenu.didDismissMenuAction = didDismissMenuAction - - let reactionsContentView = !presentReactions - ? nil - : getUpperContentView( - messageId: arg.messageId, - selectedEmoji: arg.selectedEmoji, - didSelectEmojiAction: didSelectEmojiAction, - didSelectEmojiDelegate: didSelectEmojiDelegate - ) - - let reactionsContentViewSize: CGSize = !presentReactions - ? .zero - : getUpperContentViewSize() - + + let reactionsContentView = + !presentReactions + ? nil + : getUpperContentView( + messageId: arg.messageId, + selectedEmoji: arg.selectedEmoji, + didSelectEmojiAction: didSelectEmojiAction, + didSelectEmojiDelegate: didSelectEmojiDelegate + ) + + let reactionsContentViewSize: CGSize = + !presentReactions + ? .zero + : getUpperContentViewSize() + contextMenu.presentMenu( arg: arg, upperView: reactionsContentView, upperViewSize: reactionsContentViewSize ) } - - func getUpperContentViewSize() -> CGSize { + + fileprivate func getUpperContentViewSize() -> CGSize { .init(width: 335, height: 50) } - - func getUpperContentView( + + fileprivate func getUpperContentView( messageId: String, selectedEmoji: String?, didSelectEmojiAction: DidSelectEmojiAction, @@ -532,16 +549,16 @@ private extension ChatDialogManager { } return AnyView(view) } - - func getFrequentlySelectedEmojis(selectedEmoji: String?) -> [String]? { + + fileprivate func getFrequentlySelectedEmojis(selectedEmoji: String?) -> [String]? { var emojis = emojiService?.getFrequentlySelectedEmojis() guard let selectedEmoji = selectedEmoji else { return emojis } - + if let index = emojis?.firstIndex(of: selectedEmoji) { emojis?.remove(at: index) } emojis?.insert(selectedEmoji, at: 0) - + return emojis } } diff --git a/Adamant/Modules/Chat/View/Managers/ChatDisplayManager.swift b/Adamant/Modules/Chat/View/Managers/ChatDisplayManager.swift index 0d8309fe7..c3d81d7ac 100644 --- a/Adamant/Modules/Chat/View/Managers/ChatDisplayManager.swift +++ b/Adamant/Modules/Chat/View/Managers/ChatDisplayManager.swift @@ -6,18 +6,18 @@ // Copyright © 2022 Adamant. All rights reserved. // +import Combine @preconcurrency import MessageKit import UIKit -import Combine @MainActor final class ChatDisplayManager: MessagesDisplayDelegate { private let viewModel: ChatViewModel - + init(viewModel: ChatViewModel) { self.viewModel = viewModel } - + nonisolated func messageStyle( for message: MessageType, at _: IndexPath, @@ -32,7 +32,7 @@ final class ChatDisplayManager: MessagesDisplayDelegate { ) } } - + nonisolated func backgroundColor( for message: MessageType, at _: IndexPath, @@ -40,13 +40,13 @@ final class ChatDisplayManager: MessagesDisplayDelegate { ) -> UIColor { message.fullModel.backgroundColor.uiColor } - + nonisolated func textColor( for _: MessageType, at _: IndexPath, in _: MessagesCollectionView ) -> UIColor { .adamant.primary } - + nonisolated func messageHeaderView( for indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView @@ -56,17 +56,34 @@ final class ChatDisplayManager: MessagesDisplayDelegate { ChatViewController.SpinnerCell.self, for: indexPath ) - + if viewModel.messages[indexPath.section].topSpinnerOn { header.wrappedView.startAnimating() } else { header.wrappedView.stopAnimating() } - + return header } } - + + nonisolated func messageFooterView( + for indexPath: IndexPath, + in messagesCollectionView: MessagesCollectionView + ) -> MessageReusableView { + DispatchQueue.onMainThreadSyncSafe { + guard let separatorIndex = viewModel.separatorIndex, indexPath.section == separatorIndex else { + return MessageReusableView() + } + + let footer = messagesCollectionView.dequeueReusableFooterView( + NewMessagesCell.self, + for: indexPath + ) + return footer + } + } + nonisolated func enabledDetectors( for _: MessageType, at _: IndexPath, @@ -74,7 +91,7 @@ final class ChatDisplayManager: MessagesDisplayDelegate { ) -> [DetectorType] { return [.url] } - + nonisolated func detectorAttributes( for detector: DetectorType, and _: MessageType, @@ -84,7 +101,7 @@ final class ChatDisplayManager: MessagesDisplayDelegate { ? [.foregroundColor: UIColor.adamant.active] : [:] } - + nonisolated func configureAccessoryView( _ accessoryView: UIView, for message: MessageType, @@ -95,11 +112,11 @@ final class ChatDisplayManager: MessagesDisplayDelegate { switch message.fullModel.status { case .failed: guard accessoryView.subviews.isEmpty else { break } - + if case .file = message.fullModel.content { break } - + let icon = UIImageView(frame: CGRect(x: -28, y: -10, width: 20, height: 20)) icon.contentMode = .scaleAspectFit icon.tintColor = .adamant.secondary diff --git a/Adamant/Modules/Chat/View/Managers/ChatInputBarManager.swift b/Adamant/Modules/Chat/View/Managers/ChatInputBarManager.swift index e07067248..ba5d0b6a0 100644 --- a/Adamant/Modules/Chat/View/Managers/ChatInputBarManager.swift +++ b/Adamant/Modules/Chat/View/Managers/ChatInputBarManager.swift @@ -6,17 +6,17 @@ // Copyright © 2022 Adamant. All rights reserved. // -import InputBarAccessoryView import Foundation +import InputBarAccessoryView @MainActor final class ChatInputBarManager: InputBarAccessoryViewDelegate { private let viewModel: ChatViewModel - + init(viewModel: ChatViewModel) { self.viewModel = viewModel } - + nonisolated func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith text: String) { Task { @MainActor in guard await viewModel.canSendMessage(withText: text) else { return } diff --git a/Adamant/Modules/Chat/View/Managers/ChatKeyboardManager.swift b/Adamant/Modules/Chat/View/Managers/ChatKeyboardManager.swift index 6becde0b6..b78499d30 100644 --- a/Adamant/Modules/Chat/View/Managers/ChatKeyboardManager.swift +++ b/Adamant/Modules/Chat/View/Managers/ChatKeyboardManager.swift @@ -11,17 +11,17 @@ import UIKit final class ChatKeyboardManager: NSObject, UIGestureRecognizerDelegate { private let scrollView: UIScrollView var panGesture: UIPanGestureRecognizer? - + init(scrollView: UIScrollView) { self.scrollView = scrollView super.init() } - + /// Only receive a `UITouch` event when the `scrollView`'s keyboard dismiss mode is interactive func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { return scrollView.keyboardDismissMode == .interactive } - + /// Only recognice gestures when is vertical velocity func gestureRecognizerShouldBegin( _ gestureRecognizer: UIGestureRecognizer @@ -29,11 +29,11 @@ final class ChatKeyboardManager: NSObject, UIGestureRecognizerDelegate { guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else { return true } - + let velocity = panGesture.velocity(in: scrollView) return abs(velocity.x) < abs(velocity.y) } - + /// Only recognice simultaneous gestures when its the `panGesture` func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return gestureRecognizer === panGesture diff --git a/Adamant/Modules/Chat/View/Managers/ChatLayoutManager.swift b/Adamant/Modules/Chat/View/Managers/ChatLayoutManager.swift index 3711cdeb6..4be4f7d47 100644 --- a/Adamant/Modules/Chat/View/Managers/ChatLayoutManager.swift +++ b/Adamant/Modules/Chat/View/Managers/ChatLayoutManager.swift @@ -6,24 +6,24 @@ // Copyright © 2022 Adamant. All rights reserved. // +import Combine @preconcurrency import MessageKit import UIKit -import Combine @MainActor final class ChatLayoutManager: MessagesLayoutDelegate { private let viewModel: ChatViewModel - + init(viewModel: ChatViewModel) { self.viewModel = viewModel } - + nonisolated func avatarSize( for _: MessageType, at _: IndexPath, in _: MessagesCollectionView ) -> CGSize? { .zero } - + nonisolated func cellTopLabelHeight( for message: MessageType, at indexPath: IndexPath, @@ -33,7 +33,7 @@ final class ChatLayoutManager: MessagesLayoutDelegate { ? .zero : labelHeight } - + nonisolated func messageTopLabelHeight( for message: MessageType, at _: IndexPath, @@ -43,7 +43,7 @@ final class ChatLayoutManager: MessagesLayoutDelegate { ? labelHeight : .zero } - + nonisolated func messageBottomLabelHeight( for message: MessageType, at _: IndexPath, @@ -53,7 +53,7 @@ final class ChatLayoutManager: MessagesLayoutDelegate { ? .zero : labelHeight } - + nonisolated func messageTopLabelAlignment( for message: MessageType, at _: IndexPath, @@ -66,7 +66,7 @@ final class ChatLayoutManager: MessagesLayoutDelegate { ) } } - + nonisolated func messageBottomLabelAlignment( for message: MessageType, at _: IndexPath, @@ -79,7 +79,7 @@ final class ChatLayoutManager: MessagesLayoutDelegate { ) } } - + nonisolated func textCellSizeCalculator( for _: MessageType, at _: IndexPath, @@ -93,7 +93,7 @@ final class ChatLayoutManager: MessagesLayoutDelegate { ) } } - + nonisolated func customCellSizeCalculator( for _: MessageType, at _: IndexPath, @@ -107,7 +107,7 @@ final class ChatLayoutManager: MessagesLayoutDelegate { ) } } - + nonisolated func headerViewSize( for section: Int, in messagesCollectionView: MessagesCollectionView @@ -118,7 +118,19 @@ final class ChatLayoutManager: MessagesLayoutDelegate { : .zero } } - + + nonisolated func footerViewSize( + for section: Int, + in messagesCollectionView: MessagesCollectionView + ) -> CGSize { + MainActor.assumeIsolatedSafe { + guard let separatorIndex = viewModel.separatorIndex, section == separatorIndex else { + return .zero + } + return CGSize(width: messagesCollectionView.bounds.width, height: 25) + } + } + nonisolated func attributedTextCellSizeCalculator( for message: MessageType, at indexPath: IndexPath, @@ -134,8 +146,8 @@ final class ChatLayoutManager: MessagesLayoutDelegate { } } -private extension ChatLayoutManager { - func textAlignment(for message: MessageType) -> NSTextAlignment { +extension ChatLayoutManager { + fileprivate func textAlignment(for message: MessageType) -> NSTextAlignment { message.sender.senderId == viewModel.sender.senderId ? .right : .left diff --git a/Adamant/Modules/Chat/View/Managers/ChatMenuManager.swift b/Adamant/Modules/Chat/View/Managers/ChatMenuManager.swift index c52b878ec..4384bb0f3 100644 --- a/Adamant/Modules/Chat/View/Managers/ChatMenuManager.swift +++ b/Adamant/Modules/Chat/View/Managers/ChatMenuManager.swift @@ -6,10 +6,10 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit -import SwiftUI import AdvancedContextMenuKit import CommonKit +import SwiftUI +import UIKit @MainActor protocol ChatMenuManagerDelegate: AnyObject { @@ -26,20 +26,20 @@ protocol ChatMenuManagerDelegate: AnyObject { @MainActor final class ChatMenuManager: NSObject { weak var delegate: ChatMenuManagerDelegate? - + // MARK: Init - + init(delegate: ChatMenuManagerDelegate?) { self.delegate = delegate } - - func setup(for contentView: UIView ) { + + func setup(for contentView: UIView) { guard !isMacOS else { let interaction = UIContextMenuInteraction(delegate: self) contentView.addInteraction(interaction) return } - + let longPressGesture = UILongPressGestureRecognizer( target: self, action: #selector(handleLongPress(_:)) @@ -47,18 +47,18 @@ final class ChatMenuManager: NSObject { longPressGesture.minimumPressDuration = 0.17 contentView.addGestureRecognizer(longPressGesture) } - + func presentMenuProgrammatically(for contentView: UIView) { let locationOnScreen = contentView.convert(CGPoint.zero, to: nil) - + let size = contentView.frame.size - + let copyView = delegate?.getCopyView() ?? contentView - + let getPositionOnScreen: () -> CGPoint = { [weak contentView] in contentView?.convert(CGPoint.zero, to: nil) ?? .zero } - + delegate?.presentMenu( copyView: copyView, size: size, @@ -70,24 +70,24 @@ final class ChatMenuManager: NSObject { getPositionOnScreen: getPositionOnScreen ) } - + @objc func handleLongPress(_ gesture: UILongPressGestureRecognizer) { guard !isMacOS else { return } - + guard gesture.state == .began, - let contentView = gesture.view + let contentView = gesture.view else { return } - + let locationOnScreen = contentView.convert(CGPoint.zero, to: nil) - + let size = contentView.frame.size - + let copyView = delegate?.getCopyView() ?? contentView - + let getPositionOnScreen: () -> CGPoint = { contentView.convert(CGPoint.zero, to: nil) } - + delegate?.presentMenu( copyView: copyView, size: size, @@ -106,27 +106,27 @@ extension ChatMenuManager: UIContextMenuInteractionDelegate { presentMacOverlay(interaction, configurationForMenuAtLocation: location) return nil } - + func presentMacOverlay( _ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint ) { guard let contentView = interaction.view else { return } - + let contentLocation = contentView.convert(CGPoint.zero, to: nil) let tapLocation: CGPoint = .init( x: contentLocation.x + location.x, y: contentLocation.y + location.y ) let size = contentView.frame.size - + let copyView = delegate?.getCopyView() ?? contentView - + let getPositionOnScreen: () -> CGPoint = { contentView.convert(CGPoint.zero, to: nil) } - + delegate?.presentMenu( copyView: copyView, size: size, diff --git a/Adamant/Modules/Chat/View/Managers/ChatSwipeManager.swift b/Adamant/Modules/Chat/View/Managers/ChatSwipeManager.swift index 9f959b898..c23ae3eb5 100644 --- a/Adamant/Modules/Chat/View/Managers/ChatSwipeManager.swift +++ b/Adamant/Modules/Chat/View/Managers/ChatSwipeManager.swift @@ -13,22 +13,22 @@ final class ChatSwipeManager: NSObject { private let viewModel: ChatViewModel private var chatView: UIView? private var vibrated = false - + private var requiredSwipeOffset: CGFloat { -UIScreen.main.bounds.size.width * 0.05 } - + init(viewModel: ChatViewModel) { self.viewModel = viewModel super.init() } - + func configure(chatView: UIView) { let recognizer = UIPanGestureRecognizer( target: self, action: #selector(onSwipe(_:)) ) - + recognizer.delegate = self chatView.addGestureRecognizer(recognizer) self.chatView = chatView @@ -42,27 +42,27 @@ extension ChatSwipeManager: UIGestureRecognizerDelegate { guard let recognizer = recognizer as? UIPanGestureRecognizer else { return false } - + let velocity = recognizer.velocity(in: chatView) guard abs(velocity.x) > abs(velocity.y) else { return false } - + let location = recognizer.location(in: chatView) guard let id = findChatSwipeWrapperId(location) else { return false } - + viewModel.updateSwipeableId(id) return true } - + func gestureRecognizer( _: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith _: UIGestureRecognizer ) -> Bool { true } } -private extension ChatSwipeManager { - func findChatSwipeWrapperId(_ location: CGPoint) -> String? { +extension ChatSwipeManager { + fileprivate func findChatSwipeWrapperId(_ location: CGPoint) -> String? { var view = chatView?.hitTest(location, with: nil) - + while view != nil { if let swipeWrapper = view as? ChatSwipeWrapper { return swipeWrapper.model.id @@ -70,16 +70,17 @@ private extension ChatSwipeManager { view = view?.superview } } - + return nil } - - @objc func onSwipe(_ recognizer: UIPanGestureRecognizer) { + + @objc fileprivate func onSwipe(_ recognizer: UIPanGestureRecognizer) { let translation = recognizer.translation(in: chatView) - let offset = translation.x <= .zero + let offset = + translation.x <= .zero ? translation.x : .zero - + switch recognizer.state { case .possible: break @@ -88,11 +89,11 @@ private extension ChatSwipeManager { viewModel.enableScroll.send(false) case .changed: viewModel.updateSwipingOffset(offset) - + if offset > requiredSwipeOffset { vibrated = false } - + guard !vibrated, offset <= requiredSwipeOffset else { break } UIImpactFeedbackGenerator(style: .heavy).impactOccurred() vibrated = true @@ -100,7 +101,7 @@ private extension ChatSwipeManager { if offset <= requiredSwipeOffset { viewModel.replyMessageIfNeeded(id: viewModel.swipeableMessage.id) } - + viewModel.updateSwipeableId(nil) viewModel.enableScroll.send(true) @unknown default: diff --git a/Adamant/Modules/Chat/View/Managers/FixedTextMessageSizeCalculator.swift b/Adamant/Modules/Chat/View/Managers/FixedTextMessageSizeCalculator.swift index 580d54afd..3ec1e45bb 100644 --- a/Adamant/Modules/Chat/View/Managers/FixedTextMessageSizeCalculator.swift +++ b/Adamant/Modules/Chat/View/Managers/FixedTextMessageSizeCalculator.swift @@ -1,198 +1,202 @@ // - // FixedTextMessageSizeCalculator.swift - // Adamant - // - // Created by Andrey Golubenko on 19.04.2023. - // Copyright © 2023 Adamant. All rights reserved. - // +// FixedTextMessageSizeCalculator.swift +// Adamant +// +// Created by Andrey Golubenko on 19.04.2023. +// Copyright © 2023 Adamant. All rights reserved. +// -import UIKit @preconcurrency import MessageKit +import UIKit final class FixedTextMessageSizeCalculator: MessageSizeCalculator, @unchecked Sendable { - private let getCurrentSender: () -> SenderType - private let getMessages: () -> [ChatMessage] - private let messagesFlowLayout: MessagesCollectionViewFlowLayout - - init( - layout: MessagesCollectionViewFlowLayout, - getCurrentSender: @escaping () -> SenderType, - getMessages: @escaping () -> [ChatMessage] - ) { - self.getMessages = getMessages - self.getCurrentSender = getCurrentSender - self.messagesFlowLayout = layout - super.init() - self.layout = layout - } - - override func messageContainerMaxWidth( - for message: MessageType, - at indexPath: IndexPath - ) -> CGFloat { - let maxWidth = super.messageContainerMaxWidth(for: message, at: indexPath) - let textInsets = messageLabelInsets(for: message) - return maxWidth - textInsets.horizontal - } - - override func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { - MainActor.assumeIsolatedSafe { - let maxWidth = messageContainerMaxWidth(for: message, at: indexPath) - - var messageContainerSize: CGSize = .zero - let messageInsets = messageLabelInsets(for: message) - - if case let .message(model) = getMessages()[indexPath.section].fullModel.content { - messageContainerSize = labelSize(for: model.value.text, considering: maxWidth) - messageContainerSize.width += messageInsets.horizontal - messageContainerSize.height += messageInsets.vertical - } - - if case let .reply(model) = getMessages()[indexPath.section].fullModel.content { - let messageSize = labelSize( + private let getCurrentSender: () -> SenderType + private let getMessages: () -> [ChatMessage] + private let messagesFlowLayout: MessagesCollectionViewFlowLayout + + init( + layout: MessagesCollectionViewFlowLayout, + getCurrentSender: @escaping () -> SenderType, + getMessages: @escaping () -> [ChatMessage] + ) { + self.getMessages = getMessages + self.getCurrentSender = getCurrentSender + self.messagesFlowLayout = layout + super.init() + self.layout = layout + } + + override func messageContainerMaxWidth( + for message: MessageType, + at indexPath: IndexPath + ) -> CGFloat { + let maxWidth = super.messageContainerMaxWidth(for: message, at: indexPath) + let textInsets = messageLabelInsets(for: message) + return maxWidth - textInsets.horizontal + } + + override func messageContainerSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + MainActor.assumeIsolatedSafe { + let maxWidth = messageContainerMaxWidth(for: message, at: indexPath) + + var messageContainerSize: CGSize = .zero + let messageInsets = messageLabelInsets(for: message) + + if case let .message(model) = getMessages()[indexPath.section].fullModel.content { + messageContainerSize = labelSize(for: model.value.text, considering: maxWidth) + messageContainerSize.width += messageInsets.horizontal + messageContainerSize.height += messageInsets.vertical + } + + if case let .reply(model) = getMessages()[indexPath.section].fullModel.content { + let messageSize = labelSize( for: model.value.message, considering: maxWidth - ) - - let messageReplySize = labelSize( + ) + + let messageReplySize = labelSize( for: model.value.messageReply, considering: maxWidth - ) - - let size = messageSize.width > messageReplySize.width - ? messageSize - : messageReplySize - - let contentViewHeight = model.value.contentHeight( + ) + + let size = + messageSize.width > messageReplySize.width + ? messageSize + : messageReplySize + + let contentViewHeight = model.value.contentHeight( for: size.width + messageInsets.horizontal / 2 - ) - - messageContainerSize = size - messageContainerSize.width += messageInsets.horizontal - + additionalWidth - messageContainerSize.height = contentViewHeight - } - - if case let .transaction(model) = getMessages()[indexPath.section].fullModel.content { - let contentViewHeight = model.value.height(for: maxWidth) - messageContainerSize.width = maxWidth - messageContainerSize.height = contentViewHeight - + messageInsets.vertical - + additionalHeight - } - - if case let .file(model) = getMessages()[indexPath.section].fullModel.content { - let messageSize = labelSize( + ) + + messageContainerSize = size + messageContainerSize.width += + messageInsets.horizontal + + additionalWidth + messageContainerSize.height = contentViewHeight + } + + if case let .transaction(model) = getMessages()[indexPath.section].fullModel.content { + let contentViewHeight = model.value.height(for: maxWidth) + messageContainerSize.width = maxWidth + messageContainerSize.height = + contentViewHeight + + messageInsets.vertical + + additionalHeight + } + + if case let .file(model) = getMessages()[indexPath.section].fullModel.content { + let messageSize = labelSize( for: model.value.content.comment, considering: model.value.content.width() - - commenthorizontalInsets * 2 - ) - - let contentViewHeight = model.value.height() - messageContainerSize.width = maxWidth - messageContainerSize.height = contentViewHeight - + messageSize.height - } - - return messageContainerSize - } - } - - override func configure(attributes: UICollectionViewLayoutAttributes) { - super.configure(attributes: attributes) - MainActor.assumeIsolatedSafe { - guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return } - - let dataSource = messagesLayout.messagesDataSource - let indexPath = attributes.indexPath - - let message = dataSource.messageForItem( - at: indexPath, - in: messagesLayout.messagesCollectionView - ) - - attributes.messageLabelInsets = messageLabelInsets(for: message) - attributes.messageLabelFont = messageLabelFont - - switch message.kind { - case .attributedText(let text): - guard - !text.string.isEmpty, - let font = text.attribute(.font, at: 0, effectiveRange: nil) as? UIFont - else { break } - - attributes.messageLabelFont = font - default: - break - } - } - } - } - - private extension FixedTextMessageSizeCalculator { - func messageLabelInsets(for message: MessageType) -> UIEdgeInsets { - MainActor.assumeIsolatedSafe { - let dataSource = messagesLayout.messagesDataSource - let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) - return isFromCurrentSender - ? outgoingMessageLabelInsets - : incomingMessageLabelInsets - } - } - - func labelSize( + - commenthorizontalInsets * 2 + ) + + let contentViewHeight = model.value.height() + messageContainerSize.width = maxWidth + messageContainerSize.height = + contentViewHeight + + messageSize.height + } + + return messageContainerSize + } + } + + override func configure(attributes: UICollectionViewLayoutAttributes) { + super.configure(attributes: attributes) + MainActor.assumeIsolatedSafe { + guard let attributes = attributes as? MessagesCollectionViewLayoutAttributes else { return } + + let dataSource = messagesLayout.messagesDataSource + let indexPath = attributes.indexPath + + let message = dataSource.messageForItem( + at: indexPath, + in: messagesLayout.messagesCollectionView + ) + + attributes.messageLabelInsets = messageLabelInsets(for: message) + attributes.messageLabelFont = messageLabelFont + + switch message.kind { + case .attributedText(let text): + guard + !text.string.isEmpty, + let font = text.attribute(.font, at: 0, effectiveRange: nil) as? UIFont + else { break } + + attributes.messageLabelFont = font + default: + break + } + } + } +} + +extension FixedTextMessageSizeCalculator { + fileprivate func messageLabelInsets(for message: MessageType) -> UIEdgeInsets { + MainActor.assumeIsolatedSafe { + let dataSource = messagesLayout.messagesDataSource + let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) + return isFromCurrentSender + ? outgoingMessageLabelInsets + : incomingMessageLabelInsets + } + } + + fileprivate func labelSize( for attributedText: NSAttributedString, considering maxWidth: CGFloat - ) -> CGSize { - guard !attributedText.string.isEmpty else { return .zero } - - let textContainer = NSTextContainer( + ) -> CGSize { + guard !attributedText.string.isEmpty else { return .zero } + + let textContainer = NSTextContainer( size: CGSize(width: maxWidth, height: .greatestFiniteMagnitude) - ) - textContainer.lineFragmentPadding = .zero - - let layoutManager = NSLayoutManager() - - layoutManager.addTextContainer(textContainer) - - let textStorage = NSTextStorage(attributedString: attributedText) - textStorage.addLayoutManager(layoutManager) - - let rect = layoutManager.usedRect(for: textContainer) - - return rect.integral.size - } - } - - private extension UIEdgeInsets { - var vertical: CGFloat { - top + bottom - } - - var horizontal: CGFloat { - left + right - } - } - - private extension MessageKind { - var textMessageKind: MessageKind { - switch self { - case .linkPreview(let linkItem): - return linkItem.textKind - case .text, .emoji, .attributedText: - return self - default: - assertionFailure("textMessageKind not supported for messageKind: \(self)") - return .text("") - } - } - } - - private let incomingMessageLabelInsets = UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14) - private let outgoingMessageLabelInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18) - private let messageLabelFont = UIFont.preferredFont(forTextStyle: .body) - - /// Additional width to fix incorrect size calculating + ) + textContainer.lineFragmentPadding = .zero + + let layoutManager = NSLayoutManager() + + layoutManager.addTextContainer(textContainer) + + let textStorage = NSTextStorage(attributedString: attributedText) + textStorage.addLayoutManager(layoutManager) + + let rect = layoutManager.usedRect(for: textContainer) + + return rect.integral.size + } +} + +extension UIEdgeInsets { + fileprivate var vertical: CGFloat { + top + bottom + } + + fileprivate var horizontal: CGFloat { + left + right + } +} + +extension MessageKind { + fileprivate var textMessageKind: MessageKind { + switch self { + case .linkPreview(let linkItem): + return linkItem.textKind + case .text, .emoji, .attributedText: + return self + default: + assertionFailure("textMessageKind not supported for messageKind: \(self)") + return .text("") + } + } +} + +private let incomingMessageLabelInsets = UIEdgeInsets(top: 7, left: 18, bottom: 7, right: 14) +private let outgoingMessageLabelInsets = UIEdgeInsets(top: 7, left: 14, bottom: 7, right: 18) +private let messageLabelFont = UIFont.preferredFont(forTextStyle: .body) + +/// Additional width to fix incorrect size calculating private let additionalWidth: CGFloat = 5 private let additionalHeight: CGFloat = 5 private let commenthorizontalInsets: CGFloat = 12 diff --git a/Adamant/Modules/Chat/View/SelectTextView.swift b/Adamant/Modules/Chat/View/SelectTextView.swift index c35ca09c2..60df7bee3 100644 --- a/Adamant/Modules/Chat/View/SelectTextView.swift +++ b/Adamant/Modules/Chat/View/SelectTextView.swift @@ -12,7 +12,7 @@ import SwiftUI struct SelectTextView: View { let text: String @Environment(\.dismiss) private var dismiss - + var body: some View { if #available(iOS 16.0, *) { NavigationStack { @@ -24,13 +24,13 @@ struct SelectTextView: View { } } } - + var contentView: some View { VStack { TextView(text: text) .accentColor(.blue) .padding() - + Spacer() } .navigationBarTitle(String.adamant.chat.selectText, displayMode: .inline) diff --git a/Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell+Model.swift b/Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell+Model.swift index 8a2830a19..550cbab9a 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell+Model.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell+Model.swift @@ -20,7 +20,7 @@ extension ChatMessageCell { let isFake: Bool var isHidden: Bool var swipeState: ChatSwipeWrapperModel.State - + static var `default`: Self { Self( id: "", @@ -35,7 +35,7 @@ extension ChatMessageCell { swipeState: .idle ) } - + func makeReplyContent() -> NSAttributedString { return text } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift b/Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift index 629d396cc..89af6ef45 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatBaseMessage/ChatMessageCell.swift @@ -6,21 +6,21 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit -import SnapKit -import MessageKit -import Combine -import SwiftUI import AdvancedContextMenuKit +import Combine import CommonKit +import MessageKit +import SnapKit +import SwiftUI +import UIKit final class ChatMessageCell: TextMessageCell, ChatModelView { // MARK: Dependencies - + var chatMessagesListViewModel: ChatMessagesListViewModel? - + // MARK: Proprieties - + private lazy var reactionsContanerView: UIStackView = { let stack = UIStackView(arrangedSubviews: [ownReactionLabel, opponentReactionLabel]) stack.distribution = .fillProportionally @@ -28,10 +28,10 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { stack.spacing = 6 return stack }() - + private lazy var swipeWrapper = ChatSwipeWrapper(cellContainerView) private lazy var cellContainerView = UIView() - + private lazy var ownReactionLabel: UILabel = { let label = UILabel() label.text = getReaction(for: model.address) @@ -40,17 +40,17 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { label.textAlignment = .center label.layer.masksToBounds = true label.frame.size = ownReactionSize - + let tapGesture = UITapGestureRecognizer( target: self, action: #selector(tapReactionAction) ) - + label.addGestureRecognizer(tapGesture) label.isUserInteractionEnabled = true return label }() - + private lazy var opponentReactionLabel: UILabel = { let label = UILabel() label.text = getReaction(for: model.opponentAddress) @@ -59,21 +59,21 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { label.backgroundColor = .adamant.pickedReactionBackground label.layer.cornerRadius = opponentReactionSize.height / 2 label.frame.size = opponentReactionSize - + let tapGesture = UITapGestureRecognizer( target: self, action: #selector(tapReactionAction) ) - + label.addGestureRecognizer(tapGesture) label.isUserInteractionEnabled = true return label }() - + private lazy var chatMenuManager = ChatMenuManager(delegate: self) - + // MARK: - Properties - + var model: Model = .default { didSet { guard model != oldValue else { return } @@ -87,29 +87,27 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { layoutReactionLabel() } } - + var reactionsContanerViewWidth: CGFloat { - if getReaction(for: model.address) == nil && - getReaction(for: model.opponentAddress) == nil { + if getReaction(for: model.address) == nil && getReaction(for: model.opponentAddress) == nil { return .zero } - - if getReaction(for: model.address) != nil && - getReaction(for: model.opponentAddress) != nil { + + if getReaction(for: model.address) != nil && getReaction(for: model.opponentAddress) != nil { return ownReactionSize.width + opponentReactionSize.width + 6 } - + if getReaction(for: model.address) != nil { return ownReactionSize.width } - + if getReaction(for: model.opponentAddress) != nil { return opponentReactionSize.width } - + return .zero } - + override var isSelected: Bool { didSet { messageContainerView.animateIsSelected( @@ -118,24 +116,24 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { ) } } - + var actionHandler: (ChatAction) -> Void = { _ in } var subscription: AnyCancellable? - + private var containerView: UIView = UIView() private let ownReactionSize = CGSize(width: 40, height: 27) private let opponentReactionSize = CGSize(width: 55, height: 27) private let opponentReactionImageSize = CGSize(width: 12, height: 12) private var layoutAttributes: MessagesCollectionViewLayoutAttributes? - + // MARK: - Methods - + override func prepareForReuse() { super.prepareForReuse() ownReactionLabel.text = nil opponentReactionLabel.attributedText = nil } - + override func setupSubviews() { contentView.addSubview(swipeWrapper) cellContainerView.addSubviews( @@ -148,39 +146,39 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { avatarView, messageTimestampLabel ) - + messageContainerView.addSubview(messageLabel) configureMenu() cellContainerView.addSubview(reactionsContanerView) } - + func configureMenu() { containerView.layer.cornerRadius = 10 - + messageContainerView.removeFromSuperview() cellContainerView.addSubview(containerView) - + containerView.addSubview(messageContainerView) - + chatMenuManager.setup(for: containerView) } - + func updateOwnReaction() { ownReactionLabel.text = getReaction(for: model.address) ownReactionLabel.backgroundColor = .adamant.pickedReactionBackground } - + func updateOpponentReaction() { guard let reaction = getReaction(for: model.opponentAddress), - let senderPublicKey = getSenderPublicKeyInReaction(for: model.opponentAddress) + let senderPublicKey = getSenderPublicKeyInReaction(for: model.opponentAddress) else { opponentReactionLabel.attributedText = nil opponentReactionLabel.text = nil return } - + let fullString = NSMutableAttributedString(string: reaction) - + if let image = chatMessagesListViewModel?.avatarService.avatar( for: senderPublicKey, size: opponentReactionImageSize.width @@ -191,48 +189,48 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { origin: .init(x: .zero, y: -3), size: opponentReactionImageSize ) - + let imageString = NSAttributedString(attachment: replyImageAttachment) fullString.append(NSAttributedString(string: " ")) fullString.append(imageString) } - + opponentReactionLabel.attributedText = fullString opponentReactionLabel.backgroundColor = .adamant.pickedReactionBackground } - + func getSenderPublicKeyInReaction(for senderAddress: String) -> String? { model.reactions?.first( where: { $0.sender == senderAddress } )?.senderPublicKey } - + func getReaction(for address: String) -> String? { model.reactions?.first( where: { $0.sender == address } )?.reaction } - + override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { super.apply(layoutAttributes) guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { return } - + self.layoutAttributes = attributes } - + /// Positions the message bubble's top label. /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. override func layoutMessageTopLabel( with attributes: MessagesCollectionViewLayoutAttributes ) { - messageTopLabel.textAlignment = attributes.messageTopLabelAlignment.textAlignment - messageTopLabel.textInsets = attributes.messageTopLabelAlignment.textInsets + messageTopLabel.textAlignment = attributes.messageTopLabelAlignment.textAlignment + messageTopLabel.textInsets = attributes.messageTopLabelAlignment.textInsets - let y = containerView.frame.minY - attributes.messageContainerPadding.top - attributes.messageTopLabelSize.height - let origin = CGPoint(x: 0, y: y) + let y = containerView.frame.minY - attributes.messageContainerPadding.top - attributes.messageTopLabelSize.height + let origin = CGPoint(x: 0, y: y) - messageTopLabel.frame = CGRect(origin: origin, size: attributes.messageTopLabelSize) + messageTopLabel.frame = CGRect(origin: origin, size: attributes.messageTopLabelSize) } /// Positions the message bubble's bottom label. @@ -240,15 +238,15 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { override func layoutMessageBottomLabel( with attributes: MessagesCollectionViewLayoutAttributes ) { - messageBottomLabel.textAlignment = attributes.messageBottomLabelAlignment.textAlignment - messageBottomLabel.textInsets = attributes.messageBottomLabelAlignment.textInsets + messageBottomLabel.textAlignment = attributes.messageBottomLabelAlignment.textAlignment + messageBottomLabel.textInsets = attributes.messageBottomLabelAlignment.textInsets - let y = containerView.frame.maxY + attributes.messageContainerPadding.bottom - let origin = CGPoint(x: 0, y: y) + let y = containerView.frame.maxY + attributes.messageContainerPadding.bottom + let origin = CGPoint(x: 0, y: y) - messageBottomLabel.frame = CGRect(origin: origin, size: attributes.messageBottomLabelSize) + messageBottomLabel.frame = CGRect(origin: origin, size: attributes.messageBottomLabelSize) } - + /// Positions the message bubble's time label. /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. override func layoutTimeLabelView( @@ -256,21 +254,21 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { ) { let paddingLeft: CGFloat = 10 let origin = CGPoint( - x: UIScreen.main.bounds.width + paddingLeft, - y: containerView.frame.minY - + containerView.frame.height - * 0.5 - - messageTimestampLabel.font.ascender * 0.5 + x: UIScreen.main.bounds.width + paddingLeft, + y: containerView.frame.minY + + containerView.frame.height + * 0.5 + - messageTimestampLabel.font.ascender * 0.5 ) - + let size = CGSize( width: attributes.messageTimeLabelSize.width, height: attributes.messageTimeLabelSize.height ) - + messageTimestampLabel.frame = CGRect(origin: origin, size: size) } - + /// Positions the cell's `MessageContainerView`. /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. override func layoutMessageContainerView( @@ -280,48 +278,54 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { switch attributes.avatarPosition.vertical { case .messageBottom: - origin.y = attributes.size.height - - attributes.messageContainerPadding.bottom - - attributes.cellBottomLabelSize.height - - attributes.messageBottomLabelSize.height - - attributes.messageContainerSize.height - - attributes.messageContainerPadding.top + origin.y = + attributes.size.height + - attributes.messageContainerPadding.bottom + - attributes.cellBottomLabelSize.height + - attributes.messageBottomLabelSize.height + - attributes.messageContainerSize.height + - attributes.messageContainerPadding.top case .messageCenter: - if attributes.avatarSize.height > attributes.messageContainerSize.height { - let messageHeight = attributes.messageContainerSize.height - + attributes.messageContainerPadding.top - + attributes.messageContainerPadding.bottom - origin.y = (attributes.size.height / 2) - (messageHeight / 2) - } else { - fallthrough - } + if attributes.avatarSize.height > attributes.messageContainerSize.height { + let messageHeight = + attributes.messageContainerSize.height + + attributes.messageContainerPadding.top + + attributes.messageContainerPadding.bottom + origin.y = (attributes.size.height / 2) - (messageHeight / 2) + } else { + fallthrough + } default: - if attributes.accessoryViewSize.height > attributes.messageContainerSize.height { - let messageHeight = attributes.messageContainerSize.height - + attributes.messageContainerPadding.top - + attributes.messageContainerPadding.bottom - origin.y = (attributes.size.height / 2) - (messageHeight / 2) - } else { - origin.y = attributes.cellTopLabelSize.height - + attributes.messageTopLabelSize.height - + attributes.messageContainerPadding.top - } + if attributes.accessoryViewSize.height > attributes.messageContainerSize.height { + let messageHeight = + attributes.messageContainerSize.height + + attributes.messageContainerPadding.top + + attributes.messageContainerPadding.bottom + origin.y = (attributes.size.height / 2) - (messageHeight / 2) + } else { + origin.y = + attributes.cellTopLabelSize.height + + attributes.messageTopLabelSize.height + + attributes.messageContainerPadding.top + } } let avatarPadding = attributes.avatarLeadingTrailingPadding switch attributes.avatarPosition.horizontal { case .cellLeading: - origin.x = attributes.avatarSize.width - + attributes.messageContainerPadding.left - + avatarPadding + origin.x = + attributes.avatarSize.width + + attributes.messageContainerPadding.left + + avatarPadding case .cellTrailing: - origin.x = attributes.frame.width - - attributes.avatarSize.width - - attributes.messageContainerSize.width - - attributes.messageContainerPadding.right - - avatarPadding + origin.x = + attributes.frame.width + - attributes.avatarSize.width + - attributes.messageContainerSize.width + - attributes.messageContainerPadding.right + - avatarPadding case .natural: - break + break } containerView.frame = CGRect( @@ -336,20 +340,23 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { messageContainerView.layoutIfNeeded() layoutReactionLabel() } - + func layoutReactionLabel() { - let additionalWidth: CGFloat = model.isFromCurrentSender - ? .zero - : containerView.frame.width - - var x = containerView.frame.origin.x - + additionalWidth - - reactionsContanerViewWidth / 2 - - let minSpace = model.isFromCurrentSender - ? minReactionsSpacingToOwnBoundary + reactionsContanerViewWidth - : minReactionsSpacingToOwnBoundary - + let additionalWidth: CGFloat = + model.isFromCurrentSender + ? .zero + : containerView.frame.width + + var x = + containerView.frame.origin.x + + additionalWidth + - reactionsContanerViewWidth / 2 + + let minSpace = + model.isFromCurrentSender + ? minReactionsSpacingToOwnBoundary + reactionsContanerViewWidth + : minReactionsSpacingToOwnBoundary + if model.isFromCurrentSender { x = min(x, contentView.bounds.width - minSpace) x = max(x, minReactionsSpacingToOppositeBoundary) @@ -357,19 +364,19 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { x = max(x, minSpace) x = min(x, contentView.bounds.width - minReactionsSpacingToOppositeBoundary - reactionsContanerViewWidth) } - + reactionsContanerView.frame = CGRect( origin: .init( x: x, y: containerView.frame.origin.y - + containerView.frame.height - - reactionsContanerVerticalSpace + + containerView.frame.height + - reactionsContanerVerticalSpace ), size: .init(width: reactionsContanerViewWidth, height: ownReactionSize.height) ) reactionsContanerView.layoutIfNeeded() } - + override func configure( with message: MessageType, at indexPath: IndexPath, @@ -380,20 +387,20 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { at: indexPath, and: messagesCollectionView ) - + updateOwnReaction() updateOpponentReaction() } - + /// Handle tap gesture on contentView and its subviews. override func handleTapGesture(_ gesture: UIGestureRecognizer) { let touchLocation = gesture.location(in: self) - + let containerViewContains = containerView.frame.contains(touchLocation) let canHandle = !cellContentView( canHandle: convert(touchLocation, to: containerView) ) - + switch true { case containerViewContains && canHandle: delegate?.didTapMessage(in: self) @@ -413,7 +420,7 @@ final class ChatMessageCell: TextMessageCell, ChatModelView { delegate?.didTapBackground(in: self) } } - + override func layoutSubviews() { super.layoutSubviews() swipeWrapper.frame = contentView.bounds @@ -429,42 +436,42 @@ extension ChatMessageCell { ) { [actionHandler, model] in actionHandler(.remove(id: model.id)) } - + let report = AMenuItem.action( title: .adamant.chat.report, systemImageName: "exclamationmark.bubble" ) { [actionHandler, model] in actionHandler(.report(id: model.id)) } - + let reply = AMenuItem.action( title: .adamant.chat.reply, systemImageName: "arrowshape.turn.up.left" ) { [actionHandler, model] in actionHandler(.reply(id: model.id)) } - + let copy = AMenuItem.action( title: .adamant.chat.copy, systemImageName: "doc.on.doc" ) { [actionHandler, model] in actionHandler(.copy(text: model.text.string)) } - + let copyInPart = AMenuItem.action( title: .adamant.chat.selectText, systemImageName: "selection.pin.in.out" ) { [actionHandler, model] in actionHandler(.copyInPart(text: model.text.string)) } - + guard !model.isFake else { return AMenuSection([copy]) } - + return AMenuSection([reply, copyInPart, copy, report, remove]) } - + @objc func tapReactionAction() { chatMenuManager.presentMenuProgrammatically(for: containerView) } @@ -479,7 +486,7 @@ extension ChatMessageCell: ChatMenuManagerDelegate { enabledDetectors: messageLabel.enabledDetectors )?.containerView } - + func presentMenu( copyView: UIView, size: CGSize, @@ -505,14 +512,14 @@ extension ChatMessageCell { func copy( with model: Model, attributes: MessagesCollectionViewLayoutAttributes?, - urlAttributes: [NSAttributedString.Key : Any], + urlAttributes: [NSAttributedString.Key: Any], enabledDetectors: [DetectorType] ) -> ChatMessageCell? { guard let attributes = attributes else { return nil } - + let cell = ChatMessageCell(frame: frame) cell.apply(attributes) - + cell.messageContainerView.backgroundColor = model.backgroundColor.uiColor cell.messageLabel.configure { cell.messageLabel.enabledDetectors = enabledDetectors diff --git a/Adamant/Modules/Chat/View/Subviews/ChatInputBar.swift b/Adamant/Modules/Chat/View/Subviews/ChatInputBar.swift index a5f5e6921..fb761cc54 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatInputBar.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatInputBar.swift @@ -6,10 +6,10 @@ // Copyright © 2022 Adamant. All rights reserved. // -import UIKit +import CommonKit import InputBarAccessoryView import SnapKit -import CommonKit +import UIKit final class ChatInputBar: InputBarAccessoryView { var onAttachmentButtonTap: (() -> Void)? @@ -18,156 +18,162 @@ final class ChatInputBar: InputBarAccessoryView { var fee = "" { didSet { updateFeeLabel() } } - + var isEnabled = true { didSet { updateIsEnabled() } } - + var isForcedSendEnabled = false { didSet { updateSendIsEnabled() } } - + var isAttachmentButtonEnabled = true { didSet { updateIsAttachmentButtonEnabled() } } - + var text: String { get { inputTextView.text } set { inputTextView.text = newValue } } - + private lazy var feeLabel = makeFeeLabel() private lazy var attachmentButton = makeAttachmentButton() - + override init(frame: CGRect) { super.init(frame: frame) configure() } - + required init?(coder: NSCoder) { super.init(coder: coder) configure() } - + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) updateLayerColors() } - + override func didMoveToWindow() { super.didMoveToWindow() sendButton.isEnabled = (isEnabled && !inputTextView.text.isEmpty) || isForcedSendEnabled } - + override func calculateIntrinsicContentSize() -> CGSize { let superSize = super.calculateIntrinsicContentSize() - + // Calculate the required height - let superTopStackViewHeight = topStackView.arrangedSubviews.count > .zero - ? topStackView.bounds.height - : .zero - + let superTopStackViewHeight = + topStackView.arrangedSubviews.count > .zero + ? topStackView.bounds.height + : .zero + let validTopStackViewHeight = topStackView.arrangedSubviews.map { $0.frame.height }.reduce(0, +) - + return .init( width: superSize.width, height: superSize.height - - superTopStackViewHeight - + validTopStackViewHeight + - superTopStackViewHeight + + validTopStackViewHeight ) } - + override func inputTextViewDidChange() { super.inputTextViewDidChange() - - sendButton.isEnabled = isForcedSendEnabled - ? true - : sendButton.isEnabled + + sendButton.isEnabled = + isForcedSendEnabled + ? true + : sendButton.isEnabled } } -private extension ChatInputBar { - func updateFeeLabel() { +extension ChatInputBar { + fileprivate func updateFeeLabel() { feeLabel.setTitle(fee.isEmpty ? " " : fee, for: .normal) feeLabel.setSize(feeLabel.titleLabel?.intrinsicContentSize ?? .zero, animated: false) } - - func updateIsEnabled() { + + fileprivate func updateIsEnabled() { inputTextView.isEditable = isEnabled sendButton.isEnabled = isEnabled attachmentButton.isEnabled = isEnabled - - inputTextView.placeholder = isEnabled + + inputTextView.placeholder = + isEnabled ? .adamant.chat.messageInputPlaceholder : "" - - inputTextView.backgroundColor = isEnabled + + inputTextView.backgroundColor = + isEnabled ? .adamant.chatInputFieldBarBackground : .adamant.chatInputBarBackground - + updateLayerColors() updateIsAttachmentButtonEnabled() } - - func updateSendIsEnabled() { + + fileprivate func updateSendIsEnabled() { sendButton.isEnabled = (isEnabled && !inputTextView.text.isEmpty) || isForcedSendEnabled } - - func updateIsAttachmentButtonEnabled() { + + fileprivate func updateIsAttachmentButtonEnabled() { let isEnabled = isEnabled && isAttachmentButtonEnabled - + attachmentButton.isEnabled = isEnabled - attachmentButton.tintColor = isEnabled + attachmentButton.tintColor = + isEnabled ? .adamant.primary : .adamant.disableBorderColor } - - func updateLayerColors() { - let borderColor = isEnabled + + fileprivate func updateLayerColors() { + let borderColor = + isEnabled ? UIColor.adamant.chatInputBarBorderColor.cgColor : UIColor.adamant.disableBorderColor.cgColor - + sendButton.layer.borderColor = borderColor inputTextView.layer.borderColor = borderColor } - - func configure() { + + fileprivate func configure() { backgroundColor = .adamant.chatInputBarBackground backgroundView.backgroundColor = .adamant.chatInputBarBackground separatorLine.backgroundColor = .adamant.chatInputBarBorderColor - + configureLayout() configureSendButton() configureTextView() updateIsEnabled() updateFeeLabel() } - - func configureLayout() { + + fileprivate func configureLayout() { setStackViewItems([sendButton], forStack: .right, animated: false) setStackViewItems([feeLabel, .flexibleSpace], forStack: .bottom, animated: false) setStackViewItems([attachmentButton], forStack: .left, animated: false) - + // Adding spacing between leftStackView (attachment button) and message input field setLeftStackViewWidthConstant( to: attachmentButtonSize + baseInsetSize * 2, animated: false ) - + leftStackView.layoutMargins = .init( top: .zero, left: .zero, bottom: .zero, right: baseInsetSize * 2 ) - + leftStackView.alignment = .bottom leftStackView.isLayoutMarginsRelativeArrangement = true } - - func configureSendButton() { + + fileprivate func configureSendButton() { sendButton.layer.cornerRadius = cornerRadius sendButton.layer.borderWidth = 1 sendButton.tintColor = .adamant.primary @@ -175,29 +181,29 @@ private extension ChatInputBar { sendButton.title = nil sendButton.image = .asset(named: "Arrow") } - - func configureTextView() { + + fileprivate func configureTextView() { inputTextView.autocorrectionType = .no inputTextView.placeholderTextColor = .adamant.chatPlaceholderTextColor inputTextView.layer.borderWidth = 1 inputTextView.layer.cornerRadius = cornerRadius inputTextView.layer.masksToBounds = true inputTextView.isImagePasteEnabled = false - + inputTextView.textContainerInset = .init( top: baseInsetSize + 2, left: baseInsetSize * 2, bottom: baseInsetSize - 2, right: baseInsetSize * 2 ) - + inputTextView.placeholderLabelInsets = .init( top: baseInsetSize + 2, left: baseInsetSize * 2 + 4, bottom: baseInsetSize - 2, right: baseInsetSize * 2 ) - + inputTextView.scrollIndicatorInsets = .init( top: baseInsetSize + 2, left: .zero, @@ -205,12 +211,12 @@ private extension ChatInputBar { right: .zero ) } - - func makeAttachmentButton() -> InputBarButtonItem { + + fileprivate func makeAttachmentButton() -> InputBarButtonItem { let button = InputBarButtonItem().onTouchUpInside { [weak self] _ in self?.onAttachmentButtonTap?() } - + button.image = .asset(named: "Attachment") button.setSize( .init(width: attachmentButtonSize, height: attachmentButtonSize), @@ -218,8 +224,8 @@ private extension ChatInputBar { ) return button } - - func makeFeeLabel() -> InputBarButtonItem { + + fileprivate func makeFeeLabel() -> InputBarButtonItem { let feeLabel = InputBarButtonItem() feeLabel.isEnabled = false feeLabel.titleLabel?.font = .systemFont(ofSize: 12) @@ -237,14 +243,14 @@ extension InputTextView { } return super.canPerformAction(action, withSender: sender) } - + open override func paste(_ sender: Any?) { super.paste(sender) - + guard let view = inputBarAccessoryView as? ChatInputBar, - let image = UIPasteboard.general.image + let image = UIPasteboard.general.image else { return } - + view.onImagePasted?(image) } } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatMedia/ChatMediaCell.swift b/Adamant/Modules/Chat/View/Subviews/ChatMedia/ChatMediaCell.swift index b8d03ed4f..e52c2a07b 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatMedia/ChatMediaCell.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatMedia/ChatMediaCell.swift @@ -6,51 +6,51 @@ // Copyright © 2024 Adamant. All rights reserved. // -import UIKit -import SnapKit -import MessageKit import Combine +import MessageKit +import SnapKit +import UIKit final class ChatMediaCell: MessageContentCell, ChatModelView { private let containerMediaView = ChatMediaContainerView() private let cellContainerView = UIView() private lazy var swipeWrapper = ChatSwipeWrapper(cellContainerView) - + var subscription: AnyCancellable? - + var model: ChatMediaContainerView.Model = .default { didSet { swipeWrapper.model = .init(id: model.id, state: model.swipeState) containerMediaView.model = model } } - + var actionHandler: (ChatAction) -> Void { get { containerMediaView.actionHandler } set { containerMediaView.actionHandler = newValue } } - + var chatMessagesListViewModel: ChatMessagesListViewModel? { get { containerMediaView.chatMessagesListViewModel } set { containerMediaView.chatMessagesListViewModel = newValue } } - + override var isSelected: Bool { didSet { containerMediaView.isSelected = isSelected } } - + override init(frame: CGRect) { super.init(frame: frame) configure() } - + required init?(coder: NSCoder) { super.init(coder: coder) configure() } - + override func configure( with message: MessageType, at indexPath: IndexPath, @@ -60,19 +60,19 @@ final class ChatMediaCell: MessageContentCell, ChatModelView { messageContainerView.style = .none messageContainerView.backgroundColor = .clear } - + override func layoutMessageContainerView( with attributes: MessagesCollectionViewLayoutAttributes ) { super.layoutMessageContainerView(with: attributes) - + containerMediaView.snp.remakeConstraints { make in make.horizontalEdges.equalToSuperview() make.top.equalTo(messageContainerView.frame.origin.y) make.height.equalTo(messageContainerView.frame.height) } } - + override func setupSubviews() { cellContainerView.addSubviews( accessoryView, @@ -88,8 +88,8 @@ final class ChatMediaCell: MessageContentCell, ChatModelView { } } -private extension ChatMediaCell { - func configure() { +extension ChatMediaCell { + fileprivate func configure() { contentView.addSubview(swipeWrapper) swipeWrapper.snp.makeConstraints { $0.directionalEdges.equalToSuperview() diff --git a/Adamant/Modules/Chat/View/Subviews/ChatMedia/Container/ChatMediaContainerView+Model.swift b/Adamant/Modules/Chat/View/Subviews/ChatMedia/Container/ChatMediaContainerView+Model.swift index 6b0b8fe67..64e145e9d 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatMedia/Container/ChatMediaContainerView+Model.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatMedia/Container/ChatMediaContainerView+Model.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation extension ChatMediaContainerView { struct Model: ChatReusableViewModelProtocol, MessageModel, @unchecked Sendable { @@ -20,7 +20,7 @@ extension ChatMediaContainerView { let txStatus: MessageStatus var status: FileMessageStatus var swipeState: ChatSwipeWrapperModel.State - + static var `default`: Self { Self( id: "", @@ -34,22 +34,22 @@ extension ChatMediaContainerView { swipeState: .idle ) } - + func makeReplyContent() -> NSAttributedString { let mediaFilesCount = content.fileModel.files.filter { file in return file.fileType == .image || file.fileType == .video }.count - + let otherFilesCount = content.fileModel.files.count - mediaFilesCount - + let comment = content.comment.string - + let text = FilePresentationHelper.getFilePresentationText( mediaFilesCount: mediaFilesCount, otherFilesCount: otherFilesCount, comment: comment ) - + return ChatMessageFactory.markdownParser.parse(text) } } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatMedia/Container/ChatMediaContainerView.swift b/Adamant/Modules/Chat/View/Subviews/ChatMedia/Container/ChatMediaContainerView.swift index 7f0ccfa6d..30d995ce0 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatMedia/Container/ChatMediaContainerView.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatMedia/Container/ChatMediaContainerView.swift @@ -6,9 +6,9 @@ // Copyright © 2024 Adamant. All rights reserved. // -import UIKit import Combine import CommonKit +import UIKit final class ChatMediaContainerView: UIView { private let spacingView: UIView = { @@ -16,7 +16,7 @@ final class ChatMediaContainerView: UIView { view.setContentCompressionResistancePriority(.dragThatCanResizeScene, for: .horizontal) return view }() - + private lazy var horizontalStack: UIStackView = { let stack = UIStackView() stack.alignment = .center @@ -24,7 +24,7 @@ final class ChatMediaContainerView: UIView { stack.spacing = horizontalStackSpace return stack }() - + private lazy var ownReactionLabel: UILabel = { let label = UILabel() label.text = getReaction(for: model.address) @@ -32,22 +32,22 @@ final class ChatMediaContainerView: UIView { label.layer.cornerRadius = ownReactionSize.height / 2 label.textAlignment = .center label.layer.masksToBounds = true - + label.snp.makeConstraints { make in make.width.equalTo(ownReactionSize.width) make.height.equalTo(ownReactionSize.height) } - + let tapGesture = UITapGestureRecognizer( target: self, action: #selector(tapReactionAction) ) - + label.addGestureRecognizer(tapGesture) label.isUserInteractionEnabled = true return label }() - + private lazy var opponentReactionLabel: UILabel = { let label = UILabel() label.text = getReaction(for: model.opponentAddress) @@ -55,22 +55,22 @@ final class ChatMediaContainerView: UIView { label.layer.masksToBounds = true label.backgroundColor = .adamant.pickedReactionBackground label.layer.cornerRadius = opponentReactionSize.height / 2 - + label.snp.makeConstraints { make in make.width.equalTo(opponentReactionSize.width) make.height.equalTo(opponentReactionSize.height) } - + let tapGesture = UITapGestureRecognizer( target: self, action: #selector(tapReactionAction) ) - + label.addGestureRecognizer(tapGesture) label.isUserInteractionEnabled = true return label }() - + private lazy var reactionsStack: UIStackView = { let stack = UIStackView() stack.alignment = .center @@ -82,64 +82,67 @@ final class ChatMediaContainerView: UIView { stack.addArrangedSubview(opponentReactionLabel) return stack }() - + private lazy var statusButton: UIButton = { let button = UIButton() button.addTarget(self, action: #selector(onStatusButtonTap), for: .touchUpInside) return button }() - + private lazy var contentView = ChatMediaContentView() private lazy var chatMenuManager = ChatMenuManager(delegate: self) // MARK: Dependencies - + var chatMessagesListViewModel: ChatMessagesListViewModel? - + var model: Model = .default { didSet { update() } } - + var actionHandler: (ChatAction) -> Void = { _ in } { didSet { contentView.actionHandler = actionHandler } } - + var isSelected: Bool = false { didSet { contentView.isSelected = isSelected } } - + // MARK: - Init - + override init(frame: CGRect) { super.init(frame: frame) configure() } - + required init?(coder: NSCoder) { super.init(coder: coder) configure() } - + @objc func onStatusButtonTap() { if model.status == .failed, - let file = model.content.fileModel.files.first { + let file = model.content.fileModel.files.first + { actionHandler(.openFile(messageId: model.id, file: file)) return } - + guard case .needToDownload = model.status else { return } - + let fileModel = model.content.fileModel let fileList = Array(fileModel.files.prefix(FilesConstants.maxFilesCount)) - - actionHandler(.forceDownloadAllFiles( - messageId: fileModel.messageId, - files: fileList - )) + + actionHandler( + .forceDownloadAllFiles( + messageId: fileModel.messageId, + files: fileList + ) + ) } } @@ -150,11 +153,11 @@ extension ChatMediaContainerView { $0.verticalEdges.equalToSuperview() $0.horizontalEdges.equalToSuperview().inset(4) } - + reactionsStack.snp.makeConstraints { $0.width.equalTo(reactionsWidth) } chatMenuManager.setup(for: contentView) } - + func update() { contentView.model = model.content updateLayout() @@ -164,25 +167,26 @@ extension ChatMediaContainerView { updateOpponentReaction() updateStatus(model.status) } - + func updateStatus(_ status: FileMessageStatus) { statusButton.setImage(status.image, for: .normal) statusButton.tintColor = status.imageTintColor statusButton.isHidden = status == .success } - + func updateLayout() { var viewsList = [spacingView, reactionsStack, contentView] - - viewsList = model.isFromCurrentSender + + viewsList = + model.isFromCurrentSender ? viewsList : viewsList.reversed() - + guard horizontalStack.arrangedSubviews != viewsList else { return } horizontalStack.arrangedSubviews.forEach { horizontalStack.removeArrangedSubview($0) } viewsList.forEach { horizontalStack.addArrangedSubview($0) } } - + func updateOwnReaction() { ownReactionLabel.text = getReaction(for: model.address) ownReactionLabel.backgroundColor = model.content.backgroundColor.uiColor.mixin( @@ -190,18 +194,18 @@ extension ChatMediaContainerView { alpha: 0.15 ) } - + func updateOpponentReaction() { guard let reaction = getReaction(for: model.opponentAddress), - let senderPublicKey = getSenderPublicKeyInReaction(for: model.opponentAddress) + let senderPublicKey = getSenderPublicKeyInReaction(for: model.opponentAddress) else { opponentReactionLabel.attributedText = nil opponentReactionLabel.text = nil return } - + let fullString = NSMutableAttributedString(string: reaction) - + if let image = chatMessagesListViewModel?.avatarService.avatar( for: senderPublicKey, size: opponentReactionImageSize.width @@ -212,31 +216,31 @@ extension ChatMediaContainerView { origin: .init(x: .zero, y: -3), size: opponentReactionImageSize ) - + let imageString = NSAttributedString(attachment: replyImageAttachment) fullString.append(NSAttributedString(string: " ")) fullString.append(imageString) } - + opponentReactionLabel.attributedText = fullString opponentReactionLabel.backgroundColor = model.content.backgroundColor.uiColor.mixin( infusion: .lightGray, alpha: 0.15 ) } - + func getSenderPublicKeyInReaction(for senderAddress: String) -> String? { model.reactions?.first( where: { $0.sender == senderAddress } )?.senderPublicKey } - + func getReaction(for address: String) -> String? { model.reactions?.first( where: { $0.sender == address } )?.reaction } - + @objc func tapReactionAction() { chatMenuManager.presentMenuProgrammatically(for: contentView) } @@ -246,7 +250,7 @@ extension ChatMediaContainerView: ChatMenuManagerDelegate { func getCopyView() -> UIView? { copy(with: model)?.contentView } - + func presentMenu( copyView: UIView, size: CGSize, @@ -277,32 +281,33 @@ extension ChatMediaContainerView { ) { [actionHandler, model] in actionHandler(.remove(id: model.id)) } - + let report = AMenuItem.action( title: .adamant.chat.report, systemImageName: "exclamationmark.bubble" ) { [actionHandler, model] in actionHandler(.report(id: model.id)) } - + let reply = AMenuItem.action( title: .adamant.chat.reply, systemImageName: "arrowshape.turn.up.left" ) { [actionHandler, model] in actionHandler(.reply(id: model.id)) } - + let copy = AMenuItem.action( title: .adamant.chat.copy, systemImageName: "doc.on.doc" ) { [actionHandler, model] in actionHandler(.copy(text: model.content.comment.string)) } - - let actions: [AMenuItem] = model.content.comment.string.isEmpty - ? [reply, report, remove] - : [reply, copy, report, remove] - + + let actions: [AMenuItem] = + model.content.comment.string.isEmpty + ? [reply, report, remove] + : [reply, copy, report, remove] + return AMenuSection(actions) } } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/ChatMediaContnentView+Model.swift b/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/ChatMediaContnentView+Model.swift index 72166406e..b97f379db 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/ChatMediaContnentView+Model.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/ChatMediaContnentView+Model.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation extension ChatMediaContentView { struct Model: Equatable { @@ -20,7 +20,7 @@ extension ChatMediaContentView { let replyId: String let comment: NSAttributedString let backgroundColor: ChatMessageBackgroundColor - + static var `default`: Self { Self( id: "", @@ -35,7 +35,7 @@ extension ChatMediaContentView { ) } } - + struct FileModel: Equatable { let messageId: String var files: [ChatFile] @@ -43,7 +43,7 @@ extension ChatMediaContentView { let isFromCurrentSender: Bool let txStatus: MessageStatus var showAutoDownloadWarningLabel: Bool - + static var `default`: Self { Self( messageId: .empty, @@ -55,11 +55,11 @@ extension ChatMediaContentView { ) } } - + struct FileContentModel { let chatFile: ChatFile let txStatus: MessageStatus - + static var `default`: Self { Self( chatFile: .default, diff --git a/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/ChatMediaContnentView.swift b/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/ChatMediaContnentView.swift index 0450da47e..b2026e66c 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/ChatMediaContnentView.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/ChatMediaContnentView.swift @@ -6,46 +6,48 @@ // Copyright © 2024 Adamant. All rights reserved. // -import SnapKit -import UIKit import CommonKit import FilesPickerKit import MessageKit +import SnapKit +import UIKit final class ChatMediaContentView: UIView { private let commentLabel = MessageLabel() - + private let spacingView: UIView = { let view = UIView() view.snp.makeConstraints { $0.height.equalTo(verticalInsets) } return view }() - + private var replyMessageLabel = UILabel() - + private lazy var colorView: UIView = { let view = UIView() view.clipsToBounds = true view.backgroundColor = .adamant.active return view }() - + private lazy var replyView: UIView = { let view = UIView() view.backgroundColor = .lightGray.withAlphaComponent(0.15) view.layer.cornerRadius = 5 view.clipsToBounds = true - - view.addGestureRecognizer(UITapGestureRecognizer( - target: self, - action: #selector(didTap) - )) - + + view.addGestureRecognizer( + UITapGestureRecognizer( + target: self, + action: #selector(didTap) + ) + ) + view.addSubview(colorView) view.addSubview(replyMessageLabel) - + replyMessageLabel.numberOfLines = 1 - + colorView.snp.makeConstraints { $0.top.leading.bottom.equalToSuperview() $0.width.equalTo(2) @@ -60,53 +62,53 @@ final class ChatMediaContentView: UIView { } return view }() - + private lazy var replyContainerView: UIView = { let view = UIView() view.backgroundColor = .clear - + view.addSubview(replyView) - + replyView.snp.makeConstraints { make in make.top.equalToSuperview().offset(verticalInsets) make.horizontalEdges.equalToSuperview().inset(horizontalInsets) make.bottom.equalToSuperview() } - + view.snp.makeConstraints { make in make.height.equalTo(replyContainerViewDynamicHeight) } return view }() - + private lazy var listFileContainerView: UIView = { let view = UIView() view.backgroundColor = .clear - + view.addSubview(fileContainerView) - + fileContainerView.snp.makeConstraints { make in make.verticalEdges.equalToSuperview().inset(verticalInsets) make.horizontalEdges.equalToSuperview().inset(horizontalInsets) } - + return view }() - + private lazy var commentContainerView: UIView = { let view = UIView() view.backgroundColor = .clear - + view.addSubview(commentLabel) - + commentLabel.snp.makeConstraints { make in make.verticalEdges.equalToSuperview().inset(verticalInsets) make.horizontalEdges.equalToSuperview().inset(horizontalInsets) } - + return view }() - + private lazy var verticalStack: UIStackView = { let stack = UIStackView(arrangedSubviews: [replyContainerView, spacingView, mediaContainerView, listFileContainerView, commentContainerView]) stack.axis = .vertical @@ -114,29 +116,29 @@ final class ChatMediaContentView: UIView { stack.layer.masksToBounds = true return stack }() - + private lazy var uploadImageView = UIImageView(image: .asset(named: "downloadIcon")) - + private lazy var mediaContainerView = MediaContainerView() private lazy var fileContainerView = FileListContainerView() - + var replyViewDynamicHeight: CGFloat { model.isReply ? replyViewHeight : .zero } - + var replyContainerViewDynamicHeight: CGFloat { model.isReply - ? replyViewHeight + verticalInsets - : .zero + ? replyViewHeight + verticalInsets + : .zero } - + var model: Model = .default { didSet { guard oldValue != model else { return } update() } } - + var isSelected: Bool = false { didSet { animateIsSelected( @@ -145,19 +147,19 @@ final class ChatMediaContentView: UIView { ) } } - + var actionHandler: (ChatAction) -> Void = { _ in } - + override init(frame: CGRect) { super.init(frame: frame) configure() } - + required init?(coder: NSCoder) { super.init(coder: coder) configure() } - + override func traitCollectionDidChange( _ previousTraitCollection: UITraitCollection? ) { @@ -165,63 +167,65 @@ final class ChatMediaContentView: UIView { } } -private extension ChatMediaContentView { - func configure() { +extension ChatMediaContentView { + fileprivate func configure() { layer.masksToBounds = true layer.cornerRadius = 16 layer.borderWidth = 2.5 - + addSubview(verticalStack) verticalStack.snp.makeConstraints { make in make.directionalEdges.equalToSuperview() } - + addSubview(uploadImageView) uploadImageView.snp.makeConstraints { make in make.center.equalToSuperview() make.size.equalTo(imageSize) } - + uploadImageView.transform = CGAffineTransform(rotationAngle: .pi) - + commentLabel.enabledDetectors = [.url] commentLabel.setAttributes([.foregroundColor: UIColor.adamant.active], detector: .url) } - - func update() { + + fileprivate func update() { alpha = model.isHidden ? .zero : 1.0 backgroundColor = model.backgroundColor.uiColor layer.borderColor = model.backgroundColor.uiColor.cgColor - + uploadImageView.isHidden = model.fileModel.txStatus != .failed - + commentLabel.attributedText = model.comment commentLabel.isHidden = model.comment.string.isEmpty commentContainerView.isHidden = model.comment.string.isEmpty replyContainerView.isHidden = !model.isReply spacingView.isHidden = !model.fileModel.isMediaFilesOnly - - replyMessageLabel.attributedText = model.isReply - ? model.replyMessage - : nil - + + replyMessageLabel.attributedText = + model.isReply + ? model.replyMessage + : nil + replyContainerView.snp.updateConstraints { make in make.height.equalTo(replyContainerViewDynamicHeight) } - - let spaceHeight = model.fileModel.isMediaFilesOnly && model.isReply - ? verticalInsets - : .zero + + let spaceHeight = + model.fileModel.isMediaFilesOnly && model.isReply + ? verticalInsets + : .zero spacingView.snp.remakeConstraints { $0.height.equalTo(spaceHeight) } - + updateStackLayout() } - - func updateStackLayout() { + + fileprivate func updateStackLayout() { spacingView.isHidden = !model.fileModel.isMediaFilesOnly mediaContainerView.isHidden = !model.fileModel.isMediaFilesOnly listFileContainerView.isHidden = model.fileModel.isMediaFilesOnly - + if model.fileModel.isMediaFilesOnly { mediaContainerView.actionHandler = actionHandler mediaContainerView.model = model.fileModel @@ -230,21 +234,25 @@ private extension ChatMediaContentView { fileContainerView.model = model.fileModel } } - - @objc func didTap() { - actionHandler(.scrollTo(message: .init( - id: model.id, - replyId: model.replyId, - message: NSAttributedString(string: .empty), - messageReply: NSAttributedString(string: .empty), - backgroundColor: .failed, - isFromCurrentSender: true, - reactions: nil, - address: .empty, - opponentAddress: .empty, - isHidden: false, - swipeState: .idle - ))) + + @objc fileprivate func didTap() { + actionHandler( + .scrollTo( + message: .init( + id: model.id, + replyId: model.replyId, + message: NSAttributedString(string: .empty), + messageReply: NSAttributedString(string: .empty), + backgroundColor: .failed, + isFromCurrentSender: true, + reactions: nil, + address: .empty, + opponentAddress: .empty, + isHidden: false, + swipeState: .idle + ) + ) + ) } } @@ -263,24 +271,24 @@ extension ChatMediaContentView.Model { func width() -> CGFloat { fileModel.width() } - + @MainActor func height() -> CGFloat { let replyViewDynamicHeight: CGFloat = isReply ? replyViewHeight : .zero - + var spaceCount: CGFloat = fileModel.isMediaFilesOnly ? .zero : 1 - + if isReply { spaceCount += 2 } - + if !comment.string.isEmpty { spaceCount += 3 } - + return fileModel.height() - + spaceCount * verticalInsets - + replyViewDynamicHeight + + spaceCount * verticalInsets + + replyViewDynamicHeight } } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/Views/ChatFileContainerView/FileListContainerView.swift b/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/Views/ChatFileContainerView/FileListContainerView.swift index 898a007bc..ef968324d 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/Views/ChatFileContainerView/FileListContainerView.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/Views/ChatFileContainerView/FileListContainerView.swift @@ -6,18 +6,18 @@ // Copyright © 2024 Adamant. All rights reserved. // -import SnapKit -import UIKit +import Combine import CommonKit import FilesPickerKit -import Combine +import SnapKit +import UIKit final class FileListContainerView: UIView { private lazy var filesStack: UIStackView = { let stack = UIStackView() stack.axis = .vertical stack.spacing = Self.stackSpacing - + for _ in 0.. Void = { _ in } - + // MARK: - Init - + override init(frame: CGRect) { super.init(frame: frame) configure() } - + required init?(coder: NSCoder) { super.init(coder: coder) configure() } } -private extension FileListContainerView { - func configure() { +extension FileListContainerView { + fileprivate func configure() { addSubview(filesStack) filesStack.snp.makeConstraints { $0.directionalEdges.equalToSuperview() } } - - func onAppear() { + + fileprivate func onAppear() { let filesToDownload = model.files .prefix(FilesConstants.maxFilesCount) .filter { $0.fileType.isMedia } - + guard !filesToDownload.isEmpty else { return } - - actionHandler(.autoDownloadContentIfNeeded( - messageId: model.messageId, - files: filesToDownload - )) + + actionHandler( + .autoDownloadContentIfNeeded( + messageId: model.messageId, + files: filesToDownload + ) + ) } - - func update(old: ChatMediaContentView.FileModel) { + + fileprivate func update(old: ChatMediaContentView.FileModel) { if old.messageId != model.messageId { onAppear() } - + let fileList = model.files.prefix(FilesConstants.maxFilesCount) filesStack.arrangedSubviews.forEach { $0.isHidden = true } @@ -83,12 +85,18 @@ private extension FileListContainerView { txStatus: model.txStatus ) view?.buttonActionHandler = { [weak self, file, model] in - self?.actionHandler( - .openFile( - messageId: model.messageId, - file: file + if file.isBusy, file.isUploading { + self?.actionHandler( + .cancelUploading(messageId: model.messageId, file: file) + ) + } else { + self?.actionHandler( + .openFile( + messageId: model.messageId, + file: file + ) ) - ) + } } } } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/Views/ChatFileContainerView/FileListContentView.swift b/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/Views/ChatFileContainerView/FileListContentView.swift index 6780c0e10..fa8105c68 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/Views/ChatFileContainerView/FileListContentView.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/Views/ChatFileContainerView/FileListContentView.swift @@ -6,9 +6,9 @@ // Copyright © 2024 Adamant. All rights reserved. // -import UIKit import CommonKit import SwiftUI +import UIKit final class FileListContentView: UIView { private lazy var iconImageView: UIImageView = UIImageView() @@ -22,22 +22,22 @@ final class FileListContentView: UIView { view.backgroundColor = .darkGray.withAlphaComponent(0.45) return view }() - + private lazy var horizontalStack: UIStackView = { let stack = UIStackView() stack.alignment = .center stack.axis = .horizontal stack.spacing = stackSpacing - + stack.addArrangedSubview(iconImageView) stack.addArrangedSubview(vStack) return stack }() - + private let nameLabel = UILabel(font: nameFont, textColor: .adamant.textColor) private let sizeLabel = UILabel(font: sizeFont, textColor: .lightGray) private let additionalLabel = UILabel(font: additionalFont, textColor: .adamant.cellColor) - + private lazy var vStack: UIStackView = { let stack = UIStackView() stack.alignment = .leading @@ -49,31 +49,31 @@ final class FileListContentView: UIView { stack.addArrangedSubview(additionalDataStack) return stack }() - + private lazy var additionalDataStack: UIStackView = { let stack = UIStackView() stack.alignment = .center stack.axis = .horizontal stack.spacing = stackSpacing - + let progressBar = CircularProgressView { [weak self] in guard let self = self else { return .init(progress: .zero, hidden: true) } return self.progressState } let controller = UIHostingController(rootView: progressBar) controller.view.backgroundColor = .clear - + stack.addArrangedSubview(sizeLabel) stack.addArrangedSubview(controller.view) return stack }() - + private lazy var tapBtn: UIButton = { let btn = UIButton() btn.addTarget(self, action: #selector(tapBtnAction), for: .touchUpInside) return btn }() - + private lazy var progressState: CircularProgressState = { .init( lineWidth: 2.0, @@ -83,126 +83,129 @@ final class FileListContentView: UIView { hidden: true ) }() - + var model: ChatMediaContentView.FileContentModel = .default { didSet { update() } } - + var buttonActionHandler: (() -> Void)? - + init(model: ChatMediaContentView.FileContentModel) { super.init(frame: .zero) backgroundColor = .clear configure() self.model = model } - + override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .clear - + configure() } - + required init?(coder: NSCoder) { super.init(coder: coder) configure() } - + override func layoutSubviews() { super.layoutSubviews() - + iconImageView.layer.cornerRadius = 5 } - + @objc func tapBtnAction() { buttonActionHandler?() } } -private extension FileListContentView { - func configure() { +extension FileListContentView { + fileprivate func configure() { addSubview(horizontalStack) horizontalStack.snp.makeConstraints { make in make.directionalEdges.equalToSuperview() } - + iconImageView.snp.makeConstraints { make in make.size.equalTo(imageSize) } - + addSubview(additionalLabel) additionalLabel.snp.makeConstraints { make in make.center.equalTo(iconImageView.snp.center) } - + addSubview(spinner) spinner.snp.makeConstraints { make in make.center.equalTo(iconImageView) make.size.equalTo(imageSize / 2) } - + addSubview(downloadImageView) downloadImageView.snp.makeConstraints { make in make.center.equalTo(iconImageView) make.size.equalTo(imageSize / 1.3) } - + addSubview(videoIconIV) videoIconIV.snp.makeConstraints { make in make.center.equalTo(iconImageView) make.size.equalTo(imageSize / 2) } - + addSubview(tapBtn) tapBtn.snp.makeConstraints { make in make.directionalEdges.equalToSuperview() } - + nameLabel.lineBreakMode = .byTruncatingMiddle nameLabel.textAlignment = .left sizeLabel.textAlignment = .left iconImageView.layer.cornerRadius = 5 iconImageView.layer.masksToBounds = true iconImageView.contentMode = .scaleAspectFill - additionalLabel.textAlignment = .center + additionalLabel.textAlignment = .center videoIconIV.tintColor = .adamant.active - + videoIconIV.addShadow() downloadImageView.addShadow() spinner.addShadow(shadowColor: .white) } - - func update() { + + fileprivate func update() { let chatFile = model.chatFile - + let image: UIImage? if let previewImage = chatFile.previewImage { image = previewImage additionalLabel.isHidden = true } else { - image = chatFile.fileType.isMedia - ? defaultMediaImage - : defaultImage - + image = + chatFile.fileType.isMedia + ? defaultMediaImage + : defaultImage + additionalLabel.isHidden = chatFile.fileType.isMedia } - + if iconImageView.image != image { iconImageView.image = image } - - downloadImageView.isHidden = chatFile.isCached - || chatFile.isBusy - || model.txStatus == .failed - || (chatFile.fileType.isMedia && chatFile.previewImage == nil) - + + downloadImageView.isHidden = + chatFile.isCached + || chatFile.isBusy + || model.txStatus == .failed + || (chatFile.fileType.isMedia && chatFile.previewImage == nil) + if chatFile.isDownloading { if chatFile.previewImage == nil, - chatFile.file.preview != nil, - chatFile.downloadStatus.isPreviewDownloading { + chatFile.file.preview != nil, + chatFile.downloadStatus.isPreviewDownloading + { spinner.startAnimating() } else { spinner.stopAnimating() @@ -210,7 +213,7 @@ private extension FileListContentView { } else { spinner.stopAnimating() } - + if chatFile.isBusy { if chatFile.isUploading { progressState.hidden = false @@ -218,28 +221,28 @@ private extension FileListContentView { progressState.hidden = !chatFile.downloadStatus.isOriginalDownloading } } else { - progressState.hidden = chatFile.progress == 100 - || chatFile.progress == nil + progressState.hidden = + chatFile.progress == 100 + || chatFile.progress == nil } - + let progress = chatFile.progress ?? .zero progressState.progress = Double(progress) / 100 - + let fileType = chatFile.file.extension.map { ".\($0)" } ?? .empty let fileName = chatFile.file.name ?? .adamant.chat.unknownTitle.uppercased() - + nameLabel.text = "\(fileName)\(fileType)".withoutFileExtensionDuplication() sizeLabel.text = formatSize(chatFile.file.size) additionalLabel.text = fileType.uppercased() - - videoIconIV.isHidden = !( - chatFile.isCached + + videoIconIV.isHidden = + !(chatFile.isCached && !chatFile.isBusy - && chatFile.fileType == .video - ) + && chatFile.fileType == .video) } - - func formatSize(_ bytes: Int64) -> String { + + fileprivate func formatSize(_ bytes: Int64) -> String { let formatter = ByteCountFormatter() formatter.allowedUnits = [.useGB, .useMB, .useKB, .useBytes] formatter.countStyle = .file diff --git a/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/Views/MediaContainerView/MediaContainerView.swift b/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/Views/MediaContainerView/MediaContainerView.swift index bebfe32cc..297ec88c7 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/Views/MediaContainerView/MediaContainerView.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/Views/MediaContainerView/MediaContainerView.swift @@ -6,10 +6,10 @@ // Copyright © 2024 Adamant. All rights reserved. // -import SnapKit -import UIKit import CommonKit import FilesPickerKit +import SnapKit +import UIKit final class MediaContainerView: UIView { private lazy var filesStack: UIStackView = { @@ -19,14 +19,14 @@ final class MediaContainerView: UIView { stack.alignment = .fill stack.distribution = .fill stack.layer.masksToBounds = true - + for chunk in 0..<(FilesConstants.maxFilesCount / 2) { let stackView = UIStackView() stackView.axis = .horizontal stackView.spacing = stackSpacing stackView.alignment = .fill stackView.distribution = .fill - + for file in 0..<2 { let view = MediaContentView() view.layer.masksToBounds = true @@ -35,54 +35,54 @@ final class MediaContainerView: UIView { } stackView.addArrangedSubview(view) } - + stack.addArrangedSubview(stackView) } - + return stack }() - + private lazy var previewDownloadNotAllowedLabel = EdgeInsetLabel( font: previewDownloadNotAllowedFont, textColor: .adamant.textColor.withAlphaComponent(0.4) ) - + // MARK: Proprieties - + var model: ChatMediaContentView.FileModel = .default { didSet { update(old: oldValue) } } - + var actionHandler: (ChatAction) -> Void = { _ in } - + // MARK: - Init - + override init(frame: CGRect) { super.init(frame: frame) configure() } - + required init?(coder: NSCoder) { super.init(coder: coder) configure() } } -private extension MediaContainerView { - func configure() { +extension MediaContainerView { + fileprivate func configure() { layer.masksToBounds = true - + addSubview(filesStack) filesStack.snp.makeConstraints { $0.directionalEdges.equalToSuperview() $0.width.equalTo(model.width()) } - + addSubview(previewDownloadNotAllowedLabel) previewDownloadNotAllowedLabel.snp.makeConstraints { make in make.center.equalTo(filesStack.snp.center) } - + previewDownloadNotAllowedLabel.textInsets = previewTextInsets previewDownloadNotAllowedLabel.text = previewDownloadNotAllowedText previewDownloadNotAllowedLabel.numberOfLines = .zero @@ -93,36 +93,38 @@ private extension MediaContainerView { previewDownloadNotAllowedLabel.clipsToBounds = true previewDownloadNotAllowedLabel.sizeToFit() } - - func onAppear() { + + fileprivate func onAppear() { let filesToDownload = model.files .prefix(FilesConstants.maxFilesCount) .filter { $0.fileType.isMedia } - + guard !filesToDownload.isEmpty else { return } - - actionHandler(.autoDownloadContentIfNeeded( - messageId: model.messageId, - files: filesToDownload - )) + + actionHandler( + .autoDownloadContentIfNeeded( + messageId: model.messageId, + files: filesToDownload + ) + ) } - - func update(old: ChatMediaContentView.FileModel) { + + fileprivate func update(old: ChatMediaContentView.FileModel) { if model.messageId != old.messageId { onAppear() } - + let fileList = model.files.prefix(FilesConstants.maxFilesCount) updatePreviewDownloadLabel() - + for (index, stackView) in filesStack.arrangedSubviews.enumerated() { guard let horizontalStackView = stackView as? UIStackView else { continue } - + var isHorizontal = false - + for (fileIndex, fileView) in horizontalStackView.arrangedSubviews.enumerated() { guard let mediaView = fileView as? MediaContentView else { continue } - + let fileOverallIndex = index * horizontalStackView.arrangedSubviews.count + fileIndex - + if fileOverallIndex < fileList.count { let file = fileList[fileOverallIndex] mediaView.isHidden = false @@ -131,23 +133,31 @@ private extension MediaContainerView { txStatus: model.txStatus ) mediaView.buttonActionHandler = { [weak self, file, model] in - self?.actionHandler( - .openFile( - messageId: model.messageId, - file: file - ) - ) + let action: ChatAction = + if file.isBusy, file.isUploading { + .cancelUploading( + messageId: model.messageId, + file: file + ) + } else { + .openFile( + messageId: model.messageId, + file: file + ) + } + self?.actionHandler(action) } if let resolution = file.file.resolution, - resolution.width > resolution.height { + resolution.width > resolution.height + { isHorizontal = true } } else { mediaView.isHidden = true } } - + updateCellsSize( in: horizontalStackView, isHorizontal: isHorizontal, @@ -155,8 +165,8 @@ private extension MediaContainerView { ) } } - - func updateCellsSize( + + fileprivate func updateCellsSize( in horizontalStackView: UIStackView, isHorizontal: Bool, fileList: [ChatFile] @@ -165,13 +175,14 @@ private extension MediaContainerView { let minimumWidth = calculateMinimumWidth(availableWidth: filesStackWidth) let maximumWidth = calculateMaximumWidth(availableWidth: filesStackWidth) - - let height: CGFloat = isHorizontal - ? rowHorizontalHeight - : fileList.count == 1 ? rowVerticalHeight * 2 : rowVerticalHeight + + let height: CGFloat = + isHorizontal + ? rowHorizontalHeight + : fileList.count == 1 ? rowVerticalHeight * 2 : rowVerticalHeight var totalWidthForEqualAspectRatio: CGFloat = 0.0 - + for case let mediaView as MediaContentView in horizontalStackView.arrangedSubviews { if let resolution = mediaView.model.chatFile.file.resolution { let aspectRatio = resolution.width / resolution.height @@ -190,7 +201,7 @@ private extension MediaContainerView { let widthForEqualAspectRatio = height * aspectRatio var width = max(widthForEqualAspectRatio * scaleFactor, minimumWidth) width = min(width, maximumWidth) - + mediaView.snp.remakeConstraints { $0.width.equalTo(width - stackSpacing) $0.height.equalTo(height) @@ -203,16 +214,16 @@ private extension MediaContainerView { } } } - - func calculateMinimumWidth(availableWidth: CGFloat) -> CGFloat { + + fileprivate func calculateMinimumWidth(availableWidth: CGFloat) -> CGFloat { (availableWidth - stackSpacing) * 0.3 } - - func calculateMaximumWidth(availableWidth: CGFloat) -> CGFloat { + + fileprivate func calculateMaximumWidth(availableWidth: CGFloat) -> CGFloat { (availableWidth - stackSpacing) * 0.7 } - - func updatePreviewDownloadLabel() { + + fileprivate func updatePreviewDownloadLabel() { previewDownloadNotAllowedLabel.isHidden = !model.showAutoDownloadWarningLabel } } @@ -221,33 +232,35 @@ extension ChatMediaContentView.FileModel { @MainActor func height() -> CGFloat { let fileList = Array(files.prefix(FilesConstants.maxFilesCount)) - + guard isMediaFilesOnly else { return FileListContainerView.cellSize * CGFloat(fileList.count) - + FileListContainerView.stackSpacing * CGFloat(fileList.count) + + FileListContainerView.stackSpacing * CGFloat(fileList.count) } - + let rows = fileList.chunked(into: 2) var totalHeight: CGFloat = .zero - + for row in rows { var isHorizontal = false for row in row { if let resolution = row.file.resolution, - resolution.width > resolution.height { + resolution.width > resolution.height + { isHorizontal = true } } - - let height: CGFloat = isHorizontal - ? rowHorizontalHeight - : fileList.count == 1 ? rowVerticalHeight * 2 : rowVerticalHeight - + + let height: CGFloat = + isHorizontal + ? rowHorizontalHeight + : fileList.count == 1 ? rowVerticalHeight * 2 : rowVerticalHeight + totalHeight += height } - + return totalHeight - + stackSpacing * CGFloat(rows.count) + + stackSpacing * CGFloat(rows.count) } } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/Views/MediaContainerView/MediaContentView.swift b/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/Views/MediaContainerView/MediaContentView.swift index baaba4c8c..7c538342d 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/Views/MediaContainerView/MediaContentView.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatMedia/Content/Views/MediaContainerView/MediaContentView.swift @@ -6,15 +6,15 @@ // Copyright © 2024 Adamant. All rights reserved. // -import UIKit import SnapKit import SwiftUI +import UIKit final class MediaContentView: UIView { private lazy var imageView: UIImageView = UIImageView() private lazy var downloadImageView = UIImageView(image: .asset(named: "downloadIcon")) private lazy var videoIconIV = UIImageView(image: .asset(named: "playVideoIcon")) - + private lazy var spinner: UIActivityIndicatorView = { let view = UIActivityIndicatorView(style: .medium) view.isHidden = true @@ -22,13 +22,13 @@ final class MediaContentView: UIView { view.backgroundColor = .darkGray.withAlphaComponent(0.45) return view }() - + private lazy var tapBtn: UIButton = { let btn = UIButton() btn.addTarget(self, action: #selector(tapBtnAction), for: .touchUpInside) return btn }() - + private lazy var progressState: CircularProgressState = { .init( lineWidth: 2.0, @@ -38,102 +38,102 @@ final class MediaContentView: UIView { hidden: true ) }() - + private lazy var durationLabel = EdgeInsetLabel( font: durationFont, textColor: .white.withAlphaComponent(0.8) ) - + var model: ChatMediaContentView.FileContentModel = .default { didSet { update() } } - + var buttonActionHandler: (() -> Void)? - + init(model: ChatMediaContentView.FileContentModel) { super.init(frame: .zero) backgroundColor = .clear configure() - + self.model = model } - + override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .clear - + configure() } - + required init?(coder: NSCoder) { super.init(coder: coder) configure() } - + @objc func tapBtnAction() { buttonActionHandler?() } } -private extension MediaContentView { - func configure() { +extension MediaContentView { + fileprivate func configure() { addSubview(imageView) imageView.snp.makeConstraints { make in make.directionalEdges.equalToSuperview() } - + addSubview(spinner) spinner.snp.makeConstraints { make in make.center.equalTo(imageView) make.size.equalTo(imageSize / 2) } - + addSubview(downloadImageView) downloadImageView.snp.makeConstraints { make in make.center.equalTo(imageView) make.size.equalTo(imageSize / 1.3) } - + addSubview(durationLabel) durationLabel.snp.makeConstraints { make in make.bottom.trailing.equalToSuperview().offset(-10) } - + addSubview(tapBtn) tapBtn.snp.makeConstraints { make in make.directionalEdges.equalToSuperview() } - + addSubview(videoIconIV) videoIconIV.snp.makeConstraints { make in make.center.equalTo(imageView) make.size.equalTo(imageSize / 1.6) } - + let progressBar = CircularProgressView { [weak self] in guard let self = self else { return .init(progress: .zero, hidden: true) } return self.progressState } let controller = UIHostingController(rootView: progressBar) - + controller.view.backgroundColor = .clear addSubview(controller.view) controller.view.snp.makeConstraints { make in make.top.trailing.equalToSuperview().inset(15) make.size.equalTo(progressSize) } - + imageView.layer.masksToBounds = true imageView.contentMode = .scaleAspectFill videoIconIV.tintColor = .adamant.active - + videoIconIV.addShadow() downloadImageView.addShadow() spinner.addShadow(shadowColor: .white) controller.view.addShadow() - + durationLabel.textInsets = durationTextInsets durationLabel.numberOfLines = .zero durationLabel.textAlignment = .center @@ -142,31 +142,32 @@ private extension MediaContentView { durationLabel.addShadow() durationLabel.clipsToBounds = true } - - func update() { + + fileprivate func update() { let chatFile = model.chatFile - + let image = chatFile.previewImage ?? defaultMediaImage - + if imageView.image != image { imageView.image = image } - - downloadImageView.isHidden = chatFile.isCached - || chatFile.isBusy - || model.txStatus == .failed - || chatFile.previewImage == nil - - videoIconIV.isHidden = !( + + downloadImageView.isHidden = chatFile.isCached + || chatFile.isBusy + || model.txStatus == .failed + || chatFile.previewImage == nil + + videoIconIV.isHidden = + !(chatFile.isCached && !chatFile.isBusy - && chatFile.fileType == .video - ) - + && chatFile.fileType == .video) + if chatFile.isDownloading { if chatFile.previewImage == nil, - chatFile.file.preview != nil, - chatFile.downloadStatus.isPreviewDownloading { + chatFile.file.preview != nil, + chatFile.downloadStatus.isPreviewDownloading + { spinner.startAnimating() } else { spinner.stopAnimating() @@ -174,7 +175,7 @@ private extension MediaContentView { } else { spinner.stopAnimating() } - + if chatFile.isBusy { if chatFile.isUploading { progressState.hidden = false @@ -182,27 +183,28 @@ private extension MediaContentView { progressState.hidden = !chatFile.downloadStatus.isOriginalDownloading } } else { - progressState.hidden = chatFile.progress == 100 - || chatFile.progress == nil + progressState.hidden = + chatFile.progress == 100 + || chatFile.progress == nil } - + let progress = chatFile.progress ?? .zero progressState.progress = Double(progress) / 100 - + durationLabel.isHidden = chatFile.fileType != .video - + if let duration = chatFile.file.duration { durationLabel.text = formatTime(seconds: Int(duration)) } else { durationLabel.text = "-:-" } } - - func formatTime(seconds: Int) -> String { + + fileprivate func formatTime(seconds: Int) -> String { let hours = seconds / 3600 let minutes = (seconds % 3600) / 60 let seconds = seconds % 60 - + if hours > 0 { return String(format: "%02d:%02d:%02d", hours, minutes, seconds) } else { diff --git a/Adamant/Modules/Chat/View/Subviews/ChatMessagesCollection.swift b/Adamant/Modules/Chat/View/Subviews/ChatMessagesCollection.swift index 1635ee160..ea09b4b33 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatMessagesCollection.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatMessagesCollection.swift @@ -6,92 +6,100 @@ // Copyright © 2023 Adamant. All rights reserved. // +import CommonKit import MessageKit import UIKit -import CommonKit final class ChatMessagesCollectionView: MessagesCollectionView { private var currentIds = [String]() - + var fixedBottomOffset: CGFloat? - + var bottomOffset: CGFloat { contentSize.height + fullInsets.bottom - bounds.maxY } - + var fullInsets: UIEdgeInsets { safeAreaInsets + contentInset } - + // To prevent value changes by MessageKit. Insets can be set via `setFullBottomInset` only override var contentInset: UIEdgeInsets { get { super.contentInset } set {} } - + // To prevent value changes by MessageKit. Insets can be set via `setFullBottomInset` only override var verticalScrollIndicatorInsets: UIEdgeInsets { get { super.verticalScrollIndicatorInsets } set {} } - + override func layoutSubviews() { super.layoutSubviews() - + if let fixedBottomOffset = fixedBottomOffset, bottomOffset != fixedBottomOffset { setBottomOffset(fixedBottomOffset, safely: true) } } - - func reloadData(newIds: [String]) { - guard newIds.last == currentIds.last || newIds.first != currentIds.first else { + + func reloadData(newIds: [String], isOnBottom: Bool) { + let hasNewMessagesAtTop = newIds.first != currentIds.first + let hasNewMessagesAtBottom = newIds.last != currentIds.last + let hasSameOrMoreMessages = newIds.count >= currentIds.count + + guard hasNewMessagesAtTop || hasNewMessagesAtBottom, hasSameOrMoreMessages else { return applyNewIds(newIds) } - + let bottomOffset = self.bottomOffset applyNewIds(newIds) - setBottomOffset(bottomOffset, safely: !isDragging && !isDecelerating) + + if hasNewMessagesAtTop || (hasNewMessagesAtBottom && isOnBottom) { + setBottomOffset(bottomOffset, safely: !isDragging && !isDecelerating) + } } - + func setFullBottomInset(_ inset: CGFloat) { let inset = inset - safeAreaInsets.bottom - let bottomOffset = contentSize.height < bounds.height - ? 0 - : self.bottomOffset - + let bottomOffset = + contentSize.height < bounds.height + ? 0 + : self.bottomOffset + super.contentInset.bottom = inset super.verticalScrollIndicatorInsets.bottom = inset guard !hasActiveScrollGestures else { return } setBottomOffset(bottomOffset, safely: false) } - + func setBottomOffset(_ newValue: CGFloat, safely: Bool) { setVerticalContentOffset( maxVerticalOffset - newValue, safely: safely ) } - + func stopDecelerating() { setContentOffset(contentOffset, animated: false) } } -private extension ChatMessagesCollectionView { - var maxVerticalOffset: CGFloat { +extension ChatMessagesCollectionView { + fileprivate var maxVerticalOffset: CGFloat { contentSize.height + fullInsets.bottom - bounds.height } - - var minVerticalOffset: CGFloat { + + fileprivate var minVerticalOffset: CGFloat { -fullInsets.top } - - var scrollGestureRecognizers: [UIGestureRecognizer] { + + fileprivate var scrollGestureRecognizers: [UIGestureRecognizer] { [panGestureRecognizer, pinchGestureRecognizer].compactMap { $0 } } - - var hasActiveScrollGestures: Bool { + + fileprivate var hasActiveScrollGestures: Bool { scrollGestureRecognizers.contains { switch $0.state { case .began, .changed: @@ -103,16 +111,16 @@ private extension ChatMessagesCollectionView { } } } - - func applyNewIds(_ newIds: [String]) { + + fileprivate func applyNewIds(_ newIds: [String]) { reloadData() layoutIfNeeded() currentIds = newIds } - - func setVerticalContentOffset(_ offset: CGFloat, safely: Bool) { + + fileprivate func setVerticalContentOffset(_ offset: CGFloat, safely: Bool) { guard maxVerticalOffset > minVerticalOffset else { return } - + var offset = offset if safely { if offset > maxVerticalOffset { @@ -121,7 +129,7 @@ private extension ChatMessagesCollectionView { offset = minVerticalOffset } } - + contentOffset.y = offset } } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatModelView.swift b/Adamant/Modules/Chat/View/Subviews/ChatModelView.swift index a4f868b17..22f2386e6 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatModelView.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatModelView.swift @@ -6,10 +6,10 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit import Combine -import MessageKit import CommonKit +import MessageKit +import UIKit protocol ChatReusableViewModelProtocol: Equatable { static var `default`: Self { get } @@ -17,7 +17,7 @@ protocol ChatReusableViewModelProtocol: Equatable { protocol ChatModelView: UIView, ReusableView { associatedtype Model: ChatReusableViewModelProtocol - + var model: Model { get set } var actionHandler: (ChatAction) -> Void { get set } var subscription: AnyCancellable? { get set } @@ -31,7 +31,7 @@ extension ChatModelView { subscription = publisher.sink { [weak self, weak collection] newModel in guard newModel != self?.model else { return } self?.model = newModel - + collection?.collectionViewLayout.invalidateLayout() } } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatRefreshMock.swift b/Adamant/Modules/Chat/View/Subviews/ChatRefreshMock.swift index f36ba48aa..828042c4b 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatRefreshMock.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatRefreshMock.swift @@ -13,15 +13,15 @@ final class ChatRefreshMock: UIRefreshControl { super.init() configure() } - + required init?(coder: NSCoder) { super.init(coder: coder) configure() } } -private extension ChatRefreshMock { - func configure() { +extension ChatRefreshMock { + fileprivate func configure() { tintColor = .clear addTarget(self, action: #selector(endRefreshing), for: .valueChanged) } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift b/Adamant/Modules/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift index 0f41b7987..d727187a0 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatReply/ChatMessageReplyCell+Model.swift @@ -21,7 +21,7 @@ extension ChatMessageReplyCell { let opponentAddress: String var isHidden: Bool var swipeState: ChatSwipeWrapperModel.State - + static var `default`: Self { Self( id: "", @@ -37,7 +37,7 @@ extension ChatMessageReplyCell { swipeState: .idle ) } - + func makeReplyContent() -> NSAttributedString { return message } @@ -48,17 +48,17 @@ extension ChatMessageReplyCell.Model { @MainActor func contentHeight(for width: CGFloat) -> CGFloat { let maxSize = CGSize(width: width, height: .infinity) - + let messageHeight = message.boundingRect( with: maxSize, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil ).height - + return verticalInsets * 2 - + verticalStackSpacing - + messageHeight - + ChatMessageReplyCell.replyViewHeight + + verticalStackSpacing + + messageHeight + + ChatMessageReplyCell.replyViewHeight } } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift b/Adamant/Modules/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift index ae8f2af5d..366c9dc64 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatReply/ChatMessageReplyCell.swift @@ -6,46 +6,46 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit +import AdvancedContextMenuKit +import Combine +import CommonKit import MessageKit import SnapKit -import Combine -import AdvancedContextMenuKit import SwiftUI -import CommonKit +import UIKit final class ChatMessageReplyCell: MessageContentCell, ChatModelView { // MARK: Dependencies - + var chatMessagesListViewModel: ChatMessagesListViewModel? - + // MARK: Proprieties - + /// The labels used to display the message's text. private var messageLabel = MessageLabel() private var replyMessageLabel = UILabel() - + private(set) lazy var swipeWrapper = ChatSwipeWrapper(cellContainerView) private lazy var cellContainerView = UIView() - + static let replyViewHeight: CGFloat = 25 - + private lazy var colorView: UIView = { let view = UIView() view.clipsToBounds = true view.backgroundColor = .adamant.active return view }() - + private lazy var replyView: UIView = { let view = UIView() view.backgroundColor = .lightGray.withAlphaComponent(0.15) view.layer.cornerRadius = 8 view.clipsToBounds = true - + view.addSubview(colorView) view.addSubview(replyMessageLabel) - + colorView.snp.makeConstraints { $0.top.leading.bottom.equalToSuperview() $0.width.equalTo(2) @@ -60,16 +60,16 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { } return view }() - + private var containerView: UIView = UIView() - + private lazy var verticalStack: UIStackView = { let stack = UIStackView(arrangedSubviews: [replyView, messageLabel]) stack.axis = .vertical stack.spacing = 6 return stack }() - + private lazy var reactionsContanerView: UIStackView = { let stack = UIStackView(arrangedSubviews: [ownReactionLabel, opponentReactionLabel]) stack.distribution = .fillProportionally @@ -77,7 +77,7 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { stack.spacing = 6 return stack }() - + private lazy var ownReactionLabel: UILabel = { let label = UILabel() label.text = getReaction(for: model.address) @@ -86,17 +86,17 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { label.textAlignment = .center label.layer.masksToBounds = true label.frame.size = ownReactionSize - + let tapGesture = UITapGestureRecognizer( target: self, action: #selector(tapReactionAction) ) - + label.addGestureRecognizer(tapGesture) label.isUserInteractionEnabled = true return label }() - + private lazy var opponentReactionLabel: UILabel = { let label = UILabel() label.text = getReaction(for: model.opponentAddress) @@ -105,42 +105,42 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { label.backgroundColor = .adamant.pickedReactionBackground label.layer.cornerRadius = opponentReactionSize.height / 2 label.frame.size = opponentReactionSize - + let tapGesture = UITapGestureRecognizer( target: self, action: #selector(tapReactionAction) ) - + label.addGestureRecognizer(tapGesture) label.isUserInteractionEnabled = true return label }() - + private lazy var chatMenuManager = ChatMenuManager(delegate: self) - + // MARK: - Properties - + /// The `MessageCellDelegate` for the cell. override weak var delegate: MessageCellDelegate? { didSet { messageLabel.delegate = delegate } } - + var model: Model = .default { didSet { guard model != oldValue else { return } swipeWrapper.model = .init(id: model.id, state: model.swipeState) containerView.isHidden = model.isHidden replyMessageLabel.attributedText = model.messageReply - + let leading = model.isFromCurrentSender ? smallHInset : longHInset let trailing = model.isFromCurrentSender ? longHInset : smallHInset verticalStack.snp.updateConstraints { $0.leading.equalToSuperview().inset(leading) $0.trailing.equalToSuperview().inset(trailing) } - + reactionsContanerView.isHidden = model.reactions == nil ownReactionLabel.isHidden = getReaction(for: model.address) == nil opponentReactionLabel.isHidden = getReaction(for: model.opponentAddress) == nil @@ -149,29 +149,27 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { layoutReactionLabel() } } - + var reactionsContanerViewWidth: CGFloat { - if getReaction(for: model.address) == nil && - getReaction(for: model.opponentAddress) == nil { + if getReaction(for: model.address) == nil && getReaction(for: model.opponentAddress) == nil { return .zero } - - if getReaction(for: model.address) != nil && - getReaction(for: model.opponentAddress) != nil { + + if getReaction(for: model.address) != nil && getReaction(for: model.opponentAddress) != nil { return ownReactionSize.width + opponentReactionSize.width + 6 } - + if getReaction(for: model.address) != nil { return ownReactionSize.width } - + if getReaction(for: model.opponentAddress) != nil { return opponentReactionSize.width } - + return .zero } - + override var isSelected: Bool { didSet { messageContainerView.animateIsSelected( @@ -180,10 +178,10 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { ) } } - + var actionHandler: (ChatAction) -> Void = { _ in } var subscription: AnyCancellable? - + private var trailingReplyViewOffset: CGFloat = 4 private let smallHInset: CGFloat = 8 private let longHInset: CGFloat = 14 @@ -191,9 +189,9 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { private let opponentReactionSize = CGSize(width: 55, height: 27) private let opponentReactionImageSize = CGSize(width: 12, height: 12) private var layoutAttributes: MessagesCollectionViewLayoutAttributes? - + // MARK: - Methods - + override func prepareForReuse() { super.prepareForReuse() messageLabel.attributedText = nil @@ -202,7 +200,7 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { ownReactionLabel.text = nil opponentReactionLabel.attributedText = nil } - + override func setupSubviews() { contentView.addSubview(swipeWrapper) cellContainerView.addSubviews( @@ -215,11 +213,11 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { avatarView, messageTimestampLabel ) - + messageContainerView.addSubview(verticalStack) messageLabel.numberOfLines = 0 replyMessageLabel.numberOfLines = 1 - + let leading = model.isFromCurrentSender ? smallHInset : longHInset let trailing = model.isFromCurrentSender ? longHInset : smallHInset verticalStack.snp.makeConstraints { @@ -227,38 +225,38 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { $0.leading.equalToSuperview().inset(leading) $0.trailing.equalToSuperview().inset(trailing) } - + configureMenu() - + cellContainerView.addSubview(reactionsContanerView) } - + func configureMenu() { containerView.layer.cornerRadius = 10 - + messageContainerView.removeFromSuperview() cellContainerView.addSubview(containerView) - + containerView.addSubview(messageContainerView) chatMenuManager.setup(for: containerView) } - + func updateOwnReaction() { ownReactionLabel.text = getReaction(for: model.address) ownReactionLabel.backgroundColor = .adamant.pickedReactionBackground } - + func updateOpponentReaction() { guard let reaction = getReaction(for: model.opponentAddress), - let senderPublicKey = getSenderPublicKeyInReaction(for: model.opponentAddress) + let senderPublicKey = getSenderPublicKeyInReaction(for: model.opponentAddress) else { opponentReactionLabel.attributedText = nil opponentReactionLabel.text = nil return } - + let fullString = NSMutableAttributedString(string: reaction) - + if let image = chatMessagesListViewModel?.avatarService.avatar( for: senderPublicKey, size: opponentReactionImageSize.width @@ -269,48 +267,48 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { origin: .init(x: .zero, y: -3), size: opponentReactionImageSize ) - + let imageString = NSAttributedString(attachment: replyImageAttachment) fullString.append(NSAttributedString(string: " ")) fullString.append(imageString) } - + opponentReactionLabel.attributedText = fullString opponentReactionLabel.backgroundColor = .adamant.pickedReactionBackground } - + func getSenderPublicKeyInReaction(for senderAddress: String) -> String? { model.reactions?.first( where: { $0.sender == senderAddress } )?.senderPublicKey } - + func getReaction(for address: String) -> String? { model.reactions?.first( where: { $0.sender == address } )?.reaction } - + override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { super.apply(layoutAttributes) guard let attributes = layoutAttributes as? MessagesCollectionViewLayoutAttributes else { return } - + self.layoutAttributes = attributes } - + /// Positions the message bubble's top label. /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. override func layoutMessageTopLabel( with attributes: MessagesCollectionViewLayoutAttributes ) { - messageTopLabel.textAlignment = attributes.messageTopLabelAlignment.textAlignment - messageTopLabel.textInsets = attributes.messageTopLabelAlignment.textInsets + messageTopLabel.textAlignment = attributes.messageTopLabelAlignment.textAlignment + messageTopLabel.textInsets = attributes.messageTopLabelAlignment.textInsets - let y = containerView.frame.minY - attributes.messageContainerPadding.top - attributes.messageTopLabelSize.height - let origin = CGPoint(x: 0, y: y) + let y = containerView.frame.minY - attributes.messageContainerPadding.top - attributes.messageTopLabelSize.height + let origin = CGPoint(x: 0, y: y) - messageTopLabel.frame = CGRect(origin: origin, size: attributes.messageTopLabelSize) + messageTopLabel.frame = CGRect(origin: origin, size: attributes.messageTopLabelSize) } /// Positions the message bubble's bottom label. @@ -318,15 +316,15 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { override func layoutMessageBottomLabel( with attributes: MessagesCollectionViewLayoutAttributes ) { - messageBottomLabel.textAlignment = attributes.messageBottomLabelAlignment.textAlignment - messageBottomLabel.textInsets = attributes.messageBottomLabelAlignment.textInsets + messageBottomLabel.textAlignment = attributes.messageBottomLabelAlignment.textAlignment + messageBottomLabel.textInsets = attributes.messageBottomLabelAlignment.textInsets - let y = containerView.frame.maxY + attributes.messageContainerPadding.bottom - let origin = CGPoint(x: 0, y: y) + let y = containerView.frame.maxY + attributes.messageContainerPadding.bottom + let origin = CGPoint(x: 0, y: y) - messageBottomLabel.frame = CGRect(origin: origin, size: attributes.messageBottomLabelSize) + messageBottomLabel.frame = CGRect(origin: origin, size: attributes.messageBottomLabelSize) } - + /// Positions the message bubble's time label. /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. override func layoutTimeLabelView( @@ -334,21 +332,21 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { ) { let paddingLeft: CGFloat = 10 let origin = CGPoint( - x: UIScreen.main.bounds.width + paddingLeft, - y: containerView.frame.minY - + containerView.frame.height - * 0.5 - - messageTimestampLabel.font.ascender * 0.5 + x: UIScreen.main.bounds.width + paddingLeft, + y: containerView.frame.minY + + containerView.frame.height + * 0.5 + - messageTimestampLabel.font.ascender * 0.5 ) - + let size = CGSize( width: attributes.messageTimeLabelSize.width, height: attributes.messageTimeLabelSize.height ) - + messageTimestampLabel.frame = CGRect(origin: origin, size: size) } - + /// Positions the cell's `MessageContainerView`. /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. override func layoutMessageContainerView( @@ -358,48 +356,54 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { switch attributes.avatarPosition.vertical { case .messageBottom: - origin.y = attributes.size.height - - attributes.messageContainerPadding.bottom - - attributes.cellBottomLabelSize.height - - attributes.messageBottomLabelSize.height - - attributes.messageContainerSize.height - - attributes.messageContainerPadding.top + origin.y = + attributes.size.height + - attributes.messageContainerPadding.bottom + - attributes.cellBottomLabelSize.height + - attributes.messageBottomLabelSize.height + - attributes.messageContainerSize.height + - attributes.messageContainerPadding.top case .messageCenter: - if attributes.avatarSize.height > attributes.messageContainerSize.height { - let messageHeight = attributes.messageContainerSize.height - + attributes.messageContainerPadding.top - + attributes.messageContainerPadding.bottom - origin.y = (attributes.size.height / 2) - (messageHeight / 2) - } else { - fallthrough - } + if attributes.avatarSize.height > attributes.messageContainerSize.height { + let messageHeight = + attributes.messageContainerSize.height + + attributes.messageContainerPadding.top + + attributes.messageContainerPadding.bottom + origin.y = (attributes.size.height / 2) - (messageHeight / 2) + } else { + fallthrough + } default: - if attributes.accessoryViewSize.height > attributes.messageContainerSize.height { - let messageHeight = attributes.messageContainerSize.height - + attributes.messageContainerPadding.top - + attributes.messageContainerPadding.bottom - origin.y = (attributes.size.height / 2) - (messageHeight / 2) - } else { - origin.y = attributes.cellTopLabelSize.height - + attributes.messageTopLabelSize.height - + attributes.messageContainerPadding.top - } + if attributes.accessoryViewSize.height > attributes.messageContainerSize.height { + let messageHeight = + attributes.messageContainerSize.height + + attributes.messageContainerPadding.top + + attributes.messageContainerPadding.bottom + origin.y = (attributes.size.height / 2) - (messageHeight / 2) + } else { + origin.y = + attributes.cellTopLabelSize.height + + attributes.messageTopLabelSize.height + + attributes.messageContainerPadding.top + } } let avatarPadding = attributes.avatarLeadingTrailingPadding switch attributes.avatarPosition.horizontal { case .cellLeading: - origin.x = attributes.avatarSize.width - + attributes.messageContainerPadding.left - + avatarPadding + origin.x = + attributes.avatarSize.width + + attributes.messageContainerPadding.left + + avatarPadding case .cellTrailing: - origin.x = attributes.frame.width - - attributes.avatarSize.width - - attributes.messageContainerSize.width - - attributes.messageContainerPadding.right - - avatarPadding + origin.x = + attributes.frame.width + - attributes.avatarSize.width + - attributes.messageContainerSize.width + - attributes.messageContainerPadding.right + - avatarPadding case .natural: - break + break } containerView.frame = CGRect( @@ -414,20 +418,23 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { messageContainerView.layoutIfNeeded() layoutReactionLabel() } - + func layoutReactionLabel() { - let additionalWidth: CGFloat = model.isFromCurrentSender - ? .zero - : containerView.frame.width - - var x = containerView.frame.origin.x - + additionalWidth - - reactionsContanerViewWidth / 2 - - let minSpace = model.isFromCurrentSender - ? minReactionsSpacingToOwnBoundary + reactionsContanerViewWidth - : minReactionsSpacingToOwnBoundary - + let additionalWidth: CGFloat = + model.isFromCurrentSender + ? .zero + : containerView.frame.width + + var x = + containerView.frame.origin.x + + additionalWidth + - reactionsContanerViewWidth / 2 + + let minSpace = + model.isFromCurrentSender + ? minReactionsSpacingToOwnBoundary + reactionsContanerViewWidth + : minReactionsSpacingToOwnBoundary + if model.isFromCurrentSender { x = min(x, contentView.bounds.width - minSpace) x = max(x, minReactionsSpacingToOppositeBoundary) @@ -435,62 +442,62 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { x = max(x, minSpace) x = min(x, contentView.bounds.width - minReactionsSpacingToOppositeBoundary - reactionsContanerViewWidth) } - + reactionsContanerView.frame = CGRect( origin: .init( x: x, y: containerView.frame.origin.y - + containerView.frame.height - - reactionsContanerVerticalSpace + + containerView.frame.height + - reactionsContanerVerticalSpace ), size: .init(width: reactionsContanerViewWidth, height: ownReactionSize.height) ) reactionsContanerView.layoutIfNeeded() } - + override func configure( with message: MessageType, at indexPath: IndexPath, and messagesCollectionView: MessagesCollectionView ) { super.configure(with: message, at: indexPath, and: messagesCollectionView) - + guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { return } - + let enabledDetectors = displayDelegate.enabledDetectors(for: message, at: indexPath, in: messagesCollectionView) - + messageLabel.configure { messageLabel.enabledDetectors = enabledDetectors for detector in enabledDetectors { let attributes = displayDelegate.detectorAttributes(for: detector, and: message, at: indexPath) messageLabel.setAttributes(attributes, detector: detector) } - + messageLabel.attributedText = model.message } - + replyMessageLabel.attributedText = model.messageReply updateOwnReaction() updateOpponentReaction() } - + /// Used to handle the cell's contentView's tap gesture. /// Return false when the contentView does not need to handle the gesture. override func cellContentView(canHandle touchPoint: CGPoint) -> Bool { messageLabel.handleGesture(touchPoint) } - + /// Handle tap gesture on contentView and its subviews. override func handleTapGesture(_ gesture: UIGestureRecognizer) { let touchLocation = gesture.location(in: self) - + let containerViewContains = containerView.frame.contains(touchLocation) let canHandle = !cellContentView( canHandle: convert(touchLocation, to: containerView) ) - + switch true { case containerViewContains && canHandle: delegate?.didTapMessage(in: self) @@ -511,7 +518,7 @@ final class ChatMessageReplyCell: MessageContentCell, ChatModelView { delegate?.didTapBackground(in: self) } } - + override func layoutSubviews() { super.layoutSubviews() swipeWrapper.frame = contentView.bounds @@ -527,38 +534,38 @@ extension ChatMessageReplyCell { ) { [actionHandler, id = model.id] in actionHandler(.remove(id: id)) } - + let report = AMenuItem.action( title: .adamant.chat.report, systemImageName: "exclamationmark.bubble" ) { [actionHandler, id = model.id] in actionHandler(.report(id: id)) } - + let reply = AMenuItem.action( title: .adamant.chat.reply, systemImageName: "arrowshape.turn.up.left" ) { [actionHandler, model] in actionHandler(.reply(id: model.id)) } - + let copy = AMenuItem.action( title: .adamant.chat.copy, systemImageName: "doc.on.doc" ) { [actionHandler, model] in actionHandler(.copy(text: model.message.string)) } - + let copyInPart = AMenuItem.action( title: .adamant.chat.selectText, systemImageName: "selection.pin.in.out" ) { [actionHandler, model] in actionHandler(.copyInPart(text: model.message.string)) } - + return AMenuSection([reply, copyInPart, copy, report, remove]) } - + @objc func tapReactionAction() { chatMenuManager.presentMenuProgrammatically(for: containerView) } @@ -573,7 +580,7 @@ extension ChatMessageReplyCell: ChatMenuManagerDelegate { enabledDetectors: messageLabel.enabledDetectors )?.containerView } - + func presentMenu( copyView: UIView, size: CGSize, @@ -599,23 +606,23 @@ extension ChatMessageReplyCell { func copy( with model: Model, attributes: MessagesCollectionViewLayoutAttributes?, - urlAttributes: [NSAttributedString.Key : Any], + urlAttributes: [NSAttributedString.Key: Any], enabledDetectors: [DetectorType] ) -> ChatMessageReplyCell? { guard let attributes = attributes else { return nil } - + let cell = ChatMessageReplyCell(frame: frame) cell.apply(attributes) cell.replyMessageLabel.attributedText = model.messageReply let leading = model.isFromCurrentSender ? cell.smallHInset : cell.longHInset let trailing = model.isFromCurrentSender ? cell.longHInset : cell.smallHInset - + cell.verticalStack.snp.updateConstraints { $0.leading.equalToSuperview().inset(leading) $0.trailing.equalToSuperview().inset(trailing) } - + cell.messageContainerView.backgroundColor = model.backgroundColor.uiColor cell.messageLabel.configure { cell.messageLabel.enabledDetectors = enabledDetectors diff --git a/Adamant/Modules/Chat/View/Subviews/ChatScrollButton.swift b/Adamant/Modules/Chat/View/Subviews/ChatScrollButton.swift index 2aefa615b..f69b76758 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatScrollButton.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatScrollButton.swift @@ -6,12 +6,13 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit import SnapKit +import UIKit enum Position { case up case down + case reaction } final class ChatScrollButton: UIView { @@ -23,37 +24,88 @@ final class ChatScrollButton: UIView { button.setImage(.asset(named: "scrollUp"), for: .normal) case .down: button.setImage(.asset(named: "ScrollDown"), for: .normal) + case .reaction: + let config = UIImage.SymbolConfiguration.init(paletteColors: [.adamant.imageBlack, .adamant.imageBackground]) + let image = UIImage(systemName: "heart.circle.fill")?.withConfiguration(config) + button.setImage(image, for: .normal) + button.imageView?.contentMode = .scaleAspectFit + button.contentVerticalAlignment = .fill + button.contentHorizontalAlignment = .fill } + button.addTarget(self, action: #selector(onTap), for: .touchUpInside) return button }() - + + private lazy var counterLabel: UILabel = { + let label = UILabel() + label.textColor = .lightGray + label.font = .boldSystemFont(ofSize: 10) + label.textAlignment = .center + label.backgroundColor = UIColor.adamant.active + label.layer.cornerRadius = 9 + label.layer.masksToBounds = true + label.isHidden = true + return label + }() + private let position: Position - var action: (() -> Void)? - + init(frame: CGRect = .zero, position: Position) { self.position = position super.init(frame: frame) configure() } - + required init?(coder: NSCoder) { self.position = .down super.init(coder: coder) configure() } + + func updateCounter(_ count: Int) { + counterLabel.text = count > 99 ? "99+" : "\(count)" + counterLabel.isHidden = count == 0 + } } -private extension ChatScrollButton { - func configure() { +extension ChatScrollButton { + fileprivate func configure() { addSubview(button) button.snp.makeConstraints { $0.directionalEdges.equalToSuperview() } + + if position == .reaction || position == .down { + addSubview(counterLabel) + counterLabel.snp.makeConstraints { + $0.centerX.equalToSuperview() + $0.top.equalToSuperview().offset(-9) + $0.width.height.equalTo(18) + } + } + if traitCollection.userInterfaceIdiom == .pad || traitCollection.userInterfaceIdiom == .mac { + let hover = UIHoverGestureRecognizer(target: self, action: #selector(handleHover(_:))) + addGestureRecognizer(hover) + } } - - @objc func onTap() { + + @objc fileprivate func onTap() { action?() } + @objc fileprivate func handleHover(_ gesture: UIHoverGestureRecognizer) { + switch gesture.state { + case .began, .changed: + button.alpha = 1.0 + button.transform = CGAffineTransform(scaleX: 1.05, y: 1.05) + case .ended: + UIView.animate(withDuration: 0.2) { + self.button.alpha = 0.5 + self.button.transform = .identity + } + default: + break + } + } } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatSwipeWrapper/ChatSwipeWrapper.swift b/Adamant/Modules/Chat/View/Subviews/ChatSwipeWrapper/ChatSwipeWrapper.swift index 0ebf182c3..8f1df39aa 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatSwipeWrapper/ChatSwipeWrapper.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatSwipeWrapper/ChatSwipeWrapper.swift @@ -6,44 +6,44 @@ // Copyright © 2024 Adamant. All rights reserved. // -import UIKit import SnapKit +import UIKit final class ChatSwipeWrapper: UIView { var model: ChatSwipeWrapperModel = .default { didSet { update(old: oldValue) } } - + let wrappedView: View - + init(_ wrappedView: View) { self.wrappedView = wrappedView super.init(frame: .zero) configure() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } -private extension ChatSwipeWrapper { - func configure() { +extension ChatSwipeWrapper { + fileprivate func configure() { addSubview(wrappedView) wrappedView.snp.makeConstraints { $0.directionalVerticalEdges.width.leading.equalToSuperview() } } - - func update(old: ChatSwipeWrapperModel) { + + fileprivate func update(old: ChatSwipeWrapperModel) { guard old.state != model.state else { return } - + wrappedView.snp.updateConstraints { $0.leading.equalToSuperview().offset(model.state.value) } - + guard old.id == model.id else { return } - + switch model.state { case .idle: UIView.animate( @@ -53,6 +53,6 @@ private extension ChatSwipeWrapper { ) { [weak self] in self?.layoutIfNeeded() } case .offset: break - } + } } } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatSwipeWrapper/ChatSwipeWrapperModel.swift b/Adamant/Modules/Chat/View/Subviews/ChatSwipeWrapper/ChatSwipeWrapperModel.swift index 95ebf67b4..6064ee610 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatSwipeWrapper/ChatSwipeWrapperModel.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatSwipeWrapper/ChatSwipeWrapperModel.swift @@ -11,7 +11,7 @@ import CoreGraphics struct ChatSwipeWrapperModel: Identifiable, Equatable { let id: String var state: State - + static let `default` = Self(id: .empty, state: .idle) } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatTransaction/ChatTransactionCell.swift b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/ChatTransactionCell.swift index b0d0c29fa..c2d9e6cff 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatTransaction/ChatTransactionCell.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/ChatTransactionCell.swift @@ -6,55 +6,55 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit -import SnapKit -import MessageKit import Combine +import MessageKit +import SnapKit +import UIKit final class ChatTransactionCell: MessageContentCell, ChatModelView { private let transactionView = ChatTransactionContainerView() private let cellContainerView = UIView() private lazy var swipeWrapper = ChatSwipeWrapper(cellContainerView) - + var subscription: AnyCancellable? - + var model: ChatTransactionContainerView.Model = .default { didSet { swipeWrapper.model = .init(id: model.id, state: model.swipeState) transactionView.model = model } } - + var actionHandler: (ChatAction) -> Void { get { transactionView.actionHandler } set { transactionView.actionHandler = newValue } } - + var chatMessagesListViewModel: ChatMessagesListViewModel? { get { transactionView.chatMessagesListViewModel } set { transactionView.chatMessagesListViewModel = newValue } } - + override init(frame: CGRect) { super.init(frame: frame) configure() } - + required init?(coder: NSCoder) { super.init(coder: coder) configure() } - + override func prepareForReuse() { transactionView.prepareForReuse() } - + override var isSelected: Bool { didSet { transactionView.isSelected = isSelected } } - + override func configure( with message: MessageType, at indexPath: IndexPath, @@ -64,7 +64,7 @@ final class ChatTransactionCell: MessageContentCell, ChatModelView { messageContainerView.style = .none messageContainerView.backgroundColor = .clear } - + override func layoutMessageContainerView( with attributes: MessagesCollectionViewLayoutAttributes ) { @@ -72,7 +72,7 @@ final class ChatTransactionCell: MessageContentCell, ChatModelView { transactionView.frame = messageContainerView.frame transactionView.layoutIfNeeded() } - + override func setupSubviews() { cellContainerView.addSubviews( accessoryView, @@ -88,8 +88,8 @@ final class ChatTransactionCell: MessageContentCell, ChatModelView { } } -private extension ChatTransactionCell { - func configure() { +extension ChatTransactionCell { + fileprivate func configure() { contentView.addSubview(swipeWrapper) swipeWrapper.snp.makeConstraints { $0.directionalEdges.equalToSuperview() diff --git a/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView+Model.swift b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView+Model.swift index 043a8911a..9d752d987 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView+Model.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView+Model.swift @@ -18,7 +18,7 @@ extension ChatTransactionContainerView { let address: String let opponentAddress: String var swipeState: ChatSwipeWrapperModel.State - + static var `default`: Self { Self( id: "", @@ -31,15 +31,16 @@ extension ChatTransactionContainerView { swipeState: .idle ) } - + func makeReplyContent() -> NSAttributedString { let commentRaw = content.comment ?? "" - let comment = commentRaw.isEmpty - ? commentRaw - : ": \(commentRaw)" - + let comment = + commentRaw.isEmpty + ? commentRaw + : ": \(commentRaw)" + let content = "\(content.title) \(content.currency) \(content.amount)\(comment)" - + return ChatMessageFactory.markdownParser.parse(content) } } diff --git a/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift index 68513c97e..3b7ce9255 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Container/ChatTransactionContainerView.swift @@ -6,42 +6,42 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit -import SnapKit -import Combine -import SwiftUI import AdvancedContextMenuKit +import Combine import CommonKit +import SnapKit +import SwiftUI +import UIKit final class ChatTransactionContainerView: UIView { // MARK: Dependencies - + var chatMessagesListViewModel: ChatMessagesListViewModel? - + // MARK: Proprieties - + var model: Model = .default { didSet { update() } } - + var actionHandler: (ChatAction) -> Void = { _ in } { didSet { contentView.actionHandler = actionHandler } } - + private let contentView = ChatTransactionContentView() - + private lazy var statusButton: UIButton = { let button = UIButton() button.addTarget(self, action: #selector(onStatusButtonTap), for: .touchUpInside) return button }() - + private let spacingView: UIView = { let view = UIView() view.setContentCompressionResistancePriority(.dragThatCanResizeScene, for: .horizontal) return view }() - + private let horizontalStack: UIStackView = { let stack = UIStackView() stack.alignment = .center @@ -49,23 +49,23 @@ final class ChatTransactionContainerView: UIView { stack.spacing = horizontalStackSpacing return stack }() - + private lazy var vStack: UIStackView = { let stack = UIStackView() stack.alignment = .center stack.axis = .vertical stack.spacing = 12 - + stack.addArrangedSubview(statusButton) stack.addArrangedSubview(ownReactionLabel) stack.addArrangedSubview(opponentReactionLabel) - + stack.snp.makeConstraints { $0.width.equalTo(Self.maxVStackWidth) } return stack }() - + private lazy var ownReactionLabel: UILabel = { let label = UILabel() label.text = getReaction(for: model.address) @@ -73,22 +73,22 @@ final class ChatTransactionContainerView: UIView { label.layer.cornerRadius = ownReactionSize.height / 2 label.textAlignment = .center label.layer.masksToBounds = true - + label.snp.makeConstraints { make in make.width.equalTo(ownReactionSize.width) make.height.equalTo(ownReactionSize.height) } - + let tapGesture = UITapGestureRecognizer( target: self, action: #selector(tapReactionAction) ) - + label.addGestureRecognizer(tapGesture) label.isUserInteractionEnabled = true return label }() - + private lazy var opponentReactionLabel: UILabel = { let label = UILabel() label.text = getReaction(for: model.opponentAddress) @@ -96,42 +96,42 @@ final class ChatTransactionContainerView: UIView { label.layer.masksToBounds = true label.backgroundColor = .adamant.pickedReactionBackground label.layer.cornerRadius = opponentReactionSize.height / 2 - + label.snp.makeConstraints { make in make.width.equalTo(opponentReactionSize.width) make.height.equalTo(opponentReactionSize.height) } - + let tapGesture = UITapGestureRecognizer( target: self, action: #selector(tapReactionAction) ) - + label.addGestureRecognizer(tapGesture) label.isUserInteractionEnabled = true return label }() - + private lazy var chatMenuManager = ChatMenuManager(delegate: self) - + private let ownReactionSize = CGSize(width: 40, height: 27) private let opponentReactionSize = CGSize(width: maxVStackWidth, height: 27) private let opponentReactionImageSize = CGSize(width: 12, height: 12) - + static let horizontalStackSpacing: CGFloat = 12 static let maxVStackWidth: CGFloat = 55 - + var isSelected: Bool = false { didSet { contentView.isSelected = isSelected } } - + override init(frame: CGRect) { super.init(frame: frame) configure() } - + required init?(coder: NSCoder) { super.init(coder: coder) configure() @@ -144,65 +144,66 @@ extension ChatTransactionContainerView: ReusableView { } } -private extension ChatTransactionContainerView { - func configure() { +extension ChatTransactionContainerView { + fileprivate func configure() { addSubview(horizontalStack) horizontalStack.snp.makeConstraints { $0.top.bottom.equalToSuperview() $0.horizontalEdges.equalToSuperview() } - + chatMenuManager.setup(for: contentView) } - - func update() { + + fileprivate func update() { contentView.model = model.content updateStatus(model.status) updateLayout() - + ownReactionLabel.isHidden = getReaction(for: model.address) == nil opponentReactionLabel.isHidden = getReaction(for: model.opponentAddress) == nil updateOwnReaction() updateOpponentReaction() } - - func updateStatus(_ status: TransactionStatus) { + + fileprivate func updateStatus(_ status: TransactionStatus) { statusButton.setImage(status.image, for: .normal) statusButton.tintColor = status.imageTintColor } - - func updateLayout() { + + fileprivate func updateLayout() { var viewsList = [spacingView, vStack, contentView] - - viewsList = model.isFromCurrentSender + + viewsList = + model.isFromCurrentSender ? viewsList : viewsList.reversed() - + guard horizontalStack.arrangedSubviews != viewsList else { return } horizontalStack.arrangedSubviews.forEach { horizontalStack.removeArrangedSubview($0) } viewsList.forEach { horizontalStack.addArrangedSubview($0) } } - - @objc func onStatusButtonTap() { + + @objc fileprivate func onStatusButtonTap() { actionHandler(.forceUpdateTransactionStatus(id: model.id)) } - - func updateOwnReaction() { + + fileprivate func updateOwnReaction() { ownReactionLabel.text = getReaction(for: model.address) ownReactionLabel.backgroundColor = .adamant.pickedReactionBackground } - - func updateOpponentReaction() { + + fileprivate func updateOpponentReaction() { guard let reaction = getReaction(for: model.opponentAddress), - let senderPublicKey = getSenderPublicKeyInReaction(for: model.opponentAddress) + let senderPublicKey = getSenderPublicKeyInReaction(for: model.opponentAddress) else { opponentReactionLabel.attributedText = nil opponentReactionLabel.text = nil return } - + let fullString = NSMutableAttributedString(string: reaction) - + if let image = chatMessagesListViewModel?.avatarService.avatar( for: senderPublicKey, size: opponentReactionImageSize.width @@ -213,29 +214,29 @@ private extension ChatTransactionContainerView { origin: .init(x: .zero, y: -3), size: opponentReactionImageSize ) - + let imageString = NSAttributedString(attachment: replyImageAttachment) fullString.append(NSAttributedString(string: " ")) fullString.append(imageString) } - + opponentReactionLabel.attributedText = fullString opponentReactionLabel.backgroundColor = .adamant.pickedReactionBackground } - - func getSenderPublicKeyInReaction(for senderAddress: String) -> String? { + + fileprivate func getSenderPublicKeyInReaction(for senderAddress: String) -> String? { model.reactions?.first( where: { $0.sender == senderAddress } )?.senderPublicKey } - - func getReaction(for address: String) -> String? { + + fileprivate func getReaction(for address: String) -> String? { model.reactions?.first( where: { $0.sender == address } )?.reaction } - - @objc func tapReactionAction() { + + @objc fileprivate func tapReactionAction() { chatMenuManager.presentMenuProgrammatically(for: contentView) } } @@ -247,8 +248,8 @@ extension ChatTransactionContainerView.Model { } } -private extension TransactionStatus { - var image: UIImage { +extension TransactionStatus { + fileprivate var image: UIImage { switch self { case .notInitiated: return .asset(named: "status_updating") ?? .init() case .pending, .registered: return .asset(named: "status_pending") ?? .init() @@ -257,8 +258,8 @@ private extension TransactionStatus { case .inconsistent: return .asset(named: "status_warning") ?? .init() } } - - var imageTintColor: UIColor { + + fileprivate var imageTintColor: UIColor { switch self { case .notInitiated: return .adamant.secondary case .pending, .registered: return .adamant.primary @@ -277,21 +278,21 @@ extension ChatTransactionContainerView { ) { [actionHandler, model] in actionHandler(.remove(id: model.id)) } - + let report = AMenuItem.action( title: .adamant.chat.report, systemImageName: "exclamationmark.bubble" ) { [actionHandler, model] in actionHandler(.report(id: model.id)) } - + let reply = AMenuItem.action( title: .adamant.chat.reply, systemImageName: "arrowshape.turn.up.left" ) { [actionHandler, model] in actionHandler(.reply(id: model.id)) } - + return AMenuSection([reply, report, remove]) } } @@ -300,7 +301,7 @@ extension ChatTransactionContainerView: ChatMenuManagerDelegate { func getCopyView() -> UIView? { copy(with: model)?.contentView } - + func presentMenu( copyView: UIView, size: CGSize, diff --git a/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView+Model.swift b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView+Model.swift index 9d18a90d2..2e9f3e318 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView+Model.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView+Model.swift @@ -22,7 +22,7 @@ extension ChatTransactionContentView { var replyMessage: NSAttributedString var replyId: String var isHidden: Bool - + static var `default`: Self { Self( id: "", diff --git a/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift index d2f824e18..bf2f77a1e 100644 --- a/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift +++ b/Adamant/Modules/Chat/View/Subviews/ChatTransaction/Content/ChatTransactionContentView.swift @@ -6,9 +6,9 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit -import SnapKit import CommonKit +import SnapKit +import UIKit final class ChatTransactionContentView: UIView { var model: Model = .default { @@ -17,7 +17,7 @@ final class ChatTransactionContentView: UIView { update() } } - + var isSelected: Bool = false { didSet { animateIsSelected( @@ -26,44 +26,44 @@ final class ChatTransactionContentView: UIView { ) } } - + var actionHandler: (ChatAction) -> Void = { _ in } - + private let titleLabel = UILabel(font: titleFont, textColor: .adamant.textColor) private let amountLabel = UILabel(font: .systemFont(ofSize: 24), textColor: .adamant.textColor) private let currencyLabel = UILabel(font: .systemFont(ofSize: 20), textColor: .adamant.textColor) private let dateLabel = UILabel(font: dateFont, textColor: .adamant.textColor) - + private let commentLabel = UILabel( font: commentFont, textColor: .adamant.textColor, numberOfLines: .zero ) - + var replyViewDynamicHeight: CGFloat { model.isReply ? replyViewHeight : 0 } - + private var replyMessageLabel = UILabel() - + private lazy var colorView: UIView = { let view = UIView() view.clipsToBounds = true view.backgroundColor = .adamant.active return view }() - + private lazy var replyView: UIView = { let view = UIView() view.backgroundColor = .lightGray.withAlphaComponent(0.15) view.layer.cornerRadius = 5 view.clipsToBounds = true - + view.addSubview(colorView) view.addSubview(replyMessageLabel) - + replyMessageLabel.numberOfLines = 1 - + colorView.snp.makeConstraints { $0.top.leading.bottom.equalToSuperview() $0.width.equalTo(2) @@ -78,13 +78,13 @@ final class ChatTransactionContentView: UIView { } return view }() - + private let iconView: UIImageView = { let view = UIImageView() view.contentMode = .scaleAspectFit return view }() - + private lazy var moneyInfoView: UIView = { let view = UIView() view.addSubview(iconView) @@ -92,40 +92,40 @@ final class ChatTransactionContentView: UIView { $0.top.bottom.leading.equalToSuperview() $0.size.equalTo(iconSize) } - + view.addSubview(amountLabel) amountLabel.snp.makeConstraints { $0.top.trailing.equalToSuperview() $0.leading.equalTo(iconView.snp.trailing).offset(8) } - + view.addSubview(currencyLabel) currencyLabel.snp.makeConstraints { $0.top.equalTo(amountLabel.snp.bottom) $0.leading.equalTo(amountLabel.snp.leading) $0.trailing.equalToSuperview() } - + return view }() - + private lazy var verticalStack: UIStackView = { let stack = UIStackView(arrangedSubviews: [replyView, titleLabel, moneyInfoView, dateLabel, commentLabel]) stack.axis = .vertical stack.spacing = verticalStackSpacing return stack }() - + override init(frame: CGRect) { super.init(frame: frame) configure() } - + required init?(coder: NSCoder) { super.init(coder: coder) configure() } - + func setFixWidth(width: CGFloat) { snp.remakeConstraints { $0.width.lessThanOrEqualTo(width) @@ -139,44 +139,46 @@ extension ChatTransactionContentView.Model { let opponentReactionWidth = ChatTransactionContainerView.maxVStackWidth let containerHorizontalOffset = ChatTransactionContainerView.horizontalStackSpacing * 2 let contentHorizontalOffset = horizontalInsets * 2 - + let maxSize = CGSize( width: width - - opponentReactionWidth - - containerHorizontalOffset - - contentHorizontalOffset, + - opponentReactionWidth + - containerHorizontalOffset + - contentHorizontalOffset, height: .infinity ) let titleString = NSAttributedString(string: title, attributes: [.font: titleFont]) let dateString = NSAttributedString(string: date, attributes: [.font: dateFont]) - - let commentString = comment?.isEmpty == true - ? nil - : comment.map { - NSAttributedString(string: $0, attributes: [.font: commentFont]) - } - + + let commentString = + comment?.isEmpty == true + ? nil + : comment.map { + NSAttributedString(string: $0, attributes: [.font: commentFont]) + } + let titleHeight = titleString.boundingRect( with: maxSize, options: .usesLineFragmentOrigin, context: nil ).height - + let dateHeight = dateString.boundingRect( with: maxSize, options: .usesLineFragmentOrigin, context: nil ).height - - let commentHeight: CGFloat = commentString?.boundingRect( - with: maxSize, - options: .usesLineFragmentOrigin, - context: nil - ).height ?? .zero - + + let commentHeight: CGFloat = + commentString?.boundingRect( + with: maxSize, + options: .usesLineFragmentOrigin, + context: nil + ).height ?? .zero + let replyViewDynamicHeight: CGFloat = isReply ? replyViewHeight : 0 let stackSpacingCount: CGFloat = isReply ? 4 : 3 - + return verticalStackSpacing * stackSpacingCount + iconSize + titleHeight @@ -186,23 +188,25 @@ extension ChatTransactionContentView.Model { } } -private extension ChatTransactionContentView { - func configure() { +extension ChatTransactionContentView { + fileprivate func configure() { layer.cornerRadius = 16 - - addGestureRecognizer(UITapGestureRecognizer( - target: self, - action: #selector(didTap) - )) - + + addGestureRecognizer( + UITapGestureRecognizer( + target: self, + action: #selector(didTap) + ) + ) + addSubview(verticalStack) verticalStack.snp.makeConstraints { $0.top.bottom.equalToSuperview().inset(verticalInsets) $0.leading.trailing.equalToSuperview().inset(horizontalInsets) } } - - func update() { + + fileprivate func update() { alpha = model.isHidden ? .zero : 1.0 backgroundColor = model.backgroundColor.uiColor titleLabel.text = model.title @@ -213,38 +217,42 @@ private extension ChatTransactionContentView { commentLabel.text = model.comment commentLabel.isHidden = model.comment == nil replyView.isHidden = !model.isReply - + if model.isReply { replyMessageLabel.attributedText = model.replyMessage } else { replyMessageLabel.attributedText = nil } - + replyView.snp.updateConstraints { make in make.height.equalTo(replyViewDynamicHeight) } } - - @objc func didTap(_ gesture: UIGestureRecognizer) { + + @objc fileprivate func didTap(_ gesture: UIGestureRecognizer) { let touchLocation = gesture.location(in: self) - + if replyView.frame.contains(touchLocation) { - actionHandler(.scrollTo(message: .init( - id: model.id, - replyId: model.replyId, - message: NSAttributedString(string: ""), - messageReply: NSAttributedString(string: ""), - backgroundColor: .failed, - isFromCurrentSender: true, - reactions: nil, - address: "", - opponentAddress: "", - isHidden: false, - swipeState: .idle - ))) + actionHandler( + .scrollTo( + message: .init( + id: model.id, + replyId: model.replyId, + message: NSAttributedString(string: ""), + messageReply: NSAttributedString(string: ""), + backgroundColor: .failed, + isFromCurrentSender: true, + reactions: nil, + address: "", + opponentAddress: "", + isHidden: false, + swipeState: .idle + ) + ) + ) return } - + actionHandler(.openTransactionDetails(id: model.id)) } } diff --git a/Adamant/Modules/Chat/View/Subviews/FilesToolBarView/FilesToolbarCollectionViewCell.swift b/Adamant/Modules/Chat/View/Subviews/FilesToolBarView/FilesToolbarCollectionViewCell.swift index bbba2e6c2..5896ca758 100644 --- a/Adamant/Modules/Chat/View/Subviews/FilesToolBarView/FilesToolbarCollectionViewCell.swift +++ b/Adamant/Modules/Chat/View/Subviews/FilesToolBarView/FilesToolbarCollectionViewCell.swift @@ -6,10 +6,10 @@ // Copyright © 2024 Adamant. All rights reserved. // -import UIKit -import SnapKit -import FilesStorageKit import CommonKit +import FilesStorageKit +import SnapKit +import UIKit final class FilesToolbarCollectionViewCell: UICollectionViewCell { private lazy var imageView = UIImageView(image: .init(systemName: "shareplay")) @@ -22,35 +22,35 @@ final class FilesToolbarCollectionViewCell: UICollectionViewCell { view.backgroundColor = .secondarySystemBackground view.layer.masksToBounds = true view.layer.cornerRadius = 5 - + view.addSubview(imageView) view.addSubview(nameLabel) view.addSubview(additionalLabel) view.addSubview(videoIconIV) - + imageView.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() make.bottom.equalTo(nameLabel.snp.top).offset(-7) } - + nameLabel.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview().inset(5) make.bottom.equalToSuperview().offset(-7) make.height.equalTo(17) } - + additionalLabel.snp.makeConstraints { make in make.center.equalTo(imageView.snp.center) } - + videoIconIV.snp.makeConstraints { make in make.center.equalTo(imageView.snp.center) make.size.equalTo(30) } - + return view }() - + private lazy var removeBtn: UIButton = { let btn = UIButton() btn.setImage(.asset(named: "checkMarkIcon"), for: .normal) @@ -58,67 +58,67 @@ final class FilesToolbarCollectionViewCell: UICollectionViewCell { btn.addTarget(self, action: #selector(didTapRemoveBtn), for: .touchUpInside) return btn }() - + var buttonActionHandler: ((Int) -> Void)? - + // MARK: - Init - + override init(frame: CGRect) { super.init(frame: frame) configure() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + @objc private func didTapRemoveBtn() { buttonActionHandler?(removeBtn.tag) } - + func update(_ file: FileResult, tag: Int) { imageView.image = file.preview ?? defaultImage removeBtn.tag = tag - + let fileType = file.extenstion ?? .empty let fileName = file.name ?? "UNKNWON" - + nameLabel.text = "\(fileName).\(fileType)" - + additionalLabel.text = fileType.uppercased() additionalLabel.isHidden = file.preview != nil - + videoIconIV.isHidden = file.type != .video layoutConstraints(file) } } -private extension FilesToolbarCollectionViewCell { - func configure() { +extension FilesToolbarCollectionViewCell { + fileprivate func configure() { addSubview(containerView) containerView.snp.makeConstraints { make in make.directionalEdges.equalToSuperview().inset(5) } - + addSubview(removeBtn) removeBtn.snp.makeConstraints { make in make.top.equalTo(containerView.snp.top).offset(1) make.trailing.equalTo(containerView.snp.trailing).offset(-1) make.size.equalTo(25) } - + imageView.layer.masksToBounds = true imageView.contentMode = .scaleAspectFill nameLabel.textAlignment = .center nameLabel.lineBreakMode = .byTruncatingMiddle - + removeBtn.addShadow() - + videoIconIV.tintColor = .adamant.active videoIconIV.addShadow() } - - func layoutConstraints(_ file: FileResult) { + + fileprivate func layoutConstraints(_ file: FileResult) { if file.preview == nil { imageView.snp.remakeConstraints { make in make.top.equalToSuperview() @@ -127,7 +127,7 @@ private extension FilesToolbarCollectionViewCell { } return } - + imageView.snp.remakeConstraints { make in make.top.leading.trailing.equalToSuperview() make.bottom.equalTo(nameLabel.snp.top).offset(-7) diff --git a/Adamant/Modules/Chat/View/Subviews/FilesToolBarView/FilesToolbarView.swift b/Adamant/Modules/Chat/View/Subviews/FilesToolBarView/FilesToolbarView.swift index 03b98f85c..bea1965c6 100644 --- a/Adamant/Modules/Chat/View/Subviews/FilesToolBarView/FilesToolbarView.swift +++ b/Adamant/Modules/Chat/View/Subviews/FilesToolBarView/FilesToolbarView.swift @@ -6,10 +6,10 @@ // Copyright © 2024 Adamant. All rights reserved. // -import UIKit -import SnapKit -import FilesStorageKit import CommonKit +import FilesStorageKit +import SnapKit +import UIKit final class FilesToolbarView: UIView { private lazy var collectionView: UICollectionView = { @@ -17,9 +17,11 @@ final class FilesToolbarView: UIView { flow.minimumInteritemSpacing = 5 flow.minimumLineSpacing = 5 flow.scrollDirection = .horizontal - + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flow) - collectionView.register(FilesToolbarCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: FilesToolbarCollectionViewCell.self) + collectionView.register( + FilesToolbarCollectionViewCell.self, + forCellWithReuseIdentifier: String(describing: FilesToolbarCollectionViewCell.self) ) collectionView.backgroundColor = .clear collectionView.delegate = self @@ -27,7 +29,7 @@ final class FilesToolbarView: UIView { collectionView.showsHorizontalScrollIndicator = false return collectionView }() - + private lazy var containerView: UIView = { let view = UIView() view.addSubview(collectionView) @@ -37,7 +39,7 @@ final class FilesToolbarView: UIView { } return view }() - + private lazy var closeBtn: UIButton = { let btn = UIButton() btn.setImage( @@ -45,39 +47,39 @@ final class FilesToolbarView: UIView { for: .normal ) btn.addTarget(self, action: #selector(didTapCloseBtn), for: .touchUpInside) - + btn.snp.makeConstraints { make in make.size.equalTo(30) } return btn }() - + private lazy var horizontalStack: UIStackView = { let stack = UIStackView(arrangedSubviews: [containerView, closeBtn]) stack.axis = .horizontal stack.spacing = horizontalStackSpacing return stack }() - + // MARK: Proprieties - + private var data: [FileResult] = [] var closeAction: (() -> Void)? var updatedDataAction: (([FileResult]) -> Void)? var openFileAction: ((FileResult) -> Void)? - + // MARK: Init - + override init(frame: CGRect) { super.init(frame: frame) configure() } - + required init?(coder: NSCoder) { super.init(coder: coder) configure() } - + func configure() { addSubview(horizontalStack) horizontalStack.snp.makeConstraints { @@ -85,13 +87,13 @@ final class FilesToolbarView: UIView { $0.horizontalEdges.equalToSuperview().inset(horizontalInsets) } } - + // MARK: Actions - + @objc private func didTapCloseBtn() { closeAction?() } - + private func removeFile(at index: Int) { data.remove(at: index) collectionView.reloadData() @@ -120,25 +122,27 @@ extension FilesToolbarView: UICollectionViewDelegate, UICollectionViewDataSource ) -> Int { data.count } - + func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath ) -> UICollectionViewCell { - guard let cell = collectionView.dequeueReusableCell( - withReuseIdentifier: String(describing: FilesToolbarCollectionViewCell.self), - for: indexPath - ) as? FilesToolbarCollectionViewCell else { + guard + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: String(describing: FilesToolbarCollectionViewCell.self), + for: indexPath + ) as? FilesToolbarCollectionViewCell + else { return UICollectionViewCell() } - + cell.update(data[indexPath.row], tag: indexPath.row) cell.buttonActionHandler = { [weak self] index in self?.removeFile(at: index) } return cell } - + func collectionView( _ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, @@ -149,7 +153,7 @@ extension FilesToolbarView: UICollectionViewDelegate, UICollectionViewDataSource height: self.frame.height - itemOffset ) } - + func collectionView( _ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath diff --git a/Adamant/Modules/Chat/View/Subviews/NewMessagesCell.swift b/Adamant/Modules/Chat/View/Subviews/NewMessagesCell.swift new file mode 100644 index 000000000..fbdd25b20 --- /dev/null +++ b/Adamant/Modules/Chat/View/Subviews/NewMessagesCell.swift @@ -0,0 +1,52 @@ +import CommonKit +import MessageKit +// +// NewMessagesCell.swift +// Adamant +// +// Created by Владимир Клевцов on 14.3.25.. +// Copyright © 2025 Adamant. All rights reserved. +// +import UIKit + +final class NewMessagesCell: MessageReusableView { + private let separatorLine: UIView = { + let view = UIView() + view.backgroundColor = .adamant.newMessageLineColor + return view + }() + + private let label: UILabel = { + let label = UILabel() + label.text = String.localized("chat.NewMessages") + label.font = .boldSystemFont(ofSize: 14) + label.textColor = .adamant.active + label.textAlignment = .right + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + + addSubview(separatorLine) + addSubview(label) + + separatorLine.translatesAutoresizingMaskIntoConstraints = false + label.translatesAutoresizingMaskIntoConstraints = false + + let pixelSize = 1 / UIScreen.main.scale + NSLayoutConstraint.activate([ + separatorLine.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20), + separatorLine.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20), + separatorLine.topAnchor.constraint(equalTo: topAnchor), + separatorLine.heightAnchor.constraint(equalToConstant: pixelSize), + + label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20), + label.topAnchor.constraint(equalTo: separatorLine.bottomAnchor, constant: 1) + ]) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/Adamant/Modules/Chat/ViewModel/ChatCacheService.swift b/Adamant/Modules/Chat/ViewModel/ChatCacheService.swift index 23977381f..92731ebae 100644 --- a/Adamant/Modules/Chat/ViewModel/ChatCacheService.swift +++ b/Adamant/Modules/Chat/ViewModel/ChatCacheService.swift @@ -6,29 +6,29 @@ // Copyright © 2023 Adamant. All rights reserved. // -import Foundation import Combine +import Foundation @MainActor final class ChatCacheService { private var messages: [String: [ChatMessage]] = [:] private var subscriptions = Set() - + nonisolated init() { Task { await setup() } } - + func setMessages(address: String, messages: [ChatMessage]) { self.messages[address] = messages } - + func getMessages(address: String) -> [ChatMessage]? { messages[address] } } -private extension ChatCacheService { - func setup() { +extension ChatCacheService { + fileprivate func setup() { NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedOut) .sink { @MainActor [weak self] _ in self?.messages = .init() } diff --git a/Adamant/Modules/Chat/ViewModel/ChatFileService.swift b/Adamant/Modules/Chat/ViewModel/ChatFileService.swift index 96d3494d3..76ba516e1 100644 --- a/Adamant/Modules/Chat/ViewModel/ChatFileService.swift +++ b/Adamant/Modules/Chat/ViewModel/ChatFileService.swift @@ -6,11 +6,11 @@ // Copyright © 2024 Adamant. All rights reserved. // -import CommonKit -import UIKit import Combine -import FilesStorageKit +import CommonKit import CoreData +import FilesStorageKit +import UIKit private struct FileUpload { let file: FileResult @@ -24,40 +24,43 @@ private struct FileMessage { var files: [FileUpload] var message: String? var txId: String? + var adamantMessage: AdamantMessage? } @MainActor final class ChatFileService: ChatFileProtocol, Sendable { typealias UploadResult = (decodedData: Data, encodedData: Data, nonce: String, cid: String) - + typealias UploadFileResult = (file: UploadResult, preview: UploadResult?) + // MARK: Dependencies - + private let accountService: AccountService private let filesStorage: FilesStorageProtocol private let chatsProvider: ChatsProvider private let filesNetworkManager: FilesNetworkManagerProtocol private let adamantCore: AdamantCore - + private(set) var downloadingFiles: [String: DownloadStatus] = [:] private(set) var uploadingFiles: [String] = [] private(set) var filesLoadingProgress: [String: Int] = [:] - + private var ignoreFilesIDsArray: [String] = [] private var busyFilesIDs: [String] = [] private var fileDownloadAttemptsCount: [String: Int] = [:] private var uploadingFilesDictionary: [String: FileMessage] = [:] private var previewDownloadsAttemps: [String: Int] = [:] + private var uploadTasks: [String: Task] = [:] private let synchronizer = AsyncStreamSender<@MainActor () -> Void>() private let _updateFileFields = ObservableSender() - + private var subscriptions = Set() private let maxDownloadAttemptsCount = 3 private let maxDownloadPreivewAttemptsCount = 2 - + var updateFileFields: AnyObservable { _updateFileFields.eraseToAnyPublisher() } - + init( accountService: AccountService, filesStorage: FilesStorageProtocol, @@ -70,10 +73,10 @@ final class ChatFileService: ChatFileProtocol, Sendable { self.chatsProvider = chatsProvider self.filesNetworkManager = filesNetworkManager self.adamantCore = adamantCore - + addObservers() } - + func sendFile( text: String?, chatroom: Chatroom?, @@ -82,7 +85,7 @@ final class ChatFileService: ChatFileProtocol, Sendable { saveEncrypted: Bool ) async throws { guard let filesPicked = filesPicked else { return } - + let files = filesPicked.map { FileUpload( file: $0, @@ -92,9 +95,9 @@ final class ChatFileService: ChatFileProtocol, Sendable { preview: nil ) } - + let fileMessage = FileMessage.init(files: files) - + try await sendFile( text: text, chatroom: chatroom, @@ -103,7 +106,7 @@ final class ChatFileService: ChatFileProtocol, Sendable { saveEncrypted: saveEncrypted ) } - + func resendMessage( with id: String, text: String?, @@ -112,7 +115,7 @@ final class ChatFileService: ChatFileProtocol, Sendable { saveEncrypted: Bool ) async throws { guard let fileMessage = uploadingFilesDictionary[id] else { return } - + try await sendFile( text: text, chatroom: chatroom, @@ -121,7 +124,7 @@ final class ChatFileService: ChatFileProtocol, Sendable { saveEncrypted: saveEncrypted ) } - + func downloadFile( file: ChatFile, chatroom: Chatroom?, @@ -131,7 +134,7 @@ final class ChatFileService: ChatFileProtocol, Sendable { ) async throws { let isCachedOriginal = filesStorage.isCachedLocally(file.file.id) let isCachedPreview = filesStorage.isCachedInMemory(file.file.preview?.id ?? .empty) - + try await downloadFile( file: file, chatroom: chatroom, @@ -140,7 +143,7 @@ final class ChatFileService: ChatFileProtocol, Sendable { saveEncrypted: saveEncrypted ) } - + func autoDownload( file: ChatFile, chatroom: Chatroom?, @@ -150,18 +153,18 @@ final class ChatFileService: ChatFileProtocol, Sendable { saveEncrypted: Bool ) async { guard !downloadingFiles.keys.contains(file.file.id), - !ignoreFilesIDsArray.contains(file.file.id), - !busyFilesIDs.contains(file.file.id) + !ignoreFilesIDsArray.contains(file.file.id), + !busyFilesIDs.contains(file.file.id) else { return } - + defer { busyFilesIDs.removeAll { $0 == file.file.id } } - + busyFilesIDs.append(file.file.id) - + await handleAutoDownload( file: file, chatroom: chatroom, @@ -171,7 +174,7 @@ final class ChatFileService: ChatFileProtocol, Sendable { saveEncrypted: saveEncrypted ) } - + func getDecodedData( file: FilesStorageKit.File, nonce: String, @@ -180,33 +183,48 @@ final class ChatFileService: ChatFileProtocol, Sendable { guard let keyPair = accountService.keypair else { throw FileManagerError.cantDecryptFile } - + let data = try Data(contentsOf: file.url) - + guard file.isEncrypted else { return data } - - guard let decodedData = adamantCore.decodeData( - data, - rawNonce: nonce, - senderPublicKey: chatroom?.partner?.publicKey ?? .empty, - privateKey: keyPair.privateKey - ) else { + + guard + let decodedData = adamantCore.decodeData( + data, + rawNonce: nonce, + senderPublicKey: chatroom?.partner?.publicKey ?? .empty, + privateKey: keyPair.privateKey + ) + else { throw FileManagerError.cantDecryptFile } - + return decodedData } - + + func cancelUpload(messageId: String, fileId: String) async { + if let task = uploadTasks[fileId] { + task.cancel() + uploadTasks[fileId] = nil + uploadingFiles.removeAll { $0 == fileId } + } else { + await removeFromRichFile( + oldId: fileId, + txId: messageId + ) + } + } + func isDownloadPreviewLimitReached(for fileId: String) -> Bool { let count = previewDownloadsAttemps[fileId] ?? .zero guard count < maxDownloadPreivewAttemptsCount else { return true } - + previewDownloadsAttemps[fileId] = count + 1 return false } - + func isPreviewAutoDownloadAllowedByPolicy( hasPartnerName: Bool, isFromCurrentSender: Bool, @@ -218,7 +236,7 @@ final class ChatFileService: ChatFileProtocol, Sendable { case .contacts: hasPartnerName || isFromCurrentSender } } - + func isOriginalAutoDownloadAllowedByPolicy( hasPartnerName: Bool, isFromCurrentSender: Bool, @@ -232,18 +250,18 @@ final class ChatFileService: ChatFileProtocol, Sendable { } } -private extension ChatFileService { - func addObservers() { +extension ChatFileService { + fileprivate func addObservers() { NotificationCenter.default .notifications(named: .AdamantReachabilityMonitor.reachabilityChanged) .sink { @MainActor [weak self] data in let connection = data.userInfo?[AdamantUserInfoKey.ReachabilityMonitor.connection] as? Bool - + guard connection == true else { return } self?.ignoreFilesIDsArray.removeAll() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .Storage.storageClear) .sink { @MainActor [weak self] _ in @@ -252,15 +270,15 @@ private extension ChatFileService { self?.fileDownloadAttemptsCount.removeAll() } .store(in: &subscriptions) - + synchronizer.stream.sink { @MainActor action in action() }.store(in: &subscriptions) } } -private extension ChatFileService { - func handleAutoDownload( +extension ChatFileService { + fileprivate func handleAutoDownload( file: ChatFile, chatroom: Chatroom?, hasPartnerName: Bool, @@ -273,18 +291,18 @@ private extension ChatFileService { previewDownloadPolicy: previewDownloadPolicy, hasPartnerName: hasPartnerName ) - + let shouldDownloadOriginalFile = shouldAutoDownloadOriginal( file: file, fullMediaDownloadPolicy: fullMediaDownloadPolicy, hasPartnerName: hasPartnerName ) - + guard shouldDownloadOriginalFile || shouldDownloadPreviewFile else { cacheFileToMemoryIfNeeded(file: file, chatroom: chatroom) return } - + do { try await downloadFile( file: file, @@ -304,8 +322,8 @@ private extension ChatFileService { ) } } - - func handleDownloadError( + + fileprivate func handleDownloadError( file: ChatFile, chatroom: Chatroom?, hasPartnerName: Bool, @@ -314,14 +332,14 @@ private extension ChatFileService { saveEncrypted: Bool ) async { let count = fileDownloadAttemptsCount[file.file.id] ?? .zero - + guard count < maxDownloadAttemptsCount else { ignoreFilesIDsArray.append(file.file.id) return } - + fileDownloadAttemptsCount[file.file.id] = count + 1 - + await handleAutoDownload( file: file, chatroom: chatroom, @@ -331,61 +349,63 @@ private extension ChatFileService { saveEncrypted: saveEncrypted ) } - - func cacheFileToMemoryIfNeeded( + + fileprivate func cacheFileToMemoryIfNeeded( file: ChatFile, chatroom: Chatroom? ) { guard let id = file.file.preview?.id, - let nonce = file.file.preview?.nonce, - let fileDTO = try? filesStorage.getFile(with: id).get(), - fileDTO.isPreview, - filesStorage.isCachedLocally(id), - !filesStorage.isCachedInMemory(id), - let image = try? cacheFileToMemory( + let nonce = file.file.preview?.nonce, + let fileDTO = try? filesStorage.getFile(with: id).get(), + fileDTO.isPreview, + filesStorage.isCachedLocally(id), + !filesStorage.isCachedInMemory(id), + let image = try? cacheFileToMemory( id: id, file: fileDTO, nonce: nonce, chatroom: chatroom - ) + ) else { return } - - _updateFileFields.send(.init( - id: file.file.id, - newId: nil, - fileNonce: nil, - preview: .some(image), - cached: nil, - downloadStatus: nil, - uploading: nil, - progress: nil - )) + + _updateFileFields.send( + .init( + id: file.file.id, + newId: nil, + fileNonce: nil, + preview: .some(image), + cached: nil, + downloadStatus: nil, + uploading: nil, + progress: nil + ) + ) } - - func cacheFileToMemory( + + fileprivate func cacheFileToMemory( id: String, file: FilesStorageKit.File, nonce: String, chatroom: Chatroom? ) throws -> UIImage? { let data = try Data(contentsOf: file.url) - + guard file.isEncrypted else { return filesStorage.cacheImageToMemoryIfNeeded(id: id, data: data) } - + let decodedData = try getDecodedData( file: file, nonce: nonce, chatroom: chatroom ) - + return filesStorage.cacheImageToMemoryIfNeeded(id: id, data: decodedData) } - - func downloadFile( + + fileprivate func downloadFile( file: ChatFile, chatroom: Chatroom?, shouldDownloadOriginalFile: Bool, @@ -393,19 +413,19 @@ private extension ChatFileService { saveEncrypted: Bool ) async throws { guard let keyPair = accountService.keypair, - let ownerId = accountService.account?.address, - let recipientId = chatroom?.partner?.address, - NetworkFileProtocolType(rawValue: file.storage) != nil, - (shouldDownloadOriginalFile || shouldDownloadPreviewFile), - !downloadingFiles.keys.contains(file.file.id) + let ownerId = accountService.account?.address, + let recipientId = chatroom?.partner?.address, + NetworkFileProtocolType(rawValue: file.storage) != nil, + shouldDownloadOriginalFile || shouldDownloadPreviewFile, + !downloadingFiles.keys.contains(file.file.id) else { return } - + guard !file.file.id.isEmpty, - !file.file.nonce.isEmpty + !file.file.nonce.isEmpty else { throw FileManagerError.cantDownloadFile } - + defer { downloadingFiles[file.file.id] = nil sendUpdate( @@ -417,19 +437,21 @@ private extension ChatFileService { uploading: nil ) } - - let downloadFile = shouldDownloadOriginalFile - && !filesStorage.isCachedLocally(file.file.id) - - let downloadPreview = file.file.preview != nil - && shouldDownloadPreviewFile - && !filesStorage.isCachedLocally(file.file.preview?.id ?? .empty) - + + let downloadFile = + shouldDownloadOriginalFile + && !filesStorage.isCachedLocally(file.file.id) + + let downloadPreview = + file.file.preview != nil + && shouldDownloadPreviewFile + && !filesStorage.isCachedLocally(file.file.preview?.id ?? .empty) + let downloadStatus: DownloadStatus = .init( isPreviewDownloading: downloadPreview, isOriginalDownloading: downloadFile ) - + downloadingFiles[file.file.id] = downloadStatus // Here we start showing progress from the last saved value (fileProgressValue) instead of zero because in the UI we need to show progress when the download is frozen. We have N attempts to download, and the progress is overridden. @@ -441,24 +463,24 @@ private extension ChatFileService { isOriginalDownloading: downloadFile ), uploading: nil, - progress: downloadFile + progress: downloadFile ? filesLoadingProgress[file.file.id] ?? .zero : nil ) - + let totalProgress = Progress(totalUnitCount: 100) - + let (previewWeight, fileWeight) = getProgressWeights( downloadPreview: false, downloadFile: downloadFile ) - + let previewProgress = Progress(totalUnitCount: previewWeight) totalProgress.addChild(previewProgress, withPendingUnitCount: previewWeight) - + let fileProgress = Progress(totalUnitCount: fileWeight) totalProgress.addChild(fileProgress, withPendingUnitCount: fileWeight) - + if let previewDTO = file.file.preview { if downloadPreview { try await downloadAndCacheFile( @@ -477,24 +499,26 @@ private extension ChatFileService { previewProgress.completedUnitCount = Int64(value.fractionCompleted * Double(previewWeight)) } ) - + let preview = filesStorage.getPreview(for: previewDTO.id) - - _updateFileFields.send(.init( - id: file.file.id, - newId: nil, - fileNonce: nil, - preview: .some(preview), - cached: nil, - downloadStatus: nil, - uploading: nil, - progress: nil - )) + + _updateFileFields.send( + .init( + id: file.file.id, + newId: nil, + fileNonce: nil, + preview: .some(preview), + cached: nil, + downloadStatus: nil, + uploading: nil, + progress: nil + ) + ) } else if !filesStorage.isCachedInMemory(previewDTO.id) { cacheFileToMemoryIfNeeded(file: file, chatroom: chatroom) } } - + if downloadFile { try await downloadAndCacheFile( id: file.file.id, @@ -511,7 +535,7 @@ private extension ChatFileService { downloadProgress: { [synchronizer] value in synchronizer.send { [weak self] in fileProgress.completedUnitCount = Int64(value.fractionCompleted * Double(fileWeight)) - + self?.sendProgress( for: file.file.id, progress: Int(totalProgress.fractionCompleted * 100) @@ -519,29 +543,31 @@ private extension ChatFileService { } } ) - + let cached = filesStorage.isCachedLocally(file.file.id) - - _updateFileFields.send(.init( - id: file.file.id, - newId: nil, - fileNonce: nil, - preview: nil, - cached: cached, - downloadStatus: nil, - uploading: nil, - progress: nil - )) + + _updateFileFields.send( + .init( + id: file.file.id, + newId: nil, + fileNonce: nil, + preview: nil, + cached: cached, + downloadStatus: nil, + uploading: nil, + progress: nil + ) + ) } } - - func getProgressWeights( + + fileprivate func getProgressWeights( downloadPreview: Bool, downloadFile: Bool ) -> (previewWeight: Int64, fileWeight: Int64) { var previewWeight: Int64 = .zero var fileWeight: Int64 = .zero - + if downloadPreview && downloadFile { previewWeight = 10 fileWeight = 90 @@ -552,11 +578,11 @@ private extension ChatFileService { previewWeight = .zero fileWeight = 100 } - + return (previewWeight, fileWeight) } - - func downloadAndCacheFile( + + fileprivate func downloadAndCacheFile( id: String, nonce: String, storage: String, @@ -579,7 +605,7 @@ private extension ChatFileService { saveEncrypted: saveEncrypted, downloadProgress: downloadProgress ) - + try filesStorage.cacheFile( id: id, fileExtension: fileExtension, @@ -593,23 +619,23 @@ private extension ChatFileService { isPreview: isPreview ) } - - func shouldAutoDownloadOriginal( + + fileprivate func shouldAutoDownloadOriginal( file: ChatFile, fullMediaDownloadPolicy: DownloadPolicy, hasPartnerName: Bool ) -> Bool { let isMedia = file.fileType == .image || file.fileType == .video let isCached = filesStorage.isCachedLocally(file.file.id) - + return isOriginalAutoDownloadAllowedByPolicy( hasPartnerName: hasPartnerName, isFromCurrentSender: file.isFromCurrentSender, downloadPolicy: fullMediaDownloadPolicy ) && !isCached && isMedia } - - func shouldAutoDownloadPreview( + + fileprivate func shouldAutoDownloadPreview( file: ChatFile, previewDownloadPolicy: DownloadPolicy, hasPartnerName: Bool @@ -620,15 +646,15 @@ private extension ChatFileService { !ignoreFilesIDsArray.contains(previewId), !filesStorage.isCachedLocally(previewId) else { return false } - + return isPreviewAutoDownloadAllowedByPolicy( hasPartnerName: hasPartnerName, isFromCurrentSender: file.isFromCurrentSender, downloadPolicy: previewDownloadPolicy ) } - - func downloadFile( + + fileprivate func downloadFile( id: String, storage: String, senderPublicKey: String, @@ -642,66 +668,72 @@ private extension ChatFileService { type: storage, downloadProgress: downloadProgress ).get() - - guard let decodedData = adamantCore.decodeData( - encodedData, - rawNonce: nonce, - senderPublicKey: senderPublicKey, - privateKey: recipientPrivateKey - ) else { + + guard + let decodedData = adamantCore.decodeData( + encodedData, + rawNonce: nonce, + senderPublicKey: senderPublicKey, + privateKey: recipientPrivateKey + ) + else { throw FileManagerError.cantDecryptFile } - + return (decodedData, encodedData) } } -private extension ChatFileService { - func sendUpdate( +extension ChatFileService { + fileprivate func sendUpdate( for files: [String], downloadStatus: DownloadStatus?, uploading: Bool?, progress: Int? = nil ) { files.forEach { id in - _updateFileFields.send(.init( - id: id, - newId: nil, - fileNonce: nil, - preview: nil, - cached: nil, - downloadStatus: downloadStatus, - uploading: uploading, - progress: progress - )) - + _updateFileFields.send( + .init( + id: id, + newId: nil, + fileNonce: nil, + preview: nil, + cached: nil, + downloadStatus: downloadStatus, + uploading: uploading, + progress: progress + ) + ) + if progress != nil { filesLoadingProgress[id] = progress } } } - - func sendProgress(for fileId: String, progress: Int) { + + fileprivate func sendProgress(for fileId: String, progress: Int) { guard filesLoadingProgress[fileId] != progress else { return } - - _updateFileFields.send(.init( - id: fileId, - newId: nil, - fileNonce: nil, - preview: nil, - cached: nil, - downloadStatus: nil, - uploading: nil, - progress: progress - )) - + + _updateFileFields.send( + .init( + id: fileId, + newId: nil, + fileNonce: nil, + preview: nil, + cached: nil, + downloadStatus: nil, + uploading: nil, + progress: progress + ) + ) + filesLoadingProgress[fileId] = progress } } // MARK: Upload -private extension ChatFileService { - func sendFile( +extension ChatFileService { + fileprivate func sendFile( text: String?, chatroom: Chatroom?, fileMessage: FileMessage?, @@ -709,78 +741,77 @@ private extension ChatFileService { saveEncrypted: Bool ) async throws { guard let partnerAddress = chatroom?.partner?.address, - let keyPair = accountService.keypair, - let ownerId = accountService.account?.address, - var fileMessage = fileMessage, - chatroom?.partner?.isDummy != true + let keyPair = accountService.keypair, + let ownerId = accountService.account?.address, + var fileMessage = fileMessage, + chatroom?.partner?.isDummy != true else { return } - + let storageProtocol = NetworkFileProtocolType.ipfs let files = fileMessage.files - var richFiles = createRichFiles(from: files) - + let richFiles = createRichFiles(from: files) + let messageLocally = createAdamantMessage( with: richFiles, text: text, replyMessage: replyMessage, storageProtocol: storageProtocol ) - + fileMessage.adamantMessage = messageLocally + cachePreviewFiles(files) - + let txId = try await sendMessageLocallyIfNeeded( fileMessage: fileMessage, partnerAddress: partnerAddress, chatroom: chatroom, messageLocally: messageLocally ) - + fileMessage.txId = txId - + let needToLoadFiles = richFiles.filter { $0.nonce.isEmpty } updateUploadingFilesIDs(with: needToLoadFiles.map { $0.id }, uploading: true) uploadingFilesDictionary[txId] = fileMessage do { try await processFilesUpload( - fileMessage: &fileMessage, + fileMessage: fileMessage, chatroom: chatroom, keyPair: keyPair, storageProtocol: storageProtocol, ownerId: ownerId, partnerAddress: partnerAddress, - saveEncrypted: saveEncrypted, - txId: txId, - richFiles: &richFiles, - messageLocally: messageLocally - ) - - let message = createAdamantMessage( - with: richFiles, - text: text, - replyMessage: replyMessage, - storageProtocol: storageProtocol + saveEncrypted: saveEncrypted, + txId: txId ) - + + guard let fileMessage = uploadingFilesDictionary[txId], + let adamantMessage = fileMessage.adamantMessage, + !fileMessage.files.isEmpty + else { + return await chatsProvider.removeMessage(with: txId) + } + _ = try await chatsProvider.sendFileMessage( - message, + adamantMessage, recipientId: partnerAddress, transactionLocalyId: txId, from: chatroom ) - + uploadingFilesDictionary[txId] = nil } catch { await handleUploadError( for: needToLoadFiles, txId: txId ) - + throw error } } - - func createRichFiles(from files: [FileUpload]) -> [RichMessageFile.File] { + + fileprivate func createRichFiles(from files: [FileUpload]) -> [RichMessageFile.File] { files.compactMap { .init( id: $0.serverFileID ?? $0.file.url.absoluteString, @@ -789,20 +820,21 @@ private extension ChatFileService { name: $0.file.name, extension: $0.file.extenstion, mimeType: $0.file.mimeType, - preview: $0.preview ?? $0.file.previewUrl.map { - RichMessageFile.Preview( - id: $0.absoluteString, - nonce: .empty, - extension: .empty - ) - }, + preview: $0.preview + ?? $0.file.previewUrl.map { + RichMessageFile.Preview( + id: $0.absoluteString, + nonce: .empty, + extension: .empty + ) + }, resolution: $0.file.resolution, duration: $0.file.duration ) } } - - func createAdamantMessage( + + fileprivate func createAdamantMessage( with richFiles: [RichMessageFile.File], text: String?, replyMessage: MessageModel?, @@ -817,7 +849,7 @@ private extension ChatFileService { ) ) } - + return .richMessage( payload: RichFileReply( replyto_id: replyMessage.id, @@ -829,8 +861,8 @@ private extension ChatFileService { ) ) } - - func cachePreviewFiles(_ files: [FileUpload]) { + + fileprivate func cachePreviewFiles(_ files: [FileUpload]) { let needToCache = files.filter { !$0.isUploaded } for url in needToCache.compactMap({ $0.file.previewUrl }) { filesStorage.cacheTemporaryFile( @@ -841,18 +873,18 @@ private extension ChatFileService { ) } } - - func sendMessageLocallyIfNeeded( + + fileprivate func sendMessageLocallyIfNeeded( fileMessage: FileMessage, partnerAddress: String, chatroom: Chatroom?, messageLocally: AdamantMessage ) async throws -> String { let txId: String - + if let transactionId = fileMessage.txId { txId = transactionId - + try? await chatsProvider.setTxMessageStatus( txId: txId, status: .pending @@ -865,11 +897,11 @@ private extension ChatFileService { ) txId = txLocallyId } - + return txId } - - func updateUploadingFilesIDs(with ids: [String], uploading: Bool) { + + fileprivate func updateUploadingFilesIDs(with ids: [String], uploading: Bool) { if uploading { uploadingFiles.append(contentsOf: ids) } else { @@ -877,7 +909,7 @@ private extension ChatFileService { uploadingFiles.removeAll { $0 == id } } } - + sendUpdate( for: ids, downloadStatus: nil, @@ -885,24 +917,15 @@ private extension ChatFileService { progress: uploading ? .zero : nil ) } - - func processFilesUpload( - fileMessage: inout FileMessage, + + fileprivate func createFileUploadTask( + file: FileResult, chatroom: Chatroom?, keyPair: Keypair, storageProtocol: NetworkFileProtocolType, - ownerId: String, - partnerAddress: String, - saveEncrypted: Bool, - txId: String, - richFiles: inout [RichMessageFile.File], - messageLocally: AdamantMessage - ) async throws { - let files = fileMessage.files - - for i in files.indices where !files[i].isUploaded { - let file = files[i].file - + saveEncrypted: Bool + ) -> Task { + return Task { let uploadProgress: @Sendable (Int) -> Void = { [synchronizer, file] value in synchronizer.send { [weak self] in self?.sendProgress( @@ -911,43 +934,85 @@ private extension ChatFileService { ) } } - + let result = try await uploadFileToServer( file: file, recipientPublicKey: chatroom?.partner?.publicKey ?? .empty, senderPrivateKey: keyPair.privateKey, - storageProtocol: storageProtocol, + storageProtocol: storageProtocol, progress: uploadProgress ) - - sendProgress( - for: result.file.cid, - progress: 100 - ) - - try cacheUploadedFile( - fileResult: result.file, - previewResult: result.preview, + + return result + } + } + + fileprivate func processFilesUpload( + fileMessage: FileMessage, + chatroom: Chatroom?, + keyPair: Keypair, + storageProtocol: NetworkFileProtocolType, + ownerId: String, + partnerAddress: String, + saveEncrypted: Bool, + txId: String + ) async throws { + let files = fileMessage.files + + for i in files.indices where !files[i].isUploaded { + let file = files[i].file + + // We possible already cancelled uploading this file, but effectively we didn't start uploading it + guard uploadingFiles.contains(file.url.absoluteString) else { continue } + + let uploadTask = createFileUploadTask( file: file, - ownerId: ownerId, - partnerAddress: partnerAddress, + chatroom: chatroom, + keyPair: keyPair, + storageProtocol: storageProtocol, saveEncrypted: saveEncrypted ) - - await updateRichFile( - oldId: file.url.absoluteString, - fileResult: result.file, - previewResult: result.preview, - fileMessage: &fileMessage, - richFiles: &richFiles, - file: file, - txId: txId, - messageLocally: messageLocally - ) + + uploadTasks[file.url.absoluteString] = uploadTask + + defer { + uploadTasks[file.url.absoluteString] = nil + } + + do { + let result = try await uploadTask.value + + sendProgress( + for: result.file.cid, + progress: 100 + ) + + try cacheUploadedFile( + fileResult: result.file, + previewResult: result.preview, + file: file, + ownerId: ownerId, + partnerAddress: partnerAddress, + saveEncrypted: saveEncrypted + ) + + await updateRichFile( + oldId: file.url.absoluteString, + fileResult: result.file, + previewResult: result.preview, + file: file, + txId: txId + ) + } catch is CancellationError { + await removeFromRichFile( + oldId: file.url.absoluteString, + txId: txId + ) + } } } - - func cacheUploadedFile( + + fileprivate func cacheUploadedFile( fileResult: UploadResult, previewResult: UploadResult?, file: FileResult, @@ -967,9 +1032,10 @@ private extension ChatFileService { fileType: file.type, isPreview: false ) - + if let previewUrl = file.previewUrl, - let previewResult = previewResult { + let previewResult = previewResult + { try filesStorage.cacheFile( id: previewResult.cid, fileExtension: file.previewExtension ?? .empty, @@ -984,47 +1050,52 @@ private extension ChatFileService { ) } } - - func updateRichFile( + + fileprivate func updateRichFile( oldId: String, fileResult: UploadResult, previewResult: UploadResult?, - fileMessage: inout FileMessage, - richFiles: inout [RichMessageFile.File], file: FileResult, - txId: String, - messageLocally: AdamantMessage + txId: String ) async { let cached = filesStorage.isCachedLocally(fileResult.cid) uploadingFiles.removeAll { $0 == oldId } - - _updateFileFields.send(.init( - id: oldId, - newId: fileResult.cid, - fileNonce: fileResult.nonce, - preview: .some(filesStorage.getPreview(for: previewResult?.cid ?? .empty)), - cached: cached, - downloadStatus: nil, - uploading: false, - progress: nil - )) - + + _updateFileFields.send( + .init( + id: oldId, + newId: fileResult.cid, + fileNonce: fileResult.nonce, + preview: .some(filesStorage.getPreview(for: previewResult?.cid ?? .empty)), + cached: cached, + downloadStatus: nil, + uploading: false, + progress: nil + ) + ) + var previewDTO: RichMessageFile.Preview? if let cid = previewResult?.cid, - let nonce = previewResult?.nonce { + let nonce = previewResult?.nonce + { previewDTO = .init( id: cid, nonce: nonce, extension: file.previewExtension ) } - + + guard var (fileMessage, richMessage) = uploadingFilesDictionary[richMessageId: txId] + else { return } + + var richFiles = richMessage.files + if let index = richFiles.firstIndex(where: { $0.id == oldId }) { richFiles[index].id = fileResult.cid richFiles[index].nonce = fileResult.nonce richFiles[index].preview = previewDTO } - + if let index = fileMessage.files.firstIndex(where: { $0.file.url.absoluteString == oldId }) { @@ -1034,32 +1105,58 @@ private extension ChatFileService { fileMessage.files[index].preview = previewDTO uploadingFilesDictionary[txId] = fileMessage } - - guard case let .richMessage(payload) = messageLocally, - var richMessage = payload as? RichMessageFile - else { return } - + richMessage.files = richFiles - + fileMessage.adamantMessage = .richMessage(payload: richMessage) + uploadingFilesDictionary[txId] = fileMessage + try? await chatsProvider.updateTxMessageContent( txId: txId, richMessage: richMessage ) } - - func handleUploadError( + + fileprivate func removeFromRichFile( + oldId: String, + txId: String + ) async { + uploadingFiles.removeAll { $0 == oldId } + + guard var (fileMessage, richMessage) = uploadingFilesDictionary[richMessageId: txId] + else { return } + + richMessage.files = richMessage.files.filter { $0.id != oldId } + fileMessage.adamantMessage = .richMessage(payload: richMessage) + + let updatedFiles = fileMessage.files.filter { $0.file.url.absoluteString != oldId } + + fileMessage.files = updatedFiles + uploadingFilesDictionary[txId] = fileMessage + + if !updatedFiles.isEmpty { + // skip double update which causes bugs + // first update: here + // second update: if fileMessages is empty in sendFile method + try? await chatsProvider.updateTxMessageContent( + txId: txId, + richMessage: richMessage + ) + } + } + + fileprivate func handleUploadError( for richFiles: [RichMessageFile.File], txId: String ) async { updateUploadingFilesIDs(with: richFiles.map { $0.id }, uploading: false) - + try? await chatsProvider.setTxMessageStatus( txId: txId, status: .failed ) } - - func uploadFileToServer( + + fileprivate func uploadFileToServer( file: FileResult, recipientPublicKey: String, senderPrivateKey: String, @@ -1069,18 +1166,18 @@ private extension ChatFileService { let totalProgress = Progress(totalUnitCount: 100) var previewWeight: Int64 = .zero var fileWeight: Int64 = 100 - + if file.previewUrl != nil { previewWeight = 10 fileWeight = 90 } - + let previewProgress = Progress(totalUnitCount: previewWeight) totalProgress.addChild(previewProgress, withPendingUnitCount: previewWeight) - + let fileProgress = Progress(totalUnitCount: fileWeight) totalProgress.addChild(fileProgress, withPendingUnitCount: fileWeight) - + let result = try await uploadFile( url: file.url, recipientPublicKey: recipientPublicKey, @@ -1091,9 +1188,9 @@ private extension ChatFileService { progress(Int(totalProgress.fractionCompleted * 100)) } ) - + var preview: UploadResult? - + if let url = file.previewUrl { preview = try await uploadFile( url: url, @@ -1106,11 +1203,11 @@ private extension ChatFileService { } ) } - + return (result, preview) } - - func uploadFile( + + fileprivate func uploadFile( url: URL, recipientPublicKey: String, senderPrivateKey: String, @@ -1121,27 +1218,51 @@ private extension ChatFileService { url.stopAccessingSecurityScopedResource() } _ = url.startAccessingSecurityScopedResource() - + let data = try Data(contentsOf: url) - + let encodedResult = adamantCore.encodeData( data, recipientPublicKey: recipientPublicKey, privateKey: senderPrivateKey ) - + guard let encodedData = encodedResult?.data, - let nonce = encodedResult?.nonce + let nonce = encodedResult?.nonce else { throw FileManagerError.cantEncryptFile } - - let cid = try await filesNetworkManager.uploadFiles( + + try Task.checkCancellation() + + let result = await filesNetworkManager.uploadFiles( encodedData, type: storageProtocol, uploadProgress: uploadProgress - ).get() - - return (data, encodedData, nonce, cid) + ) + + switch result { + case let .success(cid): + return (data, encodedData, nonce, cid) + case let .failure(error): + if case .apiError(let apiError) = error, + apiError == .requestCancelled + { + try Task.checkCancellation() + } + + throw error + } + } +} + +extension Dictionary where Key == String, Value == FileMessage { + fileprivate subscript(richMessageId txId: String) -> (FileMessage, RichMessageFile)? { + guard let fileMessage = self[txId], + case let .richMessage(payload) = fileMessage.adamantMessage, + let richMessage = payload as? RichMessageFile + else { return nil } + + return (fileMessage, richMessage) } } diff --git a/Adamant/Modules/Chat/ViewModel/ChatMessageFactory.swift b/Adamant/Modules/Chat/ViewModel/ChatMessageFactory.swift index 24c5f1455..199297137 100644 --- a/Adamant/Modules/Chat/ViewModel/ChatMessageFactory.swift +++ b/Adamant/Modules/Chat/ViewModel/ChatMessageFactory.swift @@ -6,15 +6,15 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit -@preconcurrency import MarkdownKit -import MessageKit import CommonKit import FilesStorageKit +@preconcurrency import MarkdownKit +import MessageKit +import UIKit struct ChatMessageFactory: Sendable { private let walletServiceCompose: WalletServiceCompose - + static let markdownParser = MarkdownParser( font: .adamantChatDefault, color: .adamant.primary, @@ -42,7 +42,7 @@ struct ChatMessageFactory: Sendable { MarkdownFileRaw(emoji: "📄", font: .adamantChatFileRawDefault) ] ) - + static let markdownReplyParser = MarkdownParser( font: .adamantChatReplyDefault, color: .adamant.primary, @@ -66,11 +66,11 @@ struct ChatMessageFactory: Sendable { MarkdownFileRaw(emoji: "📄", font: .adamantChatFileRawDefault) ] ) - + init(walletServiceCompose: WalletServiceCompose) { self.walletServiceCompose = walletServiceCompose } - + func makeMessage( _ transaction: ChatTransaction, expireDate: inout Date?, @@ -86,18 +86,18 @@ struct ChatMessageFactory: Sendable { messageStatus: transaction.statusEnum, blockId: transaction.blockId ) - + let backgroundColor = getBackgroundColor( isFromCurrentSender: isFromCurrentSender, status: status ) - + let content = makeContent( transaction, isFromCurrentSender: currentSender.senderId == senderModel.senderId, backgroundColor: backgroundColor ) - + return .init( id: transaction.chatMessageId ?? "", sentDate: sentDate, @@ -112,14 +112,15 @@ struct ChatMessageFactory: Sendable { content: content ).map { .init(string: $0) }, dateHeader: makeDateHeader(sentDate: sentDate), - topSpinnerOn: topSpinnerOn, - dateHeaderIsHidden: !dateHeaderOn + topSpinnerOn: topSpinnerOn, + dateHeaderIsHidden: !dateHeaderOn, + isUnread: checkTransactionForUnreadReaction(transaction: transaction) ) } } -private extension ChatMessageFactory { - func makeContent( +extension ChatMessageFactory { + fileprivate func makeContent( _ transaction: ChatTransaction, isFromCurrentSender: Bool, backgroundColor: ChatMessageBackgroundColor @@ -133,25 +134,24 @@ private extension ChatMessageFactory { ) case let transaction as RichMessageTransaction: if transaction.additionalType == .reply, - !transaction.isTransferReply(), - !transaction.isFileReply() { + !transaction.isTransferReply(), + !transaction.isFileReply() + { return makeReplyContent( transaction, isFromCurrentSender: isFromCurrentSender, backgroundColor: backgroundColor ) } - - if transaction.additionalType == .file || - (transaction.additionalType == .reply && - transaction.isFileReply()) { + + if transaction.additionalType == .file || (transaction.additionalType == .reply && transaction.isFileReply()) { return makeFileContent( transaction, isFromCurrentSender: isFromCurrentSender, backgroundColor: backgroundColor ) } - + return makeContent( transaction, isFromCurrentSender: isFromCurrentSender, @@ -167,8 +167,8 @@ private extension ChatMessageFactory { return .default } } - - func makeContent( + + fileprivate func makeContent( _ transaction: MessageTransaction, isFromCurrentSender: Bool, backgroundColor: ChatMessageBackgroundColor @@ -176,160 +176,176 @@ private extension ChatMessageFactory { transaction.message.map { let text = makeAttributed($0) let reactions = transaction.reactions - - let address = transaction.isOutgoing - ? transaction.senderAddress - : transaction.recipientAddress - - let opponentAddress = transaction.isOutgoing - ? transaction.recipientAddress - : transaction.senderAddress - - return .message(.init( - value: .init( - id: transaction.txId, - text: text, - backgroundColor: backgroundColor, - isFromCurrentSender: isFromCurrentSender, - reactions: reactions, - address: address, - opponentAddress: opponentAddress, - isFake: transaction.isFake, - isHidden: false, - swipeState: .idle + + let address = + transaction.isOutgoing + ? transaction.senderAddress + : transaction.recipientAddress + + let opponentAddress = + transaction.isOutgoing + ? transaction.recipientAddress + : transaction.senderAddress + + return .message( + .init( + value: .init( + id: transaction.txId, + text: text, + backgroundColor: backgroundColor, + isFromCurrentSender: isFromCurrentSender, + reactions: reactions, + address: address, + opponentAddress: opponentAddress, + isFake: transaction.isFake, + isHidden: false, + swipeState: .idle + ) ) - )) + ) } ?? .default } - - func makeReplyContent( + + fileprivate func makeReplyContent( _ transaction: RichMessageTransaction, isFromCurrentSender: Bool, backgroundColor: ChatMessageBackgroundColor ) -> ChatMessage.Content { guard let replyId = transaction.getRichValue(for: RichContentKeys.reply.replyToId), - let replyMessageRaw = transaction.getRichValue(for: RichContentKeys.reply.replyMessage) + let replyMessageRaw = transaction.getRichValue(for: RichContentKeys.reply.replyMessage) else { return .default } - + let replyMessage = makeAttributed(replyMessageRaw) let decodedMessage = transaction.getRichValue(for: RichContentKeys.reply.decodedReplyMessage) ?? "..." let decodedMessageMarkDown = Self.markdownReplyParser.parse(decodedMessage).resolveLinkColor() let reactions = transaction.richContent?[RichContentKeys.react.reactions] as? Set - - let address = transaction.isOutgoing - ? transaction.senderAddress - : transaction.recipientAddress - - let opponentAddress = transaction.isOutgoing - ? transaction.recipientAddress - : transaction.senderAddress - - return .reply(.init( - value: .init( - id: transaction.txId, - replyId: replyId, - message: replyMessage, - messageReply: decodedMessageMarkDown, - backgroundColor: backgroundColor, - isFromCurrentSender: isFromCurrentSender, - reactions: reactions, - address: address, - opponentAddress: opponentAddress, - isHidden: false, - swipeState: .idle + + let address = + transaction.isOutgoing + ? transaction.senderAddress + : transaction.recipientAddress + + let opponentAddress = + transaction.isOutgoing + ? transaction.recipientAddress + : transaction.senderAddress + + return .reply( + .init( + value: .init( + id: transaction.txId, + replyId: replyId, + message: replyMessage, + messageReply: decodedMessageMarkDown, + backgroundColor: backgroundColor, + isFromCurrentSender: isFromCurrentSender, + reactions: reactions, + address: address, + opponentAddress: opponentAddress, + isHidden: false, + swipeState: .idle + ) ) - )) + ) } - - func makeContent( + + fileprivate func makeContent( _ transaction: RichMessageTransaction, isFromCurrentSender: Bool, backgroundColor: ChatMessageBackgroundColor ) -> ChatMessage.Content { guard let transfer = transaction.transfer else { return .default } let id = transaction.chatMessageId ?? "" - + let decodedMessage = transaction.getRichValue(for: RichContentKeys.reply.decodedReplyMessage) ?? "..." let decodedMessageMarkDown = Self.markdownReplyParser.parse(decodedMessage).resolveLinkColor() let replyId = transaction.getRichValue(for: RichContentKeys.reply.replyToId) ?? "" let reactions = transaction.richContent?[RichContentKeys.react.reactions] as? Set - - let address = transaction.isOutgoing - ? transaction.senderAddress - : transaction.recipientAddress - - let opponentAddress = transaction.isOutgoing - ? transaction.recipientAddress - : transaction.senderAddress - + + let address = + transaction.isOutgoing + ? transaction.senderAddress + : transaction.recipientAddress + + let opponentAddress = + transaction.isOutgoing + ? transaction.recipientAddress + : transaction.senderAddress + let coreService = walletServiceCompose.getWallet(by: transfer.type)?.core let defaultIcon: UIImage = .asset(named: "no-token")?.withTintColor(.adamant.primary) ?? .init() - - return .transaction(.init(value: .init( - id: id, - isFromCurrentSender: isFromCurrentSender, - content: .init( - id: id, - title: isFromCurrentSender - ? .adamant.chat.transactionSent - : .adamant.chat.transactionReceived, - icon: coreService?.tokenLogo ?? defaultIcon, - amount: AdamantBalanceFormat.full.format(transfer.amount), - currency: coreService?.tokenSymbol ?? .adamant.transfer.unknownToken, - date: transaction.sentDate?.humanizedDateTime(withWeekday: false) ?? "", - comment: transfer.comments, - backgroundColor: backgroundColor, - isReply: transaction.isTransferReply(), - replyMessage: decodedMessageMarkDown, - replyId: replyId, - isHidden: false - ), - status: transaction.transactionStatus ?? .notInitiated, - reactions: reactions, - address: address, - opponentAddress: opponentAddress, - swipeState: .idle - ))) + + return .transaction( + .init( + value: .init( + id: id, + isFromCurrentSender: isFromCurrentSender, + content: .init( + id: id, + title: isFromCurrentSender + ? .adamant.chat.transactionSent + : .adamant.chat.transactionReceived, + icon: coreService?.tokenLogo ?? defaultIcon, + amount: AdamantBalanceFormat.full.format(transfer.amount), + currency: coreService?.tokenSymbol ?? .adamant.transfer.unknownToken, + date: transaction.sentDate?.humanizedDateTime(withWeekday: false) ?? "", + comment: transfer.comments, + backgroundColor: backgroundColor, + isReply: transaction.isTransferReply(), + replyMessage: decodedMessageMarkDown, + replyId: replyId, + isHidden: false + ), + status: transaction.transactionStatus ?? .notInitiated, + reactions: reactions, + address: address, + opponentAddress: opponentAddress, + swipeState: .idle + ) + ) + ) } - - func makeFileContent( + + fileprivate func makeFileContent( _ transaction: RichMessageTransaction, isFromCurrentSender: Bool, backgroundColor: ChatMessageBackgroundColor ) -> ChatMessage.Content { let id = transaction.chatMessageId ?? "" - + let files: [[String: Any]] = transaction.getRichValue(for: RichContentKeys.file.files) ?? [[:]] - + let decodedMessage = decodeMessage(transaction) let storageData: [String: Any] = transaction.getRichValue(for: RichContentKeys.file.storage) ?? [:] let storage = RichMessageFile.Storage(storageData).id - + let commentRaw = transaction.getRichValue(for: RichContentKeys.file.comment) ?? .empty let replyId = transaction.getRichValue(for: RichContentKeys.reply.replyToId) ?? .empty let reactions = transaction.richContent?[RichContentKeys.react.reactions] as? Set let comment = makeAttributed(commentRaw) - - let address = transaction.isOutgoing - ? transaction.senderAddress - : transaction.recipientAddress - - let opponentAddress = transaction.isOutgoing - ? transaction.recipientAddress - : transaction.senderAddress - + + let address = + transaction.isOutgoing + ? transaction.senderAddress + : transaction.recipientAddress + + let opponentAddress = + transaction.isOutgoing + ? transaction.recipientAddress + : transaction.senderAddress + let chatFiles = makeChatFiles( from: files, isFromCurrentSender: isFromCurrentSender, storage: storage ) - + let isMediaFilesOnly = chatFiles.allSatisfy { $0.fileType == .image || $0.fileType == .video } - + let fileModel = ChatMediaContentView.FileModel( messageId: id, files: chatFiles, @@ -338,53 +354,57 @@ private extension ChatMessageFactory { txStatus: transaction.statusEnum, showAutoDownloadWarningLabel: false ) - - return .file(.init(value: .init( - id: id, - isFromCurrentSender: isFromCurrentSender, - reactions: reactions, - content: .init( - id: id, - fileModel: fileModel, - isHidden: false, - isFromCurrentSender: isFromCurrentSender, - isReply: transaction.isFileReply(), - replyMessage: decodedMessage, - replyId: replyId, - comment: comment, - backgroundColor: backgroundColor - ), - address: address, - opponentAddress: opponentAddress, - txStatus: transaction.statusEnum, - status: .failed, - swipeState: .idle - ))) + + return .file( + .init( + value: .init( + id: id, + isFromCurrentSender: isFromCurrentSender, + reactions: reactions, + content: .init( + id: id, + fileModel: fileModel, + isHidden: false, + isFromCurrentSender: isFromCurrentSender, + isReply: transaction.isFileReply(), + replyMessage: decodedMessage, + replyId: replyId, + comment: comment, + backgroundColor: backgroundColor + ), + address: address, + opponentAddress: opponentAddress, + txStatus: transaction.statusEnum, + status: .failed, + swipeState: .idle + ) + ) + ) } - - func makeAttributed(_ text: String) -> NSMutableAttributedString { + + fileprivate func makeAttributed(_ text: String) -> NSMutableAttributedString { let attributedString = Self.markdownParser.parse(text) - + let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString) - + let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineSpacing = lineSpacing - + mutableAttributedString.addAttribute( NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSRange(location: .zero, length: attributedString.length) ) - + return mutableAttributedString } - - func decodeMessage(_ transaction: RichMessageTransaction) -> NSMutableAttributedString { + + fileprivate func decodeMessage(_ transaction: RichMessageTransaction) -> NSMutableAttributedString { let decodedMessage = transaction.getRichValue(for: RichContentKeys.reply.decodedReplyMessage) ?? "..." return Self.markdownReplyParser.parse(decodedMessage).resolveLinkColor() } - - func makeChatFiles( + + fileprivate func makeChatFiles( from files: [[String: Any]], isFromCurrentSender: Bool, storage: String @@ -392,7 +412,7 @@ private extension ChatMessageFactory { return files.map { let file = RichMessageFile.File($0) let fileType = FileType(raw: file.extension ?? .empty) ?? .other - + return ChatFile( file: file, previewImage: nil, @@ -407,57 +427,63 @@ private extension ChatMessageFactory { ) } } - - func makeContent( + + fileprivate func makeContent( _ transaction: TransferTransaction, isFromCurrentSender: Bool, backgroundColor: ChatMessageBackgroundColor ) -> ChatMessage.Content { let id = transaction.chatMessageId ?? "" - + let decodedMessage = transaction.decodedReplyMessage ?? "..." let decodedMessageMarkDown = Self.markdownReplyParser.parse(decodedMessage).resolveLinkColor() let replyId = transaction.replyToId ?? "" let reactions = transaction.reactions - - let address = transaction.isOutgoing - ? transaction.senderAddress - : transaction.recipientAddress - - let opponentAddress = transaction.isOutgoing - ? transaction.recipientAddress - : transaction.senderAddress - - return .transaction(.init(value: .init( - id: id, - isFromCurrentSender: isFromCurrentSender, - content: .init( - id: id, - title: isFromCurrentSender - ? .adamant.chat.transactionSent - : .adamant.chat.transactionReceived, - icon: AdmWalletService.currencyLogo, - amount: AdamantBalanceFormat.full.format( - (transaction.amount ?? .zero) as Decimal - ), - currency: AdmWalletService.currencySymbol, - date: transaction.sentDate?.humanizedDateTime(withWeekday: false) ?? "", - comment: transaction.comment, - backgroundColor: backgroundColor, - isReply: !replyId.isEmpty, - replyMessage: decodedMessageMarkDown, - replyId: replyId, - isHidden: false - ), - status: transaction.statusEnum.toTransactionStatus(), - reactions: reactions, - address: address, - opponentAddress: opponentAddress, - swipeState: .idle - ))) + + let address = + transaction.isOutgoing + ? transaction.senderAddress + : transaction.recipientAddress + + let opponentAddress = + transaction.isOutgoing + ? transaction.recipientAddress + : transaction.senderAddress + + return .transaction( + .init( + value: .init( + id: id, + isFromCurrentSender: isFromCurrentSender, + content: .init( + id: id, + title: isFromCurrentSender + ? .adamant.chat.transactionSent + : .adamant.chat.transactionReceived, + icon: AdmWalletService.currencyLogo, + amount: AdamantBalanceFormat.full.format( + (transaction.amount ?? .zero) as Decimal + ), + currency: AdmWalletService.currencySymbol, + date: transaction.sentDate?.humanizedDateTime(withWeekday: false) ?? "", + comment: transaction.comment, + backgroundColor: backgroundColor, + isReply: !replyId.isEmpty, + replyMessage: decodedMessageMarkDown, + replyId: replyId, + isHidden: false + ), + status: transaction.statusEnum.toTransactionStatus(), + reactions: reactions, + address: address, + opponentAddress: opponentAddress, + swipeState: .idle + ) + ) + ) } - - func makeBottomString( + + fileprivate func makeBottomString( sentDate: Date, status: ChatMessage.Status, expireDate: inout Date?, @@ -481,22 +507,22 @@ private extension ChatMessageFactory { return nil } } - - func makeMessageTimeString( + + fileprivate func makeMessageTimeString( sentDate: Date, blockchain: Bool, expireDate: inout Date? ) -> NSAttributedString? { guard sentDate.timeIntervalSince1970 > .zero else { return nil } - + let prefix = blockchain ? "⚭" : nil let humanizedTime = sentDate.humanizedTime() expireDate = humanizedTime.expireIn.map { .init().addingTimeInterval($0) } - + let string = [prefix, humanizedTime.string] .compactMap { $0 } .joined(separator: " ") - + return .init( string: string, attributes: [ @@ -505,23 +531,23 @@ private extension ChatMessageFactory { ] ) } - - func makePendingMessageString() -> NSAttributedString { + + fileprivate func makePendingMessageString() -> NSAttributedString { let attachment = NSTextAttachment() attachment.image = .asset(named: "status_pending") attachment.image?.withTintColor(.adamant.secondary) attachment.bounds = CGRect(x: .zero, y: -1, width: 10, height: 10) return NSAttributedString(attachment: attachment) } - - func getBackgroundColor( + + fileprivate func getBackgroundColor( isFromCurrentSender: Bool, status: ChatMessage.Status ) -> ChatMessageBackgroundColor { guard isFromCurrentSender else { return .opponent } - + switch status { case .delivered: return .delivered @@ -531,20 +557,40 @@ private extension ChatMessageFactory { return .failed } } - - func makeDateHeader(sentDate: Date) -> ComparableAttributedString { - .init(string: .init( - string: sentDate.humanizedDay(useTimeFormat: false), - attributes: [ - .font: UIFont.boldSystemFont(ofSize: 10), - .foregroundColor: UIColor.adamant.secondary - ] - )) + + fileprivate func makeDateHeader(sentDate: Date) -> ComparableAttributedString { + .init( + string: .init( + string: sentDate.humanizedDay(useTimeFormat: false), + attributes: [ + .font: UIFont.boldSystemFont(ofSize: 10), + .foregroundColor: UIColor.adamant.secondary + ] + ) + ) + } + + fileprivate func checkTransactionForUnreadReaction(transaction: ChatTransaction) -> Bool { + if let messageTransaction = transaction as? MessageTransaction, + let richTransactions = messageTransaction.richMessageTransactions, + !richTransactions.isEmpty + { + return richTransactions.contains { $0.isUnread } + } + + if let transferTransaction = transaction as? TransferTransaction, + let richTransactions = transferTransaction.richMessageTransactions, + !richTransactions.isEmpty + { + return richTransactions.contains { $0.isUnread } + } + + return transaction.isUnread } } -private extension ChatMessage.Status { - init(messageStatus: MessageStatus, blockId: String?) { +extension ChatMessage.Status { + fileprivate init(messageStatus: MessageStatus, blockId: String?) { switch messageStatus { case .pending: self = .pending @@ -556,8 +602,8 @@ private extension ChatMessage.Status { } } -private extension ChatSender { - init(transaction: ChatTransaction) { +extension ChatSender { + fileprivate init(transaction: ChatTransaction) { self.init( senderId: transaction.senderId ?? "", displayName: transaction.senderId ?? "" diff --git a/Adamant/Modules/Chat/ViewModel/ChatMessagesListFactory.swift b/Adamant/Modules/Chat/ViewModel/ChatMessagesListFactory.swift index 06ebf904d..bb91ff66a 100644 --- a/Adamant/Modules/Chat/ViewModel/ChatMessagesListFactory.swift +++ b/Adamant/Modules/Chat/ViewModel/ChatMessagesListFactory.swift @@ -6,35 +6,60 @@ // Copyright © 2023 Adamant. All rights reserved. // -import Foundation -import MessageKit import Combine import CommonKit +import Foundation +import MessageKit +import OrderedCollections actor ChatMessagesListFactory { private let chatMessageFactory: ChatMessageFactory - - init(chatMessageFactory: ChatMessageFactory) { + private let coreDataRelationMapper: CoreDataRealationMapperProtocol + + init(chatMessageFactory: ChatMessageFactory, coreDataRelationMapper: CoreDataRealationMapperProtocol) { self.chatMessageFactory = chatMessageFactory + self.coreDataRelationMapper = coreDataRelationMapper } - + func makeMessages( transactions: [ChatTransaction], sender: ChatSender, isNeedToLoadMoreMessages: Bool, expirationTimestamp minExpTimestamp: inout TimeInterval? - ) -> [ChatMessage] { + ) async -> ([ChatMessage], OrderedSet, OrderedSet) { assert(!Thread.isMainThread, "Do not process messages on main thread") - + + var processedTransactionIds: OrderedSet = [] + + await withTaskGroup(of: [String].self) { group in + for chatTransaction in transactions { + guard let transaction = chatTransaction as? RichMessageTransaction, + transaction.additionalType == .reaction, + transaction.isUnread + else { continue } + + group.addTask { [weak self] in + return await self?.coreDataRelationMapper.mapReactionRelationship(transaction: transaction) ?? [] + } + } + + for await result in group { + processedTransactionIds.append(contentsOf: result) + } + } let transactionsWithoutReact = transactions.filter { chatTransaction in guard let transaction = chatTransaction as? RichMessageTransaction, - transaction.additionalType == .reaction + transaction.additionalType == .reaction else { return true } - + return false } - - return transactionsWithoutReact.enumerated().map { index, transaction in + let transactionIdsWithoutReact: OrderedSet = OrderedSet( + transactionsWithoutReact + .filter { $0.isUnread } + .compactMap { $0.transactionId } + ) + let messages = transactionsWithoutReact.enumerated().map { index, transaction in var expTimestamp: TimeInterval? let message = makeMessage( transaction, @@ -46,18 +71,20 @@ actor ChatMessagesListFactory { topSpinnerOn: isNeedToLoadMoreMessages && index == .zero, willExpireAfter: &expTimestamp ) - + if let timestamp = expTimestamp, timestamp < minExpTimestamp ?? .greatestFiniteMagnitude { minExpTimestamp = timestamp } - + return message } + + return (messages, processedTransactionIds, transactionIdsWithoutReact) } } -private extension ChatMessagesListFactory { - func makeMessage( +extension ChatMessagesListFactory { + fileprivate func makeMessage( _ transaction: ChatTransaction, sender: SenderType, dateHeaderOn: Bool, @@ -72,7 +99,7 @@ private extension ChatMessagesListFactory { dateHeaderOn: dateHeaderOn, topSpinnerOn: topSpinnerOn ) - + willExpireAfter = expireDate?.timeIntervalSince1970 return message } @@ -84,11 +111,11 @@ private func isNeedToDisplayDateHeader( ) -> Bool { guard transactions[index].sentDate != .adamantNullDate else { return false } guard index > .zero else { return true } - + guard let previousDate = transactions[index - 1].sentDate, let currentDate = transactions[index].sentDate else { return false } - + return !Calendar.current.isDate(currentDate, inSameDayAs: previousDate) } diff --git a/Adamant/Modules/Chat/ViewModel/ChatMessagesListViewModel.swift b/Adamant/Modules/Chat/ViewModel/ChatMessagesListViewModel.swift index 2dc724fe4..eae253eaf 100644 --- a/Adamant/Modules/Chat/ViewModel/ChatMessagesListViewModel.swift +++ b/Adamant/Modules/Chat/ViewModel/ChatMessagesListViewModel.swift @@ -10,10 +10,10 @@ import Foundation final class ChatMessagesListViewModel { // MARK: Dependencies - + let avatarService: AvatarService let emojiService: EmojiService - + init( avatarService: AvatarService, emojiService: EmojiService diff --git a/Adamant/Modules/Chat/ViewModel/ChatPreservationProtocol.swift b/Adamant/Modules/Chat/ViewModel/ChatPreservationProtocol.swift index 50ca5d654..6d612fdbf 100644 --- a/Adamant/Modules/Chat/ViewModel/ChatPreservationProtocol.swift +++ b/Adamant/Modules/Chat/ViewModel/ChatPreservationProtocol.swift @@ -9,13 +9,11 @@ import CommonKit protocol ChatPreservationProtocol: AnyObject, Sendable { - func preserveMessage(_ message: String, forAddress address: String) - func getPreservedMessageFor(address: String, thenRemoveIt: Bool) -> String? - func setReplyMessage(_ message: MessageModel?, forAddress address: String) - func getReplyMessage(address: String, thenRemoveIt: Bool) -> MessageModel? - func preserveFiles(_ files: [FileResult]?, forAddress address: String) + var updateNotifier: ObservableSender { get } + func getPreservedMessageFor(address: String) -> String? + func getReplyMessage(address: String) -> MessageModel? + func preserveChatState(message: String?, replyMessage: MessageModel?, files: [FileResult]?, forAddress address: String) func getPreservedFiles( - for address: String, - thenRemoveIt: Bool + for address: String ) -> [FileResult]? } diff --git a/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift b/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift index 3a7118747..05f03a182 100644 --- a/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift +++ b/Adamant/Modules/Chat/ViewModel/ChatViewModel.swift @@ -6,26 +6,27 @@ // Copyright © 2022 Adamant. All rights reserved. // +import AdvancedContextMenuKit @preconcurrency import Combine -import CoreData -import MarkdownKit -import UIKit import CommonKit -import AdvancedContextMenuKit +import CoreData @preconcurrency import ElegantEmojiPicker import FilesPickerKit import FilesStorageKit +import MarkdownKit +import OrderedCollections +import UIKit @MainActor final class ChatViewModel: NSObject { // MARK: Dependencies - + private let chatsProvider: ChatsProvider private let markdownParser: MarkdownParser private let transfersProvider: TransfersProvider private let chatMessagesListFactory: ChatMessagesListFactory private let addressBookService: AddressBookService - private let visibleWalletService: VisibleWalletsService + private let walletsStoreService: WalletStoreServiceProtocol private let accountService: AccountService private let accountProvider: AccountsProvider private let richTransactionStatusService: TransactionsStatusServiceComposeProtocol @@ -40,30 +41,40 @@ final class ChatViewModel: NSObject { private let apiServiceCompose: ApiServiceComposeProtocol private let reachabilityMonitor: ReachabilityMonitor private let filesPicker: FilesPickerProtocol - + private let visibleWalletsService: VisibleWalletsService + let chatMessagesListViewModel: ChatMessagesListViewModel // MARK: Properties - + private var tasksStorage = TaskManager() private var controller: NSFetchedResultsController? private var subscriptions = Set() private var timerSubscription: AnyCancellable? - private var messageIdToShow: String? private var isLoading = false - + var messageIdToShow: String? + var separatorIndex: Int? + var separatorId: String? + var didAddSeparator: Bool = false + private var isNeedToLoadMoreMessages: Bool { get async { guard let address = chatroom?.partner?.address else { return false } - + return await chatsProvider.chatLoadedMessages[address] ?? .zero < chatsProvider.chatMaxMessages[address] ?? .zero } } - + + @UserDefaultsStorage(.needsToShowNoActiveNodesAlert) private var needsToShowNoActiveNodesAlert: Bool? private(set) var sender = ChatSender.default private(set) var chatroom: Chatroom? - private(set) var chatTransactions: [ChatTransaction] = [] + private(set) var chatTransactions: [ChatTransaction] = [] { + didSet { + updatePositionIfNeeded() + } + } + private var tempCancellables = Set() private var hideHeaderTimer: AnyCancellable? private let minDiffCountForOffset = 5 @@ -74,20 +85,23 @@ final class ChatViewModel: NSObject { private var lastDateHeaderUpdate: Date = Date() private var hasPartnerName: Bool = false private let delayHideHeaderInSeconds: Double = 2.0 - + let minIndexForStartLoadNewMessages = 4 let minOffsetForStartLoadNewMessages: CGFloat = 100 var tempOffsets: [String] = [] var needToAnimateCellIndex: Int? var indexPathsForVisibleItems: () -> [IndexPath] = { .init() } + var scrolledMessageId: Set? + var shouldScrollToBottom: Bool = true let didTapPartnerQR = ObservableSender() let didTapTransfer = ObservableSender() let dialog = ObservableSender() let didTapAdmChat = ObservableSender<(Chatroom, String?)>() let didTapAdmSend = ObservableSender() + let didTapAdmNodesList = ObservableSender() + let didTapShowTimeSettings = ObservableSender() let closeScreen = ObservableSender() - let updateChatRead = ObservableSender() let commitVibro = ObservableSender() let layoutIfNeeded = ObservableSender() let presentKeyboard = ObservableSender() @@ -99,11 +113,17 @@ final class ChatViewModel: NSObject { let presentDocumentViewerVC = ObservableSender<([FileResult], Int)>() let presentDropView = ObservableSender() let enableScroll = ObservableSender() - + let showBuyAndSell = ObservableSender() + let didUpdateCoreData = ObservableSender() + let messagesUpdated = ObservableSender() + @ObservableValue private(set) var swipeableMessage: ChatSwipeWrapperModel = .default @ObservableValue private(set) var isHeaderLoading = false @ObservableValue private(set) var fullscreenLoading = false @ObservableValue private(set) var messages = [ChatMessage]() + @ObservableValue private(set) var unreadMesaggesIndexes: Set? + @ObservableValue private(set) var unreadMessagesIds: OrderedSet? + @ObservableValue private(set) var messagesWithUnredReactionsIds: OrderedSet? @ObservableValue private(set) var isAttachmentButtonAvailable = false @ObservableValue private(set) var isSendingAvailable = false @ObservableValue private(set) var fee = "" @@ -114,49 +134,55 @@ final class ChatViewModel: NSObject { @ObservableValue private(set) var dateHeaderHidden: Bool = true @ObservableValue var inputText = "" @ObservableValue var replyMessage: MessageModel? - @ObservableValue var scrollToMessage: (toId: String?, fromId: String?) - @ObservableValue var filesPicked: [FileResult]? - + @ObservableValue var scrollToIdAndPosition: (id: String, position: UICollectionView.ScrollPosition)? + @ObservableValue var filesPicked: [FileResult]? { + didSet { + updateFeeValue() + } + } + var startPosition: ChatStartPosition? { - if let messageIdToShow = messageIdToShow { - return .messageId(messageIdToShow, toBottomIfNotFound: true) + if messageIdToShow != nil { + return nil } - + guard let address = chatroom?.partner?.address else { return nil } return chatsProvider.getChatPositon(for: address).map { .offset(.init($0)) } } - + var freeTokensURL: URL? { guard let address = accountService.account?.address else { return nil } let urlString: String = .adamant.wallets.getFreeTokensUrl(for: address) - + guard let url = URL(string: urlString) else { - dialog.send(.error( - "Failed to create URL with string: \(urlString)", - supportEmail: true - )) + dialog.send( + .error( + "Failed to create URL with string: \(urlString)", + supportEmail: true + ) + ) return nil } - + return url } - + private var hiddenMessageID: String? { didSet { updateHiddenMessage(&messages) } } - + lazy private(set) var mediaPickerDelegate = MediaPickerService(helper: filesPicker) lazy private(set) var documentPickerDelegate = DocumentPickerService(helper: filesPicker) lazy private(set) var documentViewerService = DocumentInteractionService() lazy private(set) var dropInteractionService = DropInteractionService(helper: filesPicker) - + init( chatsProvider: ChatsProvider, markdownParser: MarkdownParser, transfersProvider: TransfersProvider, chatMessagesListFactory: ChatMessagesListFactory, addressBookService: AddressBookService, - visibleWalletService: VisibleWalletsService, + walletsStoreService: WalletStoreServiceProtocol, accountService: AccountService, accountProvider: AccountsProvider, richTransactionStatusService: TransactionsStatusServiceComposeProtocol, @@ -171,7 +197,8 @@ final class ChatViewModel: NSObject { filesStorageProprieties: FilesStorageProprietiesProtocol, apiServiceCompose: ApiServiceComposeProtocol, reachabilityMonitor: ReachabilityMonitor, - filesPicker: FilesPickerProtocol + filesPicker: FilesPickerProtocol, + visibleWalletsService: VisibleWalletsService ) { self.chatsProvider = chatsProvider self.markdownParser = markdownParser @@ -179,7 +206,7 @@ final class ChatViewModel: NSObject { self.chatMessagesListFactory = chatMessagesListFactory self.addressBookService = addressBookService self.walletServiceCompose = walletServiceCompose - self.visibleWalletService = visibleWalletService + self.walletsStoreService = walletsStoreService self.accountService = accountService self.accountProvider = accountProvider self.richTransactionStatusService = richTransactionStatusService @@ -194,73 +221,77 @@ final class ChatViewModel: NSObject { self.apiServiceCompose = apiServiceCompose self.reachabilityMonitor = reachabilityMonitor self.filesPicker = filesPicker - + self.visibleWalletsService = visibleWalletsService + super.init() setupObservers() } - + func setup( account: AdamantAccount?, chatroom: Chatroom, - messageIdToShow: String? + messageIdToShow: String?, + isNewChat: Bool = false ) { + self.messageIdToShow = messageIdToShow assert(self.chatroom == nil, "Can't setup several times") self.chatroom = chatroom - self.messageIdToShow = messageIdToShow + self.chatroom?.updateLastTransaction() controller = chatsProvider.getChatController(for: chatroom) controller?.delegate = self isSendingAvailable = !chatroom.isReadonly updatePartnerInformation() updateAttachmentButtonAvailability() - + if let account = account { sender = .init(senderId: account.address, displayName: account.address) } - + if let partnerAddress = chatroom.partner?.address { chatPreservation.getPreservedMessageFor( - address: partnerAddress, - thenRemoveIt: true + address: partnerAddress ).map { inputText = $0 } - + let cachedMessages = chatCacheService.getMessages(address: partnerAddress) messages = cachedMessages ?? [] fullscreenLoading = cachedMessages == nil - - replyMessage = chatPreservation.getReplyMessage(address: partnerAddress, thenRemoveIt: true) - - filesPicked = chatPreservation.getPreservedFiles( - for: partnerAddress, - thenRemoveIt: true - ) + + replyMessage = chatPreservation.getReplyMessage(address: partnerAddress) + + filesPicked = chatPreservation.getPreservedFiles(for: partnerAddress) + } + if isNewChat && !(accountService.account?.isEnoughMoneyForTransaction ?? false) { + dialog.send(.freeTokenAlert) } } - + func presentKeyboardOnStartIfNeeded() { - guard !inputText.isEmpty + guard + !inputText.isEmpty || replyMessage != nil || (filesPicked?.count ?? .zero) > .zero else { return } - + presentKeyboard.send() } - + func loadFirstMessagesIfNeeded() { Task { guard let address = chatroom?.partner?.address else { fullscreenLoading = false return } - - let isChatLoaded = await chatsProvider.isChatLoaded(with: address) - let isChatLoading = await chatsProvider.isChatLoading(with: address) - + + async let chatLoadingState = (chatsProvider.isChatLoaded(with: address), chatsProvider.isChatLoading(with: address)) + + let (isChatLoaded, isChatLoading) = await chatLoadingState + guard !isChatLoading else { await waitForChatLoading(with: address) updateTransactions(performFetch: true) return } - + if address == AdamantContacts.adamantWelcomeWallet.name || isChatLoaded { updateTransactions(performFetch: true) } else { @@ -268,7 +299,13 @@ final class ChatViewModel: NSObject { } }.stored(in: tasksStorage) } - + + func updatePositionIfNeeded() { + if let messageIdToShow = messageIdToShow { + scroll(to: messageIdToShow) + } + } + func loadMoreMessagesIfNeeded() { guard !isLoading else { return } Task { @@ -276,27 +313,21 @@ final class ChatViewModel: NSObject { let address = chatroom?.partner?.address, await isNeedToLoadMoreMessages else { return } - + let offset = await chatsProvider.chatLoadedMessages[address] ?? .zero await loadMessages(address: address, offset: offset) }.stored(in: tasksStorage) } - + func sendMessage(text: String) { guard let partnerAddress = chatroom?.partner?.address else { return } - + guard chatroom?.partner?.isDummy != true else { dialog.send(.dummy(partnerAddress)) return } - + Task { - if apiServiceCompose.get(.adm)?.hasEnabledNode == false { - dialog.send(.alert(ApiServiceError.noEndpointsAvailable( - nodeGroupName: NodeGroup.adm.name - ).localizedDescription)) - } - if !(filesPicked?.isEmpty ?? true) { do { try await sendFiles(with: text) @@ -309,9 +340,9 @@ final class ChatViewModel: NSObject { } return } - + let message: AdamantMessage - + if let replyMessage = replyMessage { message = .richMessage( payload: RichMessageReply( @@ -320,15 +351,16 @@ final class ChatViewModel: NSObject { ) ) } else { - message = markdownParser.parse(text).length == text.count - ? .text(text) - : .markdownText(text) + message = + markdownParser.parse(text).length == text.count + ? .text(text) + : .markdownText(text) } - + guard await validateSendingMessage(message: message) else { return } - + replyMessage = nil - + do { _ = try await chatsProvider.sendMessage( message, @@ -340,128 +372,110 @@ final class ChatViewModel: NSObject { } }.stored(in: tasksStorage) } - + func forceUpdateTransactionStatus(id: String) { Task { guard let transaction = chatTransactions.first(where: { $0.chatMessageId == id }), let richMessageTransaction = transaction as? RichMessageTransaction else { return } - + await richTransactionStatusService.forceUpdate(transaction: richMessageTransaction) }.stored(in: tasksStorage) } - + func preserveMessage(_ message: String) { guard let partnerAddress = chatroom?.partner?.address else { return } - chatPreservation.preserveMessage(message, forAddress: partnerAddress) + chatPreservation.preserveChatState(message: message, replyMessage: replyMessage, files: filesPicked, forAddress: partnerAddress) } - - func preserveFiles() { - guard let partnerAddress = chatroom?.partner?.address else { return } - chatPreservation.preserveFiles(filesPicked, forAddress: partnerAddress) - } - - func preserveReplayMessage() { - guard let partnerAddress = chatroom?.partner?.address else { return } - chatPreservation.setReplyMessage(replyMessage, forAddress: partnerAddress) - } - + func blockChat() { Task { guard let address = chatroom?.partner?.address else { return assertionFailure("Can't block user without address") } - + chatroom?.isHidden = true try? chatroom?.managedObjectContext?.save() await chatsProvider.blockChat(with: address) closeScreen.send() } } - + func getKvsName(for address: String) -> String? { return addressBookService.getName(for: address) } - + func setNewName(_ newName: String) { guard let address = chatroom?.partner?.address else { return assertionFailure("Can't set name without address") } - + Task { await addressBookService.set(name: newName, for: address) }.stored(in: tasksStorage) - + partnerName = newName hasPartnerName = !newName.isEmpty } - + func saveChatOffset(_ offset: CGFloat?) { guard let address = chatroom?.partner?.address else { return } chatsProvider.setChatPositon(for: address, position: offset.map { Double.init($0) }) } - - func entireChatWasRead() { + + func markMessageAsRead(index: Int) { + guard _messages.wrappedValue.indices.contains(index) else { return } + guard let chatroom else { return } + + let message = _messages.wrappedValue[index] Task { - guard - let chatroom = chatroom, - chatroom.hasUnreadMessages == true || chatroom.lastTransaction?.isUnread == true - else { return } - - await chatsProvider.markChatAsRead(chatroom: chatroom) + await chatsProvider.markMessageAsRead(chatroom: chatroom, message: message.messageId) } } - + func hideMessage(id: String) { Task { - guard let transaction = chatTransactions.first(where: { $0.chatMessageId == id }) - else { return } - - transaction.isHidden = true - try? transaction.managedObjectContext?.save() - - chatroom?.updateLastTransaction() - await chatsProvider.removeMessage(with: transaction.transactionId) + await chatsProvider.removeMessage(with: id) } } - + func didSelectURL(_ url: URL) { if url.scheme == "adm" { guard let adm = url.absoluteString.getLegacyAdamantAddress(), - let partnerAddress = chatroom?.partner?.address + let partnerAddress = chatroom?.partner?.address else { return } - + dialog.send(.admMenu(adm, partnerAddress: partnerAddress)) return } - + dialog.send(.url(url)) } - + func process(adm: AdamantAddress, action: AddressChatShareType) { Task { if action == .send { didTapAdmSend.send(adm) return } - + guard let room = await self.chatsProvider.getChatroom(for: adm.address) else { await self.findAccount(with: adm.address, name: adm.name, message: adm.message) return } - + self.startNewChat(with: room, name: adm.name, message: adm.message) } } - + func cancelMessage(id: String) { Task { guard let transaction = chatTransactions.first(where: { $0.chatMessageId == id }) else { return } - + do { try await chatsProvider.cancelMessage(transaction) } catch { @@ -474,14 +488,14 @@ final class ChatViewModel: NSObject { } }.stored(in: tasksStorage) } - + func retrySendMessage(id: String) { Task { guard let transaction = chatTransactions.first(where: { $0.chatMessageId == id }) else { return } - + let message = messages.first(where: { $0.messageId == id }) - + if case let .file(model) = message?.content { try? await chatFileService.resendMessage( with: id, @@ -492,45 +506,57 @@ final class ChatViewModel: NSObject { ) return } - + do { try await chatsProvider.retrySendMessage(transaction) } catch { switch error as? ChatsProviderError { case .invalidTransactionStatus: break + case let .serverError(serverError): + switch serverError { + case .timestampIsInTheFuture: + dialog.send(.timestampIsInTheFuture) + default: dialog.send(.richError(error)) + } default: dialog.send(.richError(error)) } } }.stored(in: tasksStorage) } - - func scroll(to message: ChatMessageReplyCell.Model) { + + func scroll(to messageId: String) { guard let partnerAddress = chatroom?.partner?.address else { return } - + Task { do { - guard await !chatsProvider.isMessageDeleted(id: message.replyId) else { + guard await !chatsProvider.isMessageDeleted(id: messageId) else { dialog.send(.alert(.adamant.chat.messageWasDeleted)) return } - + if !chatTransactions.contains( - where: { $0.transactionId == message.replyId } + where: { $0.transactionId == messageId } ) { dialog.send(.progress(true)) try await chatsProvider.loadTransactionsUntilFound( - message.replyId, + messageId, recipient: partnerAddress ) } - - await waitForMessage(withId: message.replyId) - - scrollToMessage = (toId: message.replyId, fromId: message.id) - + + await waitForMessage(withId: messageId) + + if let lastMessageId = messages.last?.id { + let position: UICollectionView.ScrollPosition = (messageId == lastMessageId) ? .top : .bottom + scrollToIdAndPosition = (id: messageId, position: position) + } + dialog.send(.progress(false)) + if let index = messages.firstIndex(where: { $0.id == messageId }) { + markMessageAsRead(index: index) + } } catch { print(error) dialog.send(.progress(false)) @@ -538,12 +564,12 @@ final class ChatViewModel: NSObject { } }.stored(in: tasksStorage) } - + func waitForMessage(withId messageId: String) async { guard !messages.contains(where: { $0.messageId == messageId }) else { return } - + await withUnsafeContinuation { continuation in $messages .filter { $0.contains(where: { $0.messageId == messageId }) } @@ -553,64 +579,64 @@ final class ChatViewModel: NSObject { }.store(in: &tempCancellables) } } - + func replyMessageIfNeeded(id: String) { let tx = chatTransactions.first(where: { $0.txId == id }) guard isSendingAvailable, tx?.isFake == false else { return } - + let message = messages.first(where: { $0.messageId == id }) guard message?.status != .failed else { dialog.send(.warning(String.adamant.reply.failedMessageError)) return } - + guard message?.status != .pending else { dialog.send(.warning(String.adamant.reply.pendingMessageError)) return } - + replyMessage = message?.messageModel } - + func animateScrollIfNeeded(to messageIndex: Int, visibleIndex: Int?) { - guard let visibleIndex = visibleIndex else { return } - + guard let visibleIndex = visibleIndex else { return } + let max = max(visibleIndex, messageIndex) let min = min(visibleIndex, messageIndex) - + guard (max - min) >= minDiffCountForAnimateScroll else { isNeedToAnimateScroll = false return } - + isNeedToAnimateScroll = true } - + func copyMessageAction(_ text: String) { UIPasteboard.general.string = text dialog.send(.toast(.adamant.alert.copiedToPasteboardNotification)) } - + func copyTextInPartAction(_ text: String) { didTapSelectText.send(text) } - + func reportMessageAction(_ id: String) { dialog.send(.reportMessageAlert(id: id)) } - + func removeMessageAction(_ id: String) { dialog.send(.removeMessageAlert(id: id)) } - + func reactAction(_ id: String, emoji: String) { guard let partnerAddress = chatroom?.partner?.address else { return } - + guard chatroom?.partner?.isDummy != true else { dialog.send(.dummy(partnerAddress)) return } - + Task { let message: AdamantMessage = .richMessage( payload: RichMessageReaction( @@ -618,9 +644,9 @@ final class ChatViewModel: NSObject { react_message: emoji ) ) - + guard await validateSendingMessage(message: message) else { return } - + do { _ = try await chatsProvider.sendMessage( message, @@ -632,58 +658,60 @@ final class ChatViewModel: NSObject { } }.stored(in: tasksStorage) } - + func clearReplyMessage() { replyMessage = nil } - + func clearPickedFiles() { filesPicked = nil } - + func presentMenu(arg: ChatContextMenuArguments) { let didSelectEmojiAction: ChatDialogManager.DidSelectEmojiAction = { [weak self] emoji, messageId in self?.dialog.send(.dismissMenu) - - let emoji = emoji == arg.selectedEmoji - ? "" - : emoji - - let type: EmojiUpdateType = emoji.isEmpty - ? .decrement - : .increment - + + let emoji = + emoji == arg.selectedEmoji + ? "" + : emoji + + let type: EmojiUpdateType = + emoji.isEmpty + ? .decrement + : .increment + self?.emojiService.updateFrequentlySelectedEmojis( selectedEmoji: emoji, type: type ) - + self?.reactAction(messageId, emoji: emoji) self?.previousArg = nil } - + let didPresentMenuAction: ChatDialogManager.ContextMenuAction = { [weak self] messageId in self?.hiddenMessageID = messageId } - + let didDismissMenuAction: ChatDialogManager.ContextMenuAction = { [weak self] _ in self?.hiddenMessageID = nil self?.layoutIfNeeded.send() self?.previousArg = nil } - + previousArg = arg - + let tx = chatTransactions.first(where: { $0.txId == arg.messageId }) guard tx?.statusEnum == .delivered else { return } - + let amount = tx?.amountValue ?? .zero if !amount.isZero && !isSendingAvailable { return } - + let presentReactions = isSendingAvailable && tx?.isFake == false - + dialog.send( .presentMenu( presentReactions: presentReactions, @@ -695,94 +723,94 @@ final class ChatViewModel: NSObject { ) ) } - + func canSendMessage(withText text: String) async -> Bool { guard text.count <= maxMessageLenght else { dialog.send(.alert(.adamant.chat.messageIsTooBig)) return false } - - guard apiServiceCompose.get(.adm)?.hasEnabledNode == true else { - dialog.send(.alert(ApiServiceError.noEndpointsAvailable( - nodeGroupName: NodeGroup.adm.name - ).localizedDescription)) - return false - } - + return true } - + /// If the user opens the app from the background /// update messages to refresh the header dates. func refreshDateHeadersIfNeeded() { guard !Calendar.current.isDate(Date(), inSameDayAs: lastDateHeaderUpdate) else { return } - + lastDateHeaderUpdate = Date() updateMessages(resetLoadingProperty: false) } + func cancelFileUploading(messageId: String, file: ChatFile) { + Task { + await chatFileService.cancelUpload(messageId: messageId, fileId: file.file.id) + } + } + func openFile(messageId: String, file: ChatFile) { let tx = chatTransactions.first(where: { $0.txId == messageId }) let message = messages.first(where: { $0.messageId == messageId }) - + guard let tx = tx, - tx.statusEnum != .failed + tx.statusEnum != .failed else { dialog.send(.failedMessageAlert(id: messageId, sender: nil)) return } - + guard !chatFileService.downloadingFiles.keys.contains(file.file.id), - !chatFileService.uploadingFiles.contains(file.file.id), - case let(.file(fileModel)) = message?.content + !chatFileService.uploadingFiles.contains(file.file.id), + case let (.file(fileModel)) = message?.content else { return } - + let chatFiles = fileModel.value.content.fileModel.files - + let isPreviewAutoDownloadAllowed = chatFileService.isPreviewAutoDownloadAllowedByPolicy( hasPartnerName: hasPartnerName, isFromCurrentSender: file.isFromCurrentSender, downloadPolicy: filesStorageProprieties.autoDownloadPreviewPolicy() ) - + if !isPreviewAutoDownloadAllowed, - file.previewImage == nil, - file.file.preview != nil, - !chatFileService.isDownloadPreviewLimitReached(for: file.file.id) { + file.previewImage == nil, + file.file.preview != nil, + !chatFileService.isDownloadPreviewLimitReached(for: file.file.id) + { forceDownloadAllFiles(messageId: messageId, files: chatFiles) return } - + guard !file.isCached, - !filesStorage.isCachedLocally(file.file.id) + !filesStorage.isCachedLocally(file.file.id) else { self.presentFileInFullScreen(id: file.file.id, chatFiles: chatFiles) return } - + guard tx.statusEnum == .delivered else { return } - + downloadFile( file: file, previewDownloadAllowed: true, fullMediaDownloadAllowed: true ) } - + func autoDownloadContentIfNeeded( messageId: String, files: [ChatFile] ) { let tx = chatTransactions.first(where: { $0.txId == messageId }) - + guard tx?.statusEnum == .delivered || tx?.statusEnum == nil else { return } - + let chatFiles = files.filter { $0.fileType == .image || $0.fileType == .video } - + chatFiles.forEach { file in Task { await chatFileService.autoDownload( @@ -796,81 +824,90 @@ final class ChatViewModel: NSObject { } } } - + func forceDownloadAllFiles(messageId: String, files: [ChatFile]) { guard let message = messages.first(where: { $0.messageId == messageId }) else { return } - + let isPreviewDownloadAllowed = chatFileService.isPreviewAutoDownloadAllowedByPolicy( hasPartnerName: hasPartnerName, isFromCurrentSender: message.isFromCurrentSender, downloadPolicy: filesStorageProprieties.autoDownloadPreviewPolicy() ) - + let isFullMediaDownloadAllowed = chatFileService.isOriginalAutoDownloadAllowedByPolicy( hasPartnerName: hasPartnerName, isFromCurrentSender: message.isFromCurrentSender, downloadPolicy: filesStorageProprieties.autoDownloadFullMediaPolicy() ) - + let needToDownload: [ChatFile] - + let shouldDownloadFile: (ChatFile) -> Bool = { file in !file.isCached || (file.fileType.isMedia && file.previewImage == nil && isPreviewDownloadAllowed) } - + let previewFiles = files.filter { file in (file.fileType == .image || file.fileType == .video) && file.previewImage == nil } - + let notCachedFiles = files.filter { !$0.isCached } - + let downloadPreview: Bool let downloadFullMedia: Bool - + switch (isPreviewDownloadAllowed, isFullMediaDownloadAllowed) { case (true, true): needToDownload = files.filter(shouldDownloadFile) downloadPreview = true downloadFullMedia = true case (true, false): - needToDownload = previewFiles.isEmpty - ? notCachedFiles - : previewFiles - - downloadPreview = previewFiles.isEmpty - ? false - : true - - downloadFullMedia = previewFiles.isEmpty - ? true - : false + needToDownload = + previewFiles.isEmpty + ? notCachedFiles + : previewFiles + + downloadPreview = + previewFiles.isEmpty + ? false + : true + + downloadFullMedia = + previewFiles.isEmpty + ? true + : false case (false, true): - needToDownload = notCachedFiles.isEmpty - ? previewFiles - : notCachedFiles - - downloadPreview = notCachedFiles.isEmpty - ? true - : false - - downloadFullMedia = notCachedFiles.isEmpty - ? false - : true + needToDownload = + notCachedFiles.isEmpty + ? previewFiles + : notCachedFiles + + downloadPreview = + notCachedFiles.isEmpty + ? true + : false + + downloadFullMedia = + notCachedFiles.isEmpty + ? false + : true case (false, false): - needToDownload = previewFiles.isEmpty - ? notCachedFiles - : previewFiles - - downloadPreview = previewFiles.isEmpty - ? false - : true - - downloadFullMedia = previewFiles.isEmpty - ? true - : false - } - + needToDownload = + previewFiles.isEmpty + ? notCachedFiles + : previewFiles + + downloadPreview = + previewFiles.isEmpty + ? false + : true + + downloadFullMedia = + previewFiles.isEmpty + ? true + : false + } + needToDownload.forEach { file in downloadFile( file: file, @@ -879,7 +916,7 @@ final class ChatViewModel: NSObject { ) } } - + func downloadFile( file: ChatFile, previewDownloadAllowed: Bool, @@ -895,37 +932,37 @@ final class ChatViewModel: NSObject { ) } } - + func presentActionMenu() { dialog.send(.actionMenu) } - + func didSelectMenuAction(_ action: ShareType) { - if case(.sendTokens) = action { + if case (.sendTokens) = action { presentSendTokensVC.send() } - - if case(.uploadMedia) = action { + + if case (.uploadMedia) = action { presentMediaPickerVC.send() } - - if case(.uploadFile) = action { + + if case (.uploadFile) = action { presentDocumentPickerVC.send() } } - + @MainActor func processFileResult(_ result: Result<[FileResult], Error>) { switch result { case .success(let files): var oldFiles = filesPicked ?? [] - + files.forEach { file in if !oldFiles.contains(where: { $0.assetId == file.assetId }) { oldFiles.append(file) } } - + if oldFiles.count > FilesConstants.maxFilesCount { let numberOfExtraElements = oldFiles.count - FilesConstants.maxFilesCount let extraFilesToRemove = oldFiles.prefix(numberOfExtraElements) @@ -933,103 +970,93 @@ final class ChatViewModel: NSObject { let urls = [file.url] + (file.previewUrl.map { [$0] } ?? []) filesStorage.removeTempFiles(at: urls) } - + oldFiles.removeFirst(numberOfExtraElements) } - + filesPicked = oldFiles case .failure(let error): dialog.send(.alert(error.localizedDescription)) } } - + func presentDialog(progress: Bool) { dialog.send(.progress(progress)) } - + func dropSessionUpdated(_ value: Bool) { presentDropView.send(value) } - + func updatePreviewFor(indexes: [IndexPath]) { indexes.forEach { index in guard let message = messages[safe: index.section], - case let .file(model) = message.content + case let .file(model) = message.content else { return } - + autoDownloadContentIfNeeded( messageId: message.messageId, files: model.value.content.fileModel.files ) } } - + func updateSwipeableId(_ id: String?) { swipeableMessage = id.map { .init(id: $0, state: .idle) } ?? .default } - + func updateSwipingOffset(_ offset: CGFloat) { swipeableMessage.state = .offset(offset) } -} -extension ChatViewModel { - func getTempOffset(visibleIndex: Int?) -> String? { - let lastId = tempOffsets.popLast() - - guard let visibleIndex = visibleIndex, - let index = messages.firstIndex(where: { $0.messageId == lastId }) - else { - return lastId + func checkForADMNodesAvailability() { + if apiServiceCompose.get(.adm)?.hasEnabledNode == false { + guard needsToShowNoActiveNodesAlert == true else { return } + dialog.send(.noActiveNodesAlert) + needsToShowNoActiveNodesAlert = false + } else { + needsToShowNoActiveNodesAlert = true } - - return index > visibleIndex ? lastId : nil } - - func appendTempOffset(_ id: String, toId: String) { - guard let indexFrom = messages.firstIndex(where: { $0.messageId == id }), - let indexTo = messages.firstIndex(where: { $0.messageId == toId }), - (indexFrom - indexTo) >= minDiffCountForOffset - else { - return - } - - if let index = tempOffsets.firstIndex(of: id) { - tempOffsets.remove(at: index) + + func checkUpdateState() { + Task { @MainActor in + let isUpdating = await chatsProvider.state.isUpdating + self.isHeaderLoading = isUpdating } - - tempOffsets.append(id) } - +} + +extension ChatViewModel { func openPartnerQR() { guard let partner = chatroom?.partner, - isSendingAvailable + isSendingAvailable else { return } - + didTapPartnerQR.send(partner) } - + func renamePartner() { guard isSendingAvailable else { return } - + dialog.send(.renameAlert) } - + func updatePartnerName() { partnerName = chatroom?.getName(addressBookService: addressBookService) - } + } func updateFiles(_ data: [FileResult]?) { if (data?.count ?? .zero) == .zero { let previewUrls = filesPicked?.compactMap { $0.previewUrl } ?? [] let fileUrls = filesPicked?.compactMap { $0.url } ?? [] - + filesStorage.removeTempFiles(at: previewUrls + fileUrls) } - + filesPicked = data } - + func handlePastedImage(_ image: UIImage) { do { let file = try filesPicker.getFileResult(for: image) @@ -1038,21 +1065,22 @@ extension ChatViewModel { processFileResult(.failure(error)) } } - + func checkTopMessage(indexPath: IndexPath) { guard let message = messages[safe: indexPath.section], - let date = message.dateHeader?.string.string, - message.sentDate != .adamantNullDate + let date = message.dateHeader?.string.string, + message.sentDate != .adamantNullDate else { return } dateHeader = date dateHeaderHidden = false hideHeaderTimer?.cancel() hideHeaderTimer = nil } - + func startHideDateTimer() { hideHeaderTimer?.cancel() - hideHeaderTimer = Timer + hideHeaderTimer = + Timer .publish(every: delayHideHeaderInSeconds, on: .main, in: .common) .autoconnect() .first() @@ -1060,29 +1088,38 @@ extension ChatViewModel { self?.dateHeaderHidden = true } } + + func unredMessageCount() -> Int? { + unreadMessagesIds?.count + } } extension ChatViewModel: NSFetchedResultsControllerDelegate { nonisolated func controllerDidChangeContent(_: NSFetchedResultsController) { - Task { @MainActor in updateTransactions(performFetch: false) } + Task { @MainActor in didUpdateCoreData.send() } + //TODO: solve the problem with the СoreData updating too often (trello.com/c/iXjNrBsv) } } -private extension ChatViewModel { - func sendFiles(with text: String) async throws { +extension ChatViewModel { + fileprivate func sendFiles(with text: String) async throws { guard apiServiceCompose.get(.ipfs)?.hasEnabledNode == true else { - dialog.send(.alert(ApiServiceError.noEndpointsAvailable( - nodeGroupName: NodeGroup.ipfs.name - ).localizedDescription)) + dialog.send( + .alert( + ApiServiceError.noEndpointsAvailable( + nodeGroupName: NodeGroup.ipfs.name + ).localizedDescription + ) + ) return } - + let replyMessage = replyMessage let filesPicked = filesPicked - + self.replyMessage = nil self.filesPicked = nil - + try await chatFileService.sendFile( text: text, chatroom: chatroom, @@ -1091,18 +1128,37 @@ private extension ChatViewModel { saveEncrypted: filesStorageProprieties.saveFileEncrypted() ) } - - func setupObservers() { + + fileprivate func setupObservers() { $inputText .removeDuplicates() - .sink { [weak self] _ in self?.inputTextUpdated() } + .sink { [weak self] _ in self?.updateFeeValue() } + .store(in: &subscriptions) + + $messages + .map { messages in + Set( + messages.enumerated() + .filter { $0.element.isUnread } + .map { $0.offset } + ) + } + .removeDuplicates() + .sink { [weak self] unreadIndexes in + self?.unreadMesaggesIndexes = unreadIndexes + } + .store(in: &subscriptions) + $messages + .removeDuplicates() + .sink { [weak self] _ in + self?.updateSeparatorIndex() + } .store(in: &subscriptions) - chatFileService.updateFileFields .receive(on: DispatchQueue.main) .sink { [weak self] data in guard let self = self else { return } - + let fileProprieties = FileUpdateProperties( id: data.id, newId: data.newId, @@ -1113,16 +1169,16 @@ private extension ChatViewModel { uploading: data.uploading, progress: data.progress ) - + self.updateFileFields( &self.messages, fileProprieties: fileProprieties ) - + updateAutoDownloadWarning(messages: &messages) } .store(in: &subscriptions) - + $swipeableMessage .removeDuplicates() .sink { [weak self] _ in @@ -1130,21 +1186,28 @@ private extension ChatViewModel { updateSwipeStates(messages: &messages) } .store(in: &subscriptions) - - NotificationCenter.default - .notifications(named: .AdamantVisibleWalletsService.visibleWallets) - .sink { @MainActor [weak self] _ in self?.updateAttachmentButtonAvailability() } + + $unreadMessagesIds + .removeDuplicates() + .sink { [weak self] newValue in + self?.updateScrolledMessageState(newUnreadIds: newValue) + } + .store(in: &subscriptions) + + visibleWalletsService.statePublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in self?.updateAttachmentButtonAvailability() } .store(in: &subscriptions) - + Task { await chatsProvider.stateObserver .receive(on: DispatchQueue.main) .sink { [weak self] state in - self?.isHeaderLoading = state == .updating ? true : false + self?.isHeaderLoading = state.isUpdating } .store(in: &subscriptions) }.stored(in: tasksStorage) - + dropInteractionService.onPreparedDataCallback = { [weak self] result in Task { @MainActor in self?.dropSessionUpdated(false) @@ -1152,70 +1215,74 @@ private extension ChatViewModel { self?.processFileResult(result) } } - + dropInteractionService.onPreparingDataCallback = { [weak self] in Task { @MainActor in self?.presentDialog(progress: true) } } - + dropInteractionService.onSessionCallback = { [weak self] fileOnScreen in self?.dropSessionUpdated(fileOnScreen) } - + mediaPickerDelegate.onPreparedDataCallback = { [weak self] result in Task { @MainActor in self?.presentDialog(progress: false) self?.processFileResult(result) } } - + mediaPickerDelegate.onPreparingDataCallback = { [weak self] in Task { @MainActor in self?.presentDialog(progress: true) } } - + documentPickerDelegate.onPreparedDataCallback = { [weak self] result in Task { @MainActor in self?.presentDialog(progress: false) self?.processFileResult(result) } } - + documentPickerDelegate.onPreparingDataCallback = { [weak self] in Task { @MainActor in self?.presentDialog(progress: true) } } - + filesStorageProprieties.autoDownloadFullMediaPolicyPublisher .combineLatest(filesStorageProprieties.autoDownloadPreviewPolicyPublisher) .sink { [weak self] _ in self?.autoDownloadPolicyChanged() } .store(in: &subscriptions) + //this is a temporary solution to fix the issue with excessive notifications from date related to subsequent recording of messages, it will require a lot of time within a separate task + didUpdateCoreData + .debounce(for: .milliseconds(50), scheduler: DispatchQueue.main) + .sink { [weak self] in self?.updateTransactions(performFetch: false) } + .store(in: &subscriptions) } - - func loadMessages(address: String, offset: Int) async { + + fileprivate func loadMessages(address: String, offset: Int) async { guard !isLoading else { return } isLoading = true - + await chatsProvider.getChatMessages( with: address, offset: offset ) - updateTransactions(performFetch: true) } - - func updateTransactions(performFetch: Bool) { + + fileprivate func updateTransactions(performFetch: Bool) { if performFetch { try? controller?.performFetch() } - + let newTransactions = controller?.fetchedObjects ?? [] let isNewReaction = isNewReaction(old: chatTransactions, new: newTransactions) chatTransactions = newTransactions - + updateMessages( resetLoadingProperty: performFetch, completion: isNewReaction @@ -1223,63 +1290,61 @@ private extension ChatViewModel { : {} ) } - - func updateMessages( + + fileprivate func updateMessages( resetLoadingProperty: Bool, completion: @MainActor @escaping () -> Void = {} ) { timerSubscription = nil - + Task(priority: .userInitiated) { [chatTransactions, sender] in defer { completion() } var expirationTimestamp: TimeInterval? - var messages = await chatMessagesListFactory.makeMessages( + var (messages, reactId, messageId) = await chatMessagesListFactory.makeMessages( transactions: chatTransactions, sender: sender, isNeedToLoadMoreMessages: isNeedToLoadMoreMessages, expirationTimestamp: &expirationTimestamp ) - + + messagesWithUnredReactionsIds = reactId + unreadMessagesIds = messageId postProcess(messages: &messages) - + setupNewMessages( newMessages: messages, resetLoadingProperty: resetLoadingProperty, expirationTimestamp: expirationTimestamp ) - - // The 'makeMessages' method doesn't include reactions. - // If the message count is different from the number of transactions, update the chat read status if necessary. - if messages.count != chatTransactions.count { - updateChatRead.send() - } + updateSeparatorId() + messagesUpdated.send() } } - - func postProcess(messages: inout [ChatMessage]) { + + fileprivate func postProcess(messages: inout [ChatMessage]) { let indexes = messages.indices.filter { messages[$0].getFiles().count > .zero } - + indexes.forEach { index in guard case let .file(model) = messages[index].content else { return } - + model.value.content.fileModel.files.forEach { file in setupFileFields(file, messages: &messages, index: index) } } - + updateAutoDownloadWarning(messages: &messages) updateSwipeStates(messages: &messages) } - - func autoDownloadPolicyChanged() { + + fileprivate func autoDownloadPolicyChanged() { updateAutoDownloadWarning(messages: &messages) - + indexPathsForVisibleItems().forEach { index in guard let message = messages[safe: index.section] else { return } - + switch message.content { case let .file(model): autoDownloadContentIfNeeded( @@ -1291,8 +1356,8 @@ private extension ChatViewModel { } } } - - func updateAutoDownloadWarning(messages: inout [ChatMessage]) { + + fileprivate func updateAutoDownloadWarning(messages: inout [ChatMessage]) { messages.indices.forEach { messages[$0].updateFileFields { $0.content.fileModel.showAutoDownloadWarningLabel = showAutoDownloadWarning( @@ -1302,16 +1367,17 @@ private extension ChatViewModel { } } } - - func updateSwipeStates(messages: inout [ChatMessage]) { + + fileprivate func updateSwipeStates(messages: inout [ChatMessage]) { messages.indices.forEach { - messages[$0].swipeState = messages[$0].id == swipeableMessage.id + messages[$0].swipeState = + messages[$0].id == swipeableMessage.id ? swipeableMessage.state : .idle } } - - func showAutoDownloadWarning( + + fileprivate func showAutoDownloadWarning( files: [ChatFile], isFromCurrentSender: Bool ) -> Bool { @@ -1320,34 +1386,34 @@ private extension ChatViewModel { isFromCurrentSender: isFromCurrentSender, downloadPolicy: filesStorageProprieties.autoDownloadPreviewPolicy() ) - + let hasNoPreview = files.contains { $0.fileType.isMedia - && $0.previewImage == nil - && $0.file.preview.map { !filesStorage.isCachedLocally($0.id) } ?? false + && $0.previewImage == nil + && $0.file.preview.map { !filesStorage.isCachedLocally($0.id) } ?? false } return !isPreviewDownloadAllowed && hasNoPreview } - - func setupFileFields( + + fileprivate func setupFileFields( _ file: ChatFile, - messages: inout[ChatMessage], + messages: inout [ChatMessage], index: Int ) { let fileId = file.file.id - + let previewImage = (file.file.preview?.id).flatMap { !$0.isEmpty - ? filesStorage.getPreview(for: $0) - : nil + ? filesStorage.getPreview(for: $0) + : nil } - + let progress = chatFileService.filesLoadingProgress[fileId] let downloadStatus = chatFileService.downloadingFiles[fileId] ?? .default let cached = filesStorage.isCachedLocally(fileId) let isUploading = chatFileService.uploadingFiles.contains(fileId) - + let fileProprieties = FileUpdateProperties( id: file.file.id, newId: nil, @@ -1358,46 +1424,46 @@ private extension ChatViewModel { uploading: isUploading, progress: progress ) - + updateFileMessageFields(for: &messages[index], fileProprieties: fileProprieties) } - - func setupNewMessages( + + fileprivate func setupNewMessages( newMessages: [ChatMessage], resetLoadingProperty: Bool, expirationTimestamp: TimeInterval? ) { var newMessages = newMessages updateHiddenMessage(&newMessages) - + messages = newMessages - + if let address = chatroom?.partner?.address { chatCacheService.setMessages(address: address, messages: newMessages) } - + if resetLoadingProperty { isLoading = false fullscreenLoading = false } - + guard let expirationTimestamp = expirationTimestamp else { return } setupMessagesUpdateTimer(expirationTimestamp: expirationTimestamp) } - - func setupMessagesUpdateTimer(expirationTimestamp: TimeInterval) { + + fileprivate func setupMessagesUpdateTimer(expirationTimestamp: TimeInterval) { let currentTimestamp = Date().timeIntervalSince1970 guard currentTimestamp < expirationTimestamp else { return } let interval = expirationTimestamp - currentTimestamp - + timerSubscription = Timer.publish(every: interval, on: .main, in: .common) .autoconnect() .sink { [weak self] _ in self?.updateMessages(resetLoadingProperty: false) } } - - func validateSendingMessage(message: AdamantMessage) async -> Bool { + + fileprivate func validateSendingMessage(message: AdamantMessage) async -> Bool { let validationStatus = await chatsProvider.validateMessage(message) - + switch validationStatus { case .isValid: return true @@ -1408,8 +1474,8 @@ private extension ChatViewModel { return false } } - - func handleMessageSendingError( + + fileprivate func handleMessageSendingError( error: Error, sentText: String, filesPicked: [FileResult]? = nil @@ -1424,37 +1490,51 @@ private extension ChatViewModel { dialog.send(.freeTokenAlert) return } - case .serverError: - dialog.send(.richError(error)) - case .accountNotFound, .accountNotInitiated, .dependencyError, .internalError, .networkError, .notLogged, .requestCancelled, .transactionNotFound, .invalidTransactionStatus, .none: + case let .serverError(error): + if case .timestampIsInTheFuture = error { + dialog.send(.timestampIsInTheFuture) + } + case .accountNotFound, .accountNotInitiated, .dependencyError, .internalError, .networkError, .notLogged, .requestCancelled, .transactionNotFound, + .invalidTransactionStatus, .none: break } } - - func inputTextUpdated() { - guard !inputText.isEmpty else { + + fileprivate func updateFeeValue() { + let pickedFilesCount = filesPicked?.count ?? .zero + guard + let feeValue: Decimal = + switch (inputText, pickedFilesCount) { + case (inputText, pickedFilesCount) where inputText.isEmpty && pickedFilesCount == .zero: + nil + case (inputText, pickedFilesCount) where inputText.isEmpty && pickedFilesCount > .zero: + 0.001 + default: + AdamantMessage.text(inputText).fee + } + else { fee = "" return } - + let feeString = AdamantBalanceFormat.full.format( - AdamantMessage.text(inputText).fee, + feeValue, withCurrencySymbol: AdmWalletService.currencySymbol ) - + fee = "~\(feeString)" } - - func updatePartnerInformation() { + + fileprivate func updatePartnerInformation() { guard let publicKey = chatroom?.partner?.publicKey else { return } - + partnerName = chatroom?.getName(addressBookService: addressBookService) hasPartnerName = chatroom?.hasPartnerName(addressBookService: addressBookService) ?? false - + guard let avatarName = chatroom?.partner?.avatar, - let avatar = UIImage.asset(named: avatarName) + let avatar = UIImage.asset(named: avatarName) else { partnerImage = avatarService.avatar( for: publicKey, @@ -1462,23 +1542,23 @@ private extension ChatViewModel { ) return } - + partnerImage = avatar } - - func updateAttachmentButtonAvailability() { + + fileprivate func updateAttachmentButtonAvailability() { let isAnyWalletVisible = walletServiceCompose.getWallets() - .map { visibleWalletService.isInvisible($0.core) } + .map { walletsStoreService.isInvisible($0) } .contains(false) - + isAttachmentButtonAvailable = isAnyWalletVisible } - - func findAccount(with address: String, name: String?, message: String?) async { + + fileprivate func findAccount(with address: String, name: String?, message: String?) async { dialog.send(.progress(true)) do { let account = try await accountProvider.getAccount(byAddress: address) - + self.dialog.send(.progress(false)) guard let chatroom = account.chatroom else { return } self.setNameIfNeeded(for: account, chatroom: account.chatroom, name: name) @@ -1495,48 +1575,51 @@ private extension ChatViewModel { case .serverError(let apiError): self.dialog.send(.progress(false)) if let apiError = apiError as? ApiServiceError, - case .internalError(let message, _) = apiError, - message == String.adamant.sharedErrors.unknownError { + case .internalError(let message, _) = apiError, + message == String.adamant.sharedErrors.unknownError + { self.dialog.send(.alert(AccountsProviderError.notFound(address: address).localized)) return } - + self.dialog.send(.error(error.localized, supportEmail: false)) } } catch { - self.dialog.send(.error( - error.localizedDescription, - supportEmail: false - )) + self.dialog.send( + .error( + error.localizedDescription, + supportEmail: false + ) + ) } } - - func setNameIfNeeded(for account: CoreDataAccount?, chatroom: Chatroom?, name: String?) { + + fileprivate func setNameIfNeeded(for account: CoreDataAccount?, chatroom: Chatroom?, name: String?) { guard let name = name, - let account = account, - let address = account.address, - account.name == nil, - addressBookService.getName(for: address) == nil + let account = account, + let address = account.address, + account.name == nil, + addressBookService.getName(for: address) == nil else { return } - + Task { await addressBookService.set(name: name, for: address) }.stored(in: tasksStorage) - + account.name = name if let chatroom = chatroom, chatroom.title == nil { chatroom.title = name } } - - func startNewChat(with chatroom: Chatroom, name: String? = nil, message: String? = nil) { + + fileprivate func startNewChat(with chatroom: Chatroom, name: String? = nil, message: String? = nil) { setNameIfNeeded(for: chatroom.partner, chatroom: chatroom, name: name) didTapAdmChat.send((chatroom, message)) } - - func waitForChatLoading(with address: String) async { + + fileprivate func waitForChatLoading(with address: String) async { await withUnsafeContinuation { continuation in Task { await chatsProvider.chatLoadingStatusPublisher @@ -1553,32 +1636,32 @@ private extension ChatViewModel { } } } - + // TODO: Post process - func updateHiddenMessage(_ messages: inout [ChatMessage]) { + fileprivate func updateHiddenMessage(_ messages: inout [ChatMessage]) { messages.indices.forEach { messages[$0].isHidden = messages[$0].id == hiddenMessageID } } - - func updateFileFields( + + fileprivate func updateFileFields( _ messages: inout [ChatMessage], fileProprieties: FileUpdateProperties ) { let indexes = messages.indices.filter { messages[$0].getFiles().contains { $0.file.id == fileProprieties.id } } - + guard !indexes.isEmpty else { return } - + indexes.forEach { index in updateFileMessageFields(for: &messages[index], fileProprieties: fileProprieties) } } - - func updateFileMessageFields( + + fileprivate func updateFileMessageFields( for message: inout ChatMessage, fileProprieties: FileUpdateProperties ) { @@ -1596,46 +1679,46 @@ private extension ChatViewModel { model.status = getStatus(from: model) } } - - func getStatus(from model: ChatMediaContainerView.Model) -> FileMessageStatus { + + fileprivate func getStatus(from model: ChatMediaContainerView.Model) -> FileMessageStatus { if model.txStatus == .failed { return .failed } - + if model.content.fileModel.files.first(where: { $0.isBusy }) != nil { return .busy } - + if model.content.fileModel.files.contains(where: { - !$0.isCached || - ($0.isCached - && $0.file.preview != nil - && $0.previewImage == nil - && ($0.fileType == .image || $0.fileType == .video)) + !$0.isCached + || ($0.isCached + && $0.file.preview != nil + && $0.previewImage == nil + && ($0.fileType == .image || $0.fileType == .video)) }) { let failed = model.content.fileModel.files.contains(where: { guard let progress = $0.progress else { return false } return progress < 100 }) - + return .needToDownload(failed: failed) } - + return .success } - - func isNewReaction(old: [ChatTransaction], new: [ChatTransaction]) -> Bool { + + fileprivate func isNewReaction(old: [ChatTransaction], new: [ChatTransaction]) -> Bool { guard let processedDate = old.getMostRecentElementDate(), let newLastReactionDate = new.getMostRecentReactionDate() else { return false } - + return newLastReactionDate > processedDate } - - func presentFileInFullScreen(id: String, chatFiles: [ChatFile]) { + + fileprivate func presentFileInFullScreen(id: String, chatFiles: [ChatFile]) { dialog.send(.progress(true)) - + let files: [FileResult] = chatFiles.compactMap { file in guard file.isCached, @@ -1645,13 +1728,13 @@ private extension ChatViewModel { else { return nil } - + let data = try? chatFileService.getDecodedData( file: fileDTO, nonce: file.file.nonce, chatroom: chatroom ) - + return FileResult.init( assetId: file.file.id, url: fileDTO.url, @@ -1666,15 +1749,55 @@ private extension ChatViewModel { data: data ) } - + dialog.send(.progress(false)) let index = files.firstIndex(where: { $0.assetId == id }) ?? .zero presentDocumentViewerVC.send((files, index)) } + + fileprivate func updateScrolledMessageState(newUnreadIds: OrderedSet?) { + let newUnreadIds = newUnreadIds ?? [] + let previousUnreadIds = scrolledMessageId ?? [] + let hasNewIds = !newUnreadIds.isSubset(of: previousUnreadIds) + + if hasNewIds { + scrolledMessageId = previousUnreadIds.union(newUnreadIds) + shouldScrollToBottom = false + } + + if newUnreadIds.isEmpty { + shouldScrollToBottom = true + } + } + + fileprivate func updateSeparatorId() { + guard !didAddSeparator, !messages.isEmpty else { return } + + didAddSeparator = true + guard let firstUnreadId = unreadMessagesIds?.first else { + separatorId = nil + separatorIndex = nil + return + } + + separatorId = firstUnreadId + updateSeparatorIndex() + } + + fileprivate func updateSeparatorIndex() { + guard let separatorId = separatorId, + let index = messages.firstIndex(where: { $0.id == separatorId }) + else { + separatorIndex = nil + return + } + + separatorIndex = index - 1 + } } -private extension ChatMessage { - var messageModel: MessageModel { +extension ChatMessage { + fileprivate var messageModel: MessageModel { switch content { case let .message(model): return model.value @@ -1686,8 +1809,8 @@ private extension ChatMessage { return model.value } } - - var isFromCurrentSender: Bool { + + fileprivate var isFromCurrentSender: Bool { switch content { case let .message(model): return model.value.isFromCurrentSender @@ -1699,8 +1822,8 @@ private extension ChatMessage { return model.value.isFromCurrentSender } } - - var isHidden: Bool { + + fileprivate var isHidden: Bool { get { switch content { case let .message(model): @@ -1713,7 +1836,7 @@ private extension ChatMessage { return model.value.content.isHidden } } - + set { switch content { case let .message(model): @@ -1735,8 +1858,8 @@ private extension ChatMessage { } } } - - var swipeState: ChatSwipeWrapperModel.State { + + fileprivate var swipeState: ChatSwipeWrapperModel.State { get { switch content { case let .message(model): @@ -1749,7 +1872,7 @@ private extension ChatMessage { return model.value.swipeState } } - + set { switch content { case let .message(model): @@ -1771,61 +1894,63 @@ private extension ChatMessage { } } } - - func getFiles() -> [ChatFile] { + + fileprivate func getFiles() -> [ChatFile] { guard case let .file(model) = content else { return [] } return model.value.content.fileModel.files } - - mutating func updateFileFields( + + fileprivate mutating func updateFileFields( id: String, mutateFile: (inout ChatFile) -> Void, mutateModel: (inout ChatMediaContainerView.Model) -> Void ) { guard case let .file(fileModel) = content else { return } var model = fileModel.value - - guard let index = model.content.fileModel.files.firstIndex( - where: { $0.file.id == id } - ) else { return } - + + guard + let index = model.content.fileModel.files.firstIndex( + where: { $0.file.id == id } + ) + else { return } + let previousValue = model - + mutateFile(&model.content.fileModel.files[index]) mutateModel(&model) - + guard model != previousValue else { return } - + content = .file(.init(value: model)) } - - mutating func updateFileFields( + + fileprivate mutating func updateFileFields( mutateModel: (inout ChatMediaContainerView.Model) -> Void ) { guard case let .file(fileModel) = content else { return } var model = fileModel.value let previousValue = model mutateModel(&model) - + guard model != previousValue else { return } content = .file(.init(value: model)) } } -private extension Sequence where Element == ChatTransaction { - func getMostRecentElementDate() -> Date? { +extension Sequence where Element == ChatTransaction { + fileprivate func getMostRecentElementDate() -> Date? { map { $0.sentDate ?? .adamantNullDate }.max() } - - func getMostRecentReactionDate() -> Date? { + + fileprivate func getMostRecentReactionDate() -> Date? { compactMap { guard let tx = $0 as? RichMessageTransaction, tx.additionalType == .reaction else { return nil } - + return $0.sentDate ?? .adamantNullDate }.max() } @@ -1834,25 +1959,27 @@ private extension Sequence where Element == ChatTransaction { extension ChatViewModel: ElegantEmojiPickerDelegate { nonisolated func emojiPicker(_ picker: ElegantEmojiPicker, didSelectEmoji emoji: Emoji?) { let sendableEmoji = Atomic(emoji) - + MainActor.assumeIsolatedSafe { dialog.send(.dismissMenu) - + guard let previousArg = previousArg else { return } - - let emoji = sendableEmoji.value?.emoji == previousArg.selectedEmoji - ? "" - : (sendableEmoji.value?.emoji ?? "") - - let type: EmojiUpdateType = emoji.isEmpty - ? .decrement - : .increment - + + let emoji = + sendableEmoji.value?.emoji == previousArg.selectedEmoji + ? "" + : (sendableEmoji.value?.emoji ?? "") + + let type: EmojiUpdateType = + emoji.isEmpty + ? .decrement + : .increment + emojiService.updateFrequentlySelectedEmojis( selectedEmoji: emoji, type: type ) - + reactAction(previousArg.messageId, emoji: emoji) } } diff --git a/Adamant/Modules/Chat/ViewModel/Models/ChatDialog.swift b/Adamant/Modules/Chat/ViewModel/Models/ChatDialog.swift index f572003a8..6efbdab90 100644 --- a/Adamant/Modules/Chat/ViewModel/Models/ChatDialog.swift +++ b/Adamant/Modules/Chat/ViewModel/Models/ChatDialog.swift @@ -6,9 +6,9 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit import CommonKit import ElegantEmojiPicker +import UIKit enum ChatDialog { case toast(String) @@ -17,6 +17,8 @@ enum ChatDialog { case warning(String) case richError(Error) case freeTokenAlert + case noActiveNodesAlert + case timestampIsInTheFuture case removeMessageAlert(id: String) case reportMessageAlert(id: String) case menu(sender: UIBarButtonItem) diff --git a/Adamant/Modules/Chat/ViewModel/Models/ChatMessage.swift b/Adamant/Modules/Chat/ViewModel/Models/ChatMessage.swift index cc12d6399..e01452db9 100644 --- a/Adamant/Modules/Chat/ViewModel/Models/ChatMessage.swift +++ b/Adamant/Modules/Chat/ViewModel/Models/ChatMessage.swift @@ -6,9 +6,9 @@ // Copyright © 2023 Adamant. All rights reserved. // +import CommonKit import MessageKit import UIKit -import CommonKit struct ChatMessage: Identifiable, Equatable, Sendable { let id: String @@ -21,7 +21,8 @@ struct ChatMessage: Identifiable, Equatable, Sendable { let dateHeader: ComparableAttributedString? let topSpinnerOn: Bool let dateHeaderIsHidden: Bool - + var isUnread: Bool + static var `default`: Self { Self( id: "", @@ -33,7 +34,8 @@ struct ChatMessage: Identifiable, Equatable, Sendable { bottomString: nil, dateHeader: nil, topSpinnerOn: false, - dateHeaderIsHidden: true + dateHeaderIsHidden: true, + isUnread: true ) } } @@ -41,22 +43,22 @@ struct ChatMessage: Identifiable, Equatable, Sendable { extension ChatMessage { struct EqualWrapper: Equatable { let value: Value - + static func == (lhs: Self, rhs: Self) -> Bool { true } } - + enum Status: Equatable { case delivered(blockchain: Bool) case pending case failed } - + enum Content: Equatable, Sendable { case message(EqualWrapper) case transaction(EqualWrapper) - case reply(EqualWrapper) + case reply(EqualWrapper) case file(EqualWrapper) - + static var `default`: Self { Self.message(.init(value: .default)) } @@ -66,7 +68,7 @@ extension ChatMessage { extension ChatMessage: MessageType { var messageId: String { id } var sender: SenderType { senderModel } - + var kind: MessageKind { switch content { case let .message(model): @@ -74,9 +76,10 @@ extension ChatMessage: MessageType { case let .transaction(model): return .custom(model) case let .reply(model): - let message = model.value.message.string.count > model.value.messageReply.string.count - ? model.value.message - : model.value.messageReply + let message = + model.value.message.string.count > model.value.messageReply.string.count + ? model.value.message + : model.value.messageReply return .attributedText(message) case let .file(model): return .custom(model) diff --git a/Adamant/Modules/Chat/ViewModel/Models/ChatMessageBackgroundColor.swift b/Adamant/Modules/Chat/ViewModel/Models/ChatMessageBackgroundColor.swift index 79ea75743..d5bfab4f1 100644 --- a/Adamant/Modules/Chat/ViewModel/Models/ChatMessageBackgroundColor.swift +++ b/Adamant/Modules/Chat/ViewModel/Models/ChatMessageBackgroundColor.swift @@ -13,7 +13,7 @@ enum ChatMessageBackgroundColor: Equatable { case delivered case pending case failed - + var uiColor: UIColor { switch self { case .opponent: diff --git a/Adamant/Modules/Chat/ViewModel/Models/ChatSender.swift b/Adamant/Modules/Chat/ViewModel/Models/ChatSender.swift index fb20e8791..089710dce 100644 --- a/Adamant/Modules/Chat/ViewModel/Models/ChatSender.swift +++ b/Adamant/Modules/Chat/ViewModel/Models/ChatSender.swift @@ -11,6 +11,6 @@ import MessageKit struct ChatSender: SenderType, Equatable { let senderId: String let displayName: String - + static let `default` = Self(senderId: "", displayName: "") } diff --git a/Adamant/Modules/Chat/ViewModel/Models/MessageModel.swift b/Adamant/Modules/Chat/ViewModel/Models/MessageModel.swift index c78d79cf1..e4ff99329 100644 --- a/Adamant/Modules/Chat/ViewModel/Models/MessageModel.swift +++ b/Adamant/Modules/Chat/ViewModel/Models/MessageModel.swift @@ -10,6 +10,6 @@ import UIKit protocol MessageModel: Sendable { var id: String { get } - + func makeReplyContent() -> NSAttributedString } diff --git a/Adamant/Modules/ChatsList/ChatListFactory.swift b/Adamant/Modules/ChatsList/ChatListFactory.swift index c830143a5..256b5bc49 100644 --- a/Adamant/Modules/ChatsList/ChatListFactory.swift +++ b/Adamant/Modules/ChatsList/ChatListFactory.swift @@ -6,14 +6,14 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import Swinject import CommonKit +import Swinject +import UIKit @MainActor struct ChatListFactory { let assembler: Assembler - + func makeChatListVC(screensFactory: ScreensFactory) -> UIViewController { ChatListViewController( accountService: assembler.resolve(AccountService.self)!, @@ -24,10 +24,11 @@ struct ChatListFactory { dialogService: assembler.resolve(DialogService.self)!, addressBook: assembler.resolve(AddressBookService.self)!, avatarService: assembler.resolve(AvatarService.self)!, - walletServiceCompose: assembler.resolve(WalletServiceCompose.self)! + walletServiceCompose: assembler.resolve(WalletServiceCompose.self)!, + chatPreservation: assembler.resolve(ChatPreservationProtocol.self)! ) } - + func makeNewChatVC(screensFactory: ScreensFactory) -> NewChatViewController { let c = NewChatViewController() c.dialogService = assembler.resolve(DialogService.self) @@ -36,17 +37,17 @@ struct ChatListFactory { c.screensFactory = screensFactory return c } - + func makeComplexTransferVC(screensFactory: ScreensFactory) -> UIViewController { ComplexTransferViewController( - visibleWalletsService: assembler.resolve(VisibleWalletsService.self)!, + walletsStoreService: assembler.resolve(WalletStoreServiceProtocol.self)!, addressBookService: assembler.resolve(AddressBookService.self)!, screensFactory: screensFactory, - walletServiceCompose: assembler.resolve(WalletServiceCompose.self)!, + walletServiceCompose: assembler.resolve(WalletServiceCompose.self)!, nodesStorage: assembler.resolve(NodesStorageProtocol.self)! ) } - + func makeSearchResultsViewController(screensFactory: ScreensFactory) -> SearchResultsViewController { SearchResultsViewController( screensFactory: screensFactory, diff --git a/Adamant/Modules/ChatsList/ChatListViewController.swift b/Adamant/Modules/ChatsList/ChatListViewController.swift index 6783bb9f6..85eaf9722 100644 --- a/Adamant/Modules/ChatsList/ChatListViewController.swift +++ b/Adamant/Modules/ChatsList/ChatListViewController.swift @@ -6,12 +6,12 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit +import Combine +import CommonKit @preconcurrency import CoreData import MarkdownKit import MessageKit -import Combine -import CommonKit +import UIKit extension String.adamant { enum chatList { @@ -41,13 +41,13 @@ extension String.adamant { final class ChatListViewController: KeyboardObservingViewController { typealias SpinnerCell = TableCellWrapper - + let cellIdentifier = "cell" let loadingCellIdentifier = "loadingCell" let cellHeight: CGFloat = 76.0 - + // MARK: Dependencies - + private let accountService: AccountService private let chatsProvider: ChatsProvider private let transfersProvider: TransfersProvider @@ -57,29 +57,29 @@ final class ChatListViewController: KeyboardObservingViewController { private let addressBook: AddressBookService private let avatarService: AvatarService private let walletServiceCompose: WalletServiceCompose - + private let chatPreservation: ChatPreservationProtocol + // MARK: IBOutlet @IBOutlet weak var tableView: UITableView! @IBOutlet weak var newChatButton: UIBarButtonItem! - + private lazy var scrollUpButton = ChatScrollButton(position: .up) // MARK: Properties var chatsController: NSFetchedResultsController? var unreadController: NSFetchedResultsController? var searchController: UISearchController? - + private var transactionsRequiringBalanceUpdate: [String] = [] - + private var chatsManuallyMarkedAsUnread: Set = Set() { + didSet { + setBadgeValue(unreadController?.fetchedObjects?.count) + } + } + private var chatDeselectedIndex: IndexPath? + let defaultAvatar = UIImage.asset(named: "avatar-chat-placeholder") ?? .init() - - private lazy var refreshControl: UIRefreshControl = { - let refreshControl = UIRefreshControl() - refreshControl.addTarget(self, action: #selector(self.handleRefresh(_:)), for: UIControl.Event.valueChanged) - refreshControl.tintColor = .clear - return refreshControl - }() - + private lazy var markdownParser: MarkdownParser = { let parser = MarkdownParser( font: UIFont.systemFont(ofSize: ChatTableViewCell.shortDescriptionTextSize), @@ -109,37 +109,37 @@ final class ChatListViewController: KeyboardObservingViewController { MarkdownFileRaw(emoji: "📄", font: .adamantChatFileRawDefault) ] ) - + return parser }() - + private lazy var updatingIndicatorView: UpdatingIndicatorView = { let view = UpdatingIndicatorView(title: String.adamant.chatList.title) return view }() - + private var defaultSeparatorInstets: UIEdgeInsets? - + // MARK: Busy indicator - + @IBOutlet weak var busyBackgroundView: UIView! @IBOutlet weak var busyIndicatorView: UIView! @IBOutlet weak var busyIndicatorLabel: UILabel! - + private(set) var isBusy: Bool = true private var lastSystemChatPositionRow: Int? - + private var onMessagesLoadedActions = [() -> Void]() private var areMessagesLoaded = false private var lastDatesUpdate: Date = Date() - + // MARK: Tasks - + private var loadNewChatTask: Task<(), Never>? private var subscriptions = Set() - + private var swipedIndex: IndexPath? // MARK: Init - + init( accountService: AccountService, chatsProvider: ChatsProvider, @@ -149,7 +149,8 @@ final class ChatListViewController: KeyboardObservingViewController { dialogService: DialogService, addressBook: AddressBookService, avatarService: AvatarService, - walletServiceCompose: WalletServiceCompose + walletServiceCompose: WalletServiceCompose, + chatPreservation: ChatPreservationProtocol ) { self.accountService = accountService self.chatsProvider = chatsProvider @@ -160,50 +161,53 @@ final class ChatListViewController: KeyboardObservingViewController { self.addressBook = addressBook self.avatarService = avatarService self.walletServiceCompose = walletServiceCompose - + self.chatPreservation = chatPreservation + super.init(nibName: "ChatListViewController", bundle: nil) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: Lifecycle - + override func viewDidLoad() { super.viewDidLoad() - + setupNavigationController() - + setupTableView() - + if self.accountService.account != nil { initFetchedRequestControllers(provider: chatsProvider) } - + setupSearchController() - + // MARK: Busy Indicator busyIndicatorLabel.text = String.adamant.chatList.syncingChats - + busyIndicatorView.layer.cornerRadius = 14 busyIndicatorView.clipsToBounds = true - + configureScrollUpButton() addObservers() setColors() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - if let indexPath = tableView.indexPathForSelectedRow { - tableView.deselectRow(at: indexPath, animated: animated) + + if UIDevice.current.userInterfaceIdiom == .phone { + if let indexPath = tableView.indexPathForSelectedRow { + tableView.deselectRow(at: indexPath, animated: animated) + } } } - override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - + if UIScreen.main.traitCollection.userInterfaceIdiom == .pad { var insets = tableView.contentInset if let rect = self.tabBarController?.tabBar.frame { @@ -212,9 +216,9 @@ final class ChatListViewController: KeyboardObservingViewController { tableView.contentInset = insets } } - + // MARK: Navigation controller - + private func setupNavigationController() { navigationController?.navigationBar.prefersLargeTitles = true navigationItem.largeTitleDisplayMode = .never @@ -224,62 +228,60 @@ final class ChatListViewController: KeyboardObservingViewController { UIBarButtonItem(barButtonSystemItem: .search, target: self, action: #selector(beginSearch)) ] } - + // MARK: Search controller - + private func setupSearchController() { Task { isBusy = await !chatsProvider.isInitiallySynced if !isBusy { setIsBusy(false, animated: false) } - + loadNewChatTask?.cancel() let searchResultController = screensFactory.makeSearchResults() searchResultController.delegate = self - + searchController = UISearchController(searchResultsController: searchResultController) searchController?.searchResultsUpdater = self searchController?.searchBar.delegate = self searchController?.searchBar.placeholder = String.adamant.chatList.searchPlaceholder definesPresentationContext = true - + searchController?.obscuresBackgroundDuringPresentation = false searchController?.hidesNavigationBarDuringPresentation = true navigationItem.searchController = searchController } } - + // MARK: TableView - + private func setupTableView() { tableView.dataSource = self tableView.delegate = self tableView.register(UINib(nibName: "ChatTableViewCell", bundle: nil), forCellReuseIdentifier: cellIdentifier) tableView.register(SpinnerCell.self, forCellReuseIdentifier: loadingCellIdentifier) - tableView.refreshControl = refreshControl tableView.backgroundColor = .clear tableView.tableHeaderView = UIView() } - + func configureScrollUpButton() { view.addSubview(scrollUpButton) - + scrollUpButton.isHidden = true - + scrollUpButton.action = { [weak self] in self?.tableView.scrollToRow(at: IndexPath(row: .zero, section: .zero), at: .top, animated: true) } - + scrollUpButton.snp.makeConstraints { make in make.trailing.equalToSuperview().offset(-20) make.top.equalTo(view.safeAreaLayoutGuide).offset(20) make.size.equalTo(30) } } - + // MARK: Add Observers - private func addObservers() { // Login/Logout NotificationCenter.default @@ -288,15 +290,16 @@ final class ChatListViewController: KeyboardObservingViewController { self?.initFetchedRequestControllers(provider: self?.chatsProvider) } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedOut, object: nil) .sink { @MainActor [weak self] _ in self?.initFetchedRequestControllers(provider: nil) self?.areMessagesLoaded = false + self?.chatsManuallyMarkedAsUnread = Set() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantChatsProvider.initiallySyncedChanged, object: nil) .sink { @MainActor notification in @@ -305,32 +308,32 @@ final class ChatListViewController: KeyboardObservingViewController { } } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .LanguageStorageService.languageUpdated) .sink { @MainActor [weak self] _ in self?.updateUITitles() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .Storage.storageClear) .sink { @MainActor [weak self] _ in self?.closeDetailVC() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .Storage.storageProprietiesUpdated) .sink { @MainActor [weak self] _ in self?.closeDetailVC() } .store(in: &subscriptions) - + Task { let chatsProviderState = await chatsProvider.stateObserver let transfersProviderState = await transfersProvider.stateObserver - + chatsProviderState .combineLatest(transfersProviderState) .map { $0.0.isUpdating || $0.1.isUpdating } @@ -339,22 +342,38 @@ final class ChatListViewController: KeyboardObservingViewController { .sink { @MainActor [weak self] in self?.setIsStateUpdating($0) } .store(in: &subscriptions) } + chatPreservation.updateNotifier + .receive(on: DispatchQueue.main) + .sink { [weak self] in + guard let deselectedIndex = self?.chatDeselectedIndex, + let cell = self?.tableView.cellForRow(at: deselectedIndex) as? ChatTableViewCell, + let chatroom = self?.chatsController?.fetchedObjects?[safe: deselectedIndex.row] + else { return } + self?.configureCell(cell, for: chatroom) + } + .store(in: &subscriptions) } - + + @MainActor + private func updateChatsManuallyMarkedAsUnread() async { + let addresses = await chatsProvider.getMarkAdressesFromChain() + self.chatsManuallyMarkedAsUnread = addresses + } + private func closeDetailVC() { guard let splitVC = tabBarController?.viewControllers?.first as? UISplitViewController, - !splitVC.isCollapsed + !splitVC.isCollapsed else { return } - + splitVC.showDetailViewController(WelcomeViewController(), sender: nil) } - + private func updateUITitles() { - updatingIndicatorView.updateTitle(title: String.adamant.chatList.title) - tableView.reloadData() + updatingIndicatorView.updateTitle(title: String.adamant.chatList.title) + tableView.reloadDataPreservingSelection() searchController?.searchBar.placeholder = String.adamant.chatList.searchPlaceholder } - + private func setIsStateUpdating(_ isUpdating: Bool) { if isUpdating { updatingIndicatorView.startAnimate() @@ -363,30 +382,32 @@ final class ChatListViewController: KeyboardObservingViewController { updatingIndicatorView.stopAnimate() } } - + /// If the user opens the app from the background and new chats are not loaded, /// update specific rows in the tableView to refresh the dates. private func refreshDatesIfNeeded() { guard !isBusy, - let indexPaths = tableView.indexPathsForVisibleRows + var indexPaths = tableView.indexPathsForVisibleRows else { return } - + lastDatesUpdate = Date() - tableView.reloadRows(at: indexPaths, with: .none) + indexPaths.removeAll { $0 == swipedIndex } + tableView.reloadRowsAndPreserveSelection(at: indexPaths) } - + private func updateChats() { guard accountService.account?.address != nil, - accountService.keypair?.privateKey != nil + accountService.keypair?.privateKey != nil else { return } - - self.handleRefresh(self.refreshControl) + Task { + await handleRefresh() + } } - + @MainActor private func handleInitiallySyncedNotification(_ notification: Notification) async { guard let userInfo = notification.userInfo, @@ -396,18 +417,21 @@ final class ChatListViewController: KeyboardObservingViewController { setIsBusy(!synced) return } - + areMessagesLoaded = true + Task { + await updateChatsManuallyMarkedAsUnread() + } performOnMessagesLoadedActions() setIsBusy(!synced) - tableView.reloadData() + tableView.reloadDataPreservingSelection() } - + // MARK: IB Actions @IBAction func newChat(sender: Any) { let controller = screensFactory.makeNewChat() controller.delegate = self - + if let split = splitViewController { let nav = UINavigationController(rootViewController: controller) split.showDetailViewController(nav, sender: self) @@ -415,23 +439,25 @@ final class ChatListViewController: KeyboardObservingViewController { navigationController?.pushViewController(controller, animated: true) } } - + // MARK: Helpers func chatViewController( for chatroom: Chatroom, - with messageId: String? = nil + with messageId: String? = nil, + newChat: Bool = false ) -> ChatViewController { let vc = screensFactory.makeChat() vc.hidesBottomBarWhenPushed = true vc.viewModel.setup( account: accountService.account, chatroom: chatroom, - messageIdToShow: messageId + messageIdToShow: messageId, + isNewChat: newChat ) return vc } - + /// - Parameter provider: nil to drop controllers and reset table @MainActor private func initFetchedRequestControllers(provider: ChatsProvider?) { @@ -442,13 +468,13 @@ final class ChatListViewController: KeyboardObservingViewController { tableView.reloadData() return } - + chatsController = await provider.getChatroomsController() unreadController = await provider.getUnreadMessagesController() - + chatsController?.delegate = self unreadController?.delegate = self - + do { try chatsController?.performFetch() try unreadController?.performFetch() @@ -457,83 +483,72 @@ final class ChatListViewController: KeyboardObservingViewController { unreadController = nil print("There was an error performing fetch: \(error)") } - + tableView.reloadData() - setBadgeValue(unreadController?.fetchedObjects?.count) } } - + @MainActor - @objc private func handleRefresh(_ refreshControl: UIRefreshControl) { - Task { - let result = await chatsProvider.update(notifyState: true) - - guard let result = result else { - refreshControl.endRefreshing() - return - } - - switch result { - case .success: - tableView.reloadData() - - case .failure(let error): - dialogService.showRichError(error: error) - } - - refreshControl.endRefreshing() + private func handleRefresh() async { + guard let result = await chatsProvider.update(notifyState: true) else { return } + + switch result { + case .success: + tableView.reloadDataPreservingSelection() + case .failure(let error): + dialogService.showRichError(error: error) } } - + func setIsBusy(_ busy: Bool, animated: Bool = true) { isBusy = busy - + // MARK: 0. Check if animated. guard animated else { if !busy { busyBackgroundView.isHidden = true return } - + DispatchQueue.onMainAsync { self.busyBackgroundView.isHidden = false self.busyBackgroundView.alpha = 1.0 self.busyIndicatorView.transform = CGAffineTransform(scaleX: 1.0, y: 1.0) } - + return } - + // MARK: 1. Prepare animation and completion let animations: () -> Void = { self.busyBackgroundView.alpha = busy ? 1.0 : 0.0 self.busyIndicatorView.transform = busy ? CGAffineTransform(scaleX: 1.0, y: 1.0) : CGAffineTransform(scaleX: 1.2, y: 1.2) } - + let completion: (Bool) -> Void = { completed in guard completed else { return } - + self.busyBackgroundView.isHidden = !busy } - + // MARK: 2. Initial values let initialValues: () -> Void = { self.busyBackgroundView.alpha = busy ? 0.0 : 1.0 self.busyIndicatorView.transform = busy ? CGAffineTransform(scaleX: 1.2, y: 1.2) : CGAffineTransform(scaleX: 1.0, y: 1.0) - + self.busyBackgroundView.isHidden = false } - + DispatchQueue.onMainAsync { initialValues() UIView.animate(withDuration: 0.2, animations: animations, completion: completion) } } - + // MARK: - Other - + private func setColors() { view.backgroundColor = UIColor.adamant.backgroundColor } @@ -551,35 +566,41 @@ extension ChatListViewController: UITableViewDelegate, UITableViewDataSource { return 0 } } - + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return cellHeight } - + func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { return cellHeight } - + func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { return UIView() } - + + func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) { + chatDeselectedIndex = indexPath + } + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if isBusy, - indexPath.row == lastSystemChatPositionRow, - let cell = tableView.cellForRow(at: indexPath), - cell is SpinnerCell { + indexPath.row == lastSystemChatPositionRow, + let cell = tableView.cellForRow(at: indexPath), + cell is SpinnerCell + { tableView.deselectRow(at: indexPath, animated: true) return } - + let nIndexPath = chatControllerIndexPath(tableViewIndexPath: indexPath) if let chatroom = chatsController?.fetchedObjects?[safe: nIndexPath.row] { let vc = chatViewController(for: chatroom) vc.hidesBottomBarWhenPushed = true - + removeManualAdress(adress: chatroom.partner?.address) + if let split = self.splitViewController { - let chat = UINavigationController(rootViewController:vc) + let chat = UINavigationController(rootViewController: vc) split.showDetailViewController(chat, sender: self) } else if let nav = navigationController { nav.pushViewController(vc, animated: true) @@ -589,11 +610,18 @@ extension ChatListViewController: UITableViewDelegate, UITableViewDataSource { } } } - + func scrollViewDidScroll(_ scrollView: UIScrollView) { let offsetY = scrollView.contentOffset.y + scrollView.safeAreaInsets.top scrollUpButton.isHidden = offsetY < cellHeight * 0.75 } + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + guard scrollView.contentOffset.y <= 0, scrollView.contentOffset.y < -100 else { return } + + Task { + await handleRefresh() + } + } } // MARK: - UITableView Cells @@ -604,9 +632,9 @@ extension ChatListViewController { cell.wrappedView.startAnimating() return cell } - + let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! ChatTableViewCell - + cell.accessoryType = .none cell.accountLabel.textColor = UIColor.adamant.primary cell.dateLabel.textColor = UIColor.adamant.secondary @@ -615,14 +643,15 @@ extension ChatListViewController { cell.badgeColor = UIColor.adamant.primary cell.lastMessageLabel.textColor = UIColor.adamant.primary cell.borderWidth = 1 - + return cell } - + func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { if isBusy, - indexPath.row == lastSystemChatPositionRow, - let cell = cell as? SpinnerCell { + indexPath.row == lastSystemChatPositionRow, + let cell = cell as? SpinnerCell + { configureCell(cell) } else if let cell = cell as? ChatTableViewCell { let nIndexPath = chatControllerIndexPath(tableViewIndexPath: indexPath) @@ -630,7 +659,8 @@ extension ChatListViewController { configureCell(cell, for: chat) } if isBusy, - indexPath.row == (lastSystemChatPositionRow ?? 0) - 1 { + indexPath.row == (lastSystemChatPositionRow ?? 0) - 1 + { cell.separatorInset = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 0) } else { if let defaultSeparatorInstets = defaultSeparatorInstets { @@ -640,29 +670,29 @@ extension ChatListViewController { } } } - + Task { @MainActor in guard let roomsLoadedCount = await chatsProvider.roomsLoadedCount, - let roomsMaxCount = await chatsProvider.roomsMaxCount, - roomsLoadedCount < roomsMaxCount, - roomsMaxCount > 0, - !isBusy, - tableView.numberOfRows(inSection: .zero) - indexPath.row < 3 + let roomsMaxCount = await chatsProvider.roomsMaxCount, + roomsLoadedCount < roomsMaxCount, + roomsMaxCount > 0, + !isBusy, + tableView.numberOfRows(inSection: .zero) - indexPath.row < 3 else { return } - + isBusy = true insertReloadRow() loadNewChats(offset: roomsLoadedCount) } } - + private func configureCell(_ cell: SpinnerCell) { cell.wrappedView.startAnimating() cell.backgroundColor = .clear } - + private func configureCell(_ cell: ChatTableViewCell, for chatroom: Chatroom) { cell.backgroundColor = .clear if let partner = chatroom.partner { @@ -677,7 +707,7 @@ extension ChatListViewController { cell.avatarImage = image } } - + cell.avatarImageView.roundingMode = .round cell.avatarImageView.clipsToBounds = true } else { @@ -686,35 +716,40 @@ extension ChatListViewController { cell.borderWidth = 0 } } - + cell.accountLabel.text = chatroom.getName(addressBookService: addressBook) cell.hasUnreadMessages = chatroom.hasUnreadMessages - - if let lastTransaction = chatroom.lastTransaction { - cell.hasUnreadMessages = lastTransaction.isUnread + if let address = chatroom.partner?.address, + let preservedMessage = shortDescription(for: address) + { + cell.hasUnreadMessages = chatroom.hasUnreadMessages + cell.lastMessageLabel.attributedText = preservedMessage + } else if let lastTransaction = chatroom.lastTransaction { + cell.hasUnreadMessages = chatroom.hasUnreadMessages cell.lastMessageLabel.attributedText = shortDescription(for: lastTransaction) + cell.messageStatus = lastTransaction.statusEnum } else { cell.lastMessageLabel.text = nil } - + if let date = chatroom.updatedAt as Date?, date != .adamantNullDate { cell.dateLabel.text = date.humanizedDay(useTimeFormat: true) } else { cell.dateLabel.text = nil } } - + private func insertReloadRow() { lastSystemChatPositionRow = getBottomSystemChatIndex() - tableView.reloadData() + tableView.reloadDataPreservingSelection() } - + @MainActor private func loadNewChats(offset: Int) { loadNewChatTask = Task { await chatsProvider.getChatRooms(offset: offset) isBusy = false - tableView.reloadData() + tableView.reloadDataPreservingSelection() } } } @@ -728,23 +763,28 @@ extension ChatListViewController: NSFetchedResultsControllerDelegate { updatingIndicatorView.startAnimate() } } - + func controllerDidChangeContent(_ controller: NSFetchedResultsController) { if isBusy { return } switch controller { case let c where c == chatsController: tableView.endUpdates() - updatingIndicatorView.stopAnimate() - + case let c where c == unreadController: setBadgeValue(controller.fetchedObjects?.count) - + default: break } } - - func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { + + func controller( + _ controller: NSFetchedResultsController, + didChange anObject: Any, + at indexPath: IndexPath?, + for type: NSFetchedResultsChangeType, + newIndexPath: IndexPath? + ) { if isBusy { return } switch controller { // MARK: Chats controller @@ -754,19 +794,20 @@ extension ChatListViewController: NSFetchedResultsControllerDelegate { if let newIndexPath = newIndexPath { tableView.insertRows(at: [newIndexPath], with: .automatic) } - + case .delete: if let indexPath = indexPath { tableView.deleteRows(at: [indexPath], with: .automatic) } - + case .update: if let indexPath = indexPath, - let cell = self.tableView.cellForRow(at: indexPath) as? ChatTableViewCell, - let chatroom = anObject as? Chatroom { + let cell = self.tableView.cellForRow(at: indexPath) as? ChatTableViewCell, + let chatroom = anObject as? Chatroom + { configureCell(cell, for: chatroom) } - + case .move: if let indexPath = indexPath, let newIndexPath = newIndexPath { if let cell = tableView.cellForRow(at: indexPath) as? ChatTableViewCell, let chatroom = anObject as? Chatroom { @@ -777,41 +818,43 @@ extension ChatListViewController: NSFetchedResultsControllerDelegate { @unknown default: break } - + // MARK: Unread controller - + case let c where c == unreadController: guard let transaction = anObject as? ChatTransaction else { break } - + if self.view.window == nil, - type == .insert { + type == .insert + { showNotification(for: transaction) } - - let shouldForceUpdate = anObject is TransferTransaction - || anObject is RichMessageTransaction - + + let shouldForceUpdate = + anObject is TransferTransaction + || anObject is RichMessageTransaction + if shouldForceUpdate, type == .insert { transactionsRequiringBalanceUpdate.append(transaction.txId) } - + guard shouldForceUpdate, - let blockId = transaction.blockId, - !blockId.isEmpty, - transactionsRequiringBalanceUpdate.contains(transaction.txId) + let blockId = transaction.blockId, + !blockId.isEmpty, + transactionsRequiringBalanceUpdate.contains(transaction.txId) else { break } - + if let index = transactionsRequiringBalanceUpdate.firstIndex(of: transaction.txId) { transactionsRequiringBalanceUpdate.remove(at: index) } - + NotificationCenter.default.post( name: .AdamantAccountService.forceUpdateBalance, object: nil ) - + default: break } @@ -828,41 +871,41 @@ extension ChatListViewController: NewChatViewControllerDelegate { guard let chatroom = account.chatroom else { fatalError("No chatroom?") } - + if let name = name, - let address = account.address, - addressBook.getName(for: address) == nil { + let address = account.address, + addressBook.getName(for: address) == nil + { account.name = name chatroom.title = name Task { await self.addressBook.set(name: name, for: address) } } - + DispatchQueue.main.async { [self] in - let vc = chatViewController(for: chatroom) - + let vc = chatViewController(for: chatroom, newChat: true) + if let split = splitViewController { let nav = UINavigationController(rootViewController: vc) split.showDetailViewController(nav, sender: self) vc.becomeFirstResponder() - + if let count = vc.viewModel.chatroom?.transactions?.count, count == 0 { vc.messageInputBar.inputTextView.becomeFirstResponder() } } else { navigationController?.setViewControllers([self, vc], animated: true) } - + if let count = vc.viewModel.chatroom?.transactions?.count, count == 0 { vc.messageInputBar.inputTextView.becomeFirstResponder() } - + if let preMessage = preMessage { vc.viewModel.inputText = preMessage } } - // Select row after awhile DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.seconds(1)) { [weak self] in if let indexPath = self?.chatsController?.indexPath(forObject: chatroom) { @@ -880,25 +923,25 @@ extension ChatListViewController { guard await chatsProvider.isInitiallySynced else { return } - + // MARK: 1. Show notification only for incomming transactions guard !transaction.silentNotification, - !transaction.isOutgoing, - let chatroom = transaction.chatroom, - chatroom != presentedChatroom(), - !chatroom.isHidden, - let partner = chatroom.partner, - let address = partner.address + !transaction.isOutgoing, + let chatroom = transaction.chatroom, + chatroom != presentedChatroom(), + !chatroom.isHidden, + let partner = chatroom.partner, + let address = partner.address else { return } - + // MARK: 2. Prepare notification - + let name: String? = partner.name ?? addressBook.getName(for: address) let title = name ?? partner.address let text = shortDescription(for: transaction) - + let image: UIImage if let ava = partner.avatar, let img = UIImage.asset(named: ava) { image = img @@ -907,32 +950,40 @@ extension ChatListViewController { } else { image = defaultAvatar } - + // MARK: 4. Show notification with tap handler dialogService.showNotification( title: title?.checkAndReplaceSystemWallets(), message: text?.string, image: image ) { [weak self] in - self?.presentChatroom(chatroom) + self?.presentChatroom(chatroom, with: self?.messageId(transaction: transaction)) } } } - + + private func messageId(transaction: ChatTransaction) -> String? { + if let richTransaction = transaction as? RichMessageTransaction { + return richTransaction.getRichValue(for: RichContentKeys.react.reactto_id) ?? richTransaction.transactionId + } else { + return transaction.transactionId + } + } + @MainActor - func presentChatroom(_ chatroom: Chatroom, with message: MessageTransaction? = nil) { + func presentChatroom(_ chatroom: Chatroom, with message: String? = nil) { // MARK: 1. Create and config ViewController - let vc = chatViewController(for: chatroom, with: message?.transactionId) - + let vc = chatViewController(for: chatroom, with: message) + if let split = self.splitViewController, UIScreen.main.traitCollection.userInterfaceIdiom == .pad { - let chat = UINavigationController(rootViewController:vc) + let chat = UINavigationController(rootViewController: vc) split.showDetailViewController(chat, sender: self) tabBarController?.selectedIndex = .zero } else { // MARK: 2. Config TabBarController let animated = tabBarController?.selectedIndex == .zero tabBarController?.selectedIndex = .zero - + // MARK: 3. Present ViewController if let nav = navigationController { nav.dismiss(animated: true) @@ -944,7 +995,10 @@ extension ChatListViewController { } } } - + private func presentBuyAndSell() { + let buyAndSellVC = screensFactory.makeBuyAndSell() + navigationController?.pushViewController(buyAndSellVC, animated: true) + } private func shortDescription(for transaction: ChatTransaction) -> NSAttributedString? { switch transaction { case let message as MessageTransaction: @@ -952,80 +1006,88 @@ extension ChatListViewController { return nil } text = MessageProcessHelper.process(text) - + var raw: String if message.isOutgoing { raw = "\(String.adamant.chatList.sentMessagePrefix)\(text)" } else { raw = text } - + var attributedText = markdownParser.parse(raw).resolveLinkColor() attributedText = MessageProcessHelper.process(attributedText: attributedText) - + return attributedText - + case let transfer as TransferTransaction: if let admService = walletServiceCompose.getWallet( by: AdmWalletService.richMessageType )?.core as? AdmWalletService { return markdownParser.parse(admService.shortDescription(for: transfer)) } - + return nil case let richMessage as RichMessageTransaction: if let type = richMessage.richType, - let provider = walletServiceCompose.getWallet(by: type) { + let provider = walletServiceCompose.getWallet(by: type) + { return provider.core.shortDescription(for: richMessage) } - + if richMessage.additionalType == .reply, - let content = richMessage.richContent, - let text = content[RichContentKeys.reply.replyMessage] as? String { + let content = richMessage.richContent, + let text = content[RichContentKeys.reply.replyMessage] as? String + { return getRawReplyPresentation(isOutgoing: richMessage.isOutgoing, text: text) } - + if richMessage.additionalType == .reaction, - let content = richMessage.richContent, - let reaction = content[RichContentKeys.react.react_message] as? String { - let prefix = richMessage.isOutgoing - ? "\(String.adamant.chatList.sentMessagePrefix)" - : "" - - let text = reaction.isEmpty - ? NSMutableAttributedString(string: "\(prefix)\(String.adamant.chatList.removedReaction) \(reaction)") - : NSMutableAttributedString(string: "\(prefix)\(String.adamant.chatList.reacted) \(reaction)") - + let content = richMessage.richContent, + let reaction = content[RichContentKeys.react.react_message] as? String + { + let prefix = + richMessage.isOutgoing + ? "\(String.adamant.chatList.sentMessagePrefix)" + : "" + + let text = + reaction.isEmpty + ? NSMutableAttributedString(string: "\(prefix)\(String.adamant.chatList.removedReaction) \(reaction)") + : NSMutableAttributedString(string: "\(prefix)\(String.adamant.chatList.reacted) \(reaction)") + return text } - + if richMessage.additionalType == .reply, - let content = richMessage.richContent, - richMessage.isFileReply() { + let content = richMessage.richContent, + richMessage.isFileReply() + { let text = FilePresentationHelper.getFilePresentationText(content) return getRawReplyPresentation(isOutgoing: richMessage.isOutgoing, text: text) } - + if richMessage.additionalType == .file, - let content = richMessage.richContent { - let prefix = richMessage.isOutgoing - ? "\(String.adamant.chatList.sentMessagePrefix)" - : "" - + let content = richMessage.richContent + { + let prefix = + richMessage.isOutgoing + ? "\(String.adamant.chatList.sentMessagePrefix)" + : "" + let fileText = FilePresentationHelper.getFilePresentationText(content) - + let attributesText = markdownParser.parse(prefix + fileText).resolveLinkColor() - + return attributesText } - + if let serialized = richMessage.serializedMessage() { return NSAttributedString(string: serialized) } - + return nil - - /* + + /* if richMessage.isOutgoing { let mutable = NSMutableAttributedString(attributedString: description) let prefix = NSAttributedString(string: String.adamant.chatList.sentMessagePrefix) @@ -1035,41 +1097,89 @@ extension ChatListViewController { return description } */ - + default: return nil } } - + private func shortDescription(for address: String) -> NSAttributedString? { + var descriptionParts: [NSAttributedString] = [] + + if chatPreservation.getReplyMessage(address: address) != nil { + let replyImageAttachment = NSTextAttachment() + replyImageAttachment.image = UIImage(systemName: "arrowshape.turn.up.left")?.withTintColor(.adamant.primary) + replyImageAttachment.bounds = CGRect(x: .zero, y: -3, width: 23, height: 20) + + descriptionParts.append(NSAttributedString(attachment: replyImageAttachment)) + } + if let files = chatPreservation.getPreservedFiles(for: address), !files.isEmpty { + let mediaCount = files.count(where: { $0.type.isMedia }) + let otherCount = files.count(where: { !$0.type.isMedia }) + + let fileParts = [ + mediaCount > 0 ? "📸" + (mediaCount >= 2 ? "\(mediaCount)" : "") : nil, + otherCount > 0 ? "📄" + (otherCount >= 2 ? "\(otherCount)" : "") : nil + ].compactMap { $0 } + + if !fileParts.isEmpty { + let parsedFileParts = + fileParts + .map { markdownParser.parse($0).resolveLinkColor() } + .reduce(NSMutableAttributedString()) { result, part in + if !result.string.isEmpty { result.append(NSAttributedString(string: " ")) } + result.append(part) + return result + } + descriptionParts.append(parsedFileParts) + } + } + + if let preservedMessage = chatPreservation.getPreservedMessageFor(address: address) { + let processedMessage = MessageProcessHelper.process(preservedMessage) + descriptionParts.append(NSAttributedString(string: processedMessage)) + } + guard descriptionParts.contains(where: { !$0.string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).isEmpty }) else { + return nil + } + + let result = NSMutableAttributedString(string: "✏️: ") + for (index, part) in descriptionParts.enumerated() { + if index > 0 { result.append(NSAttributedString(string: " ")) } + result.append(part) + } + + return result + } private func getRawReplyPresentation(isOutgoing: Bool, text: String) -> NSMutableAttributedString { - let prefix = isOutgoing - ? "\(String.adamant.chatList.sentMessagePrefix)" - : "" - + let prefix = + isOutgoing + ? "\(String.adamant.chatList.sentMessagePrefix)" + : "" + let replyImageAttachment = NSTextAttachment() - + replyImageAttachment.image = UIImage( systemName: "arrowshape.turn.up.left" )?.withTintColor(.adamant.primary) - + replyImageAttachment.bounds = CGRect( x: .zero, y: -3, width: 23, height: 20 ) - + let extraSpace = isOutgoing ? " " : "" let imageString = NSAttributedString(attachment: replyImageAttachment) - + let markDownText = markdownParser.parse("\(extraSpace)\(text)").resolveLinkColor() - + let fullString = NSMutableAttributedString(string: prefix) if isOutgoing { fullString.append(imageString) } fullString.append(markDownText) - + return MessageProcessHelper.process(attributedText: fullString) } } @@ -1080,39 +1190,52 @@ extension ChatListViewController { _ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath ) -> UISwipeActionsConfiguration? { + swipedIndex = indexPath guard let chatroom = chatsController?.fetchedObjects?[safe: indexPath.row] else { return nil } - + var actions: [UIContextualAction] = [] - + // More let more = makeMooreContextualAction(for: chatroom) actions.append(more) - + // Block let block = makeBlockContextualAction(for: chatroom) actions.append(block) - + return UISwipeActionsConfiguration(actions: actions) } - + func tableView( _ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath ) -> UISwipeActionsConfiguration? { + swipedIndex = indexPath guard let chatroom = chatsController?.fetchedObjects?[safe: indexPath.row] else { return nil } - + var actions: [UIContextualAction] = [] - - let markAsRead = makeMarkAsReadContextualAction(for: chatroom) - actions.append(markAsRead) - + + if let adress = chatroom.partner?.address { + let markAsRead = makeMarkAsReadContextualAction(for: chatroom, adress: adress) + actions.append(markAsRead) + } + return UISwipeActionsConfiguration(actions: actions) } - + + func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?) { + swipedIndex = nil + guard let deselectedIndex = indexPath, + let cell = tableView.cellForRow(at: deselectedIndex) as? ChatTableViewCell, + let chatroom = chatsController?.fetchedObjects?[safe: deselectedIndex.row] + else { return } + configureCell(cell, for: chatroom) + } + private func blockChat(with address: String, for chatroom: Chatroom?) { Task { chatroom?.isHidden = true @@ -1120,7 +1243,7 @@ extension ChatListViewController { await chatsProvider.blockChat(with: address) } } - + private func makeBlockContextualAction(for chatroom: Chatroom) -> UIContextualAction { let block = UIContextualAction( style: .destructive, @@ -1130,7 +1253,7 @@ extension ChatListViewController { completionHandler(false) return } - + self?.dialogService.showAlert( title: String.adamant.chatList.blockUser, message: nil, @@ -1153,22 +1276,30 @@ extension ChatListViewController { from: nil ) } - + block.image = .asset(named: "swipe_block")?.withTintColor(.adamant.warning, renderingMode: .alwaysOriginal) block.backgroundColor = .adamant.swipeBlockColor - + return block } - - private func makeMarkAsReadContextualAction(for chatroom: Chatroom) -> UIContextualAction { + + private func makeMarkAsReadContextualAction(for chatroom: Chatroom, adress: String) -> UIContextualAction { let markAsRead = UIContextualAction( style: .normal, title: "👀" ) { (_, _, completionHandler) in - if chatroom.hasUnread { + if chatroom.hasUnreadMessages { chatroom.markAsReaded() + self.chatsManuallyMarkedAsUnread.remove(adress) + Task { + await self.chatsProvider.removeManualMarkChatAsUnread(chatroomId: adress) + } } else { chatroom.markAsUnread() + self.chatsManuallyMarkedAsUnread.insert(adress) + Task { + await self.chatsProvider.setManualMarkChatAsUnread(chatroomId: adress) + } } try? chatroom.managedObjectContext?.save() completionHandler(true) @@ -1177,15 +1308,15 @@ extension ChatListViewController { markAsRead.backgroundColor = UIColor.adamant.contextMenuDefaultBackgroundColor return markAsRead } - + private func makeMooreContextualAction(for chatroom: Chatroom) -> UIContextualAction { let more = UIContextualAction( style: .normal, title: nil ) { [weak self] (_, view, completionHandler) in guard let self = self, - let partner = chatroom.partner, - let address = partner.address + let partner = chatroom.partner, + let address = partner.address else { completionHandler(false) return @@ -1204,43 +1335,43 @@ extension ChatListViewController { params: params ) ) - + guard !partner.isSystem else { self.dialogService.presentShareAlertFor( string: address, types: [ .copyToPasteboard, .share, - .generateQr( - encodedContent: encodedAddress, - sharingTip: address, - withLogo: true - ) + .generateQr( + encodedContent: encodedAddress, + sharingTip: address, + withLogo: true + ) ], excludedActivityTypes: ShareContentType.address.excludedActivityTypes, animated: true, from: view, completion: nil ) - + completionHandler(true) return } - + let closeAction: (() -> Void)? = { [completionHandler] in completionHandler(true) } - + let share = self.makeShareAction( for: address, encodedAddress: encodedAddress, sender: view, completion: closeAction ) - + let rename = self.makeRenameAction(for: address, completion: closeAction) let cancel = self.makeCancelAction(completion: closeAction) - + self.dialogService.showAlert( title: nil, message: nil, @@ -1249,12 +1380,12 @@ extension ChatListViewController { from: .view(view) ) } - + more.image = .asset(named: "swipe_more") more.backgroundColor = .adamant.swipeMoreColor return more } - + private func makeShareAction( for address: String, encodedAddress: String, @@ -1266,7 +1397,7 @@ extension ChatListViewController { style: .default ) { [weak self] _ in guard let self = self else { return } - + self.dialogService.presentShareAlertFor( string: address, types: [ @@ -1285,7 +1416,7 @@ extension ChatListViewController { ) } } - + private func makeRenameAction( for address: String, completion: (() -> Void)? = nil @@ -1294,48 +1425,29 @@ extension ChatListViewController { title: .adamant.chat.rename, style: .default ) { [weak self] _ in - guard let alert = self?.makeRenameAlert(for: address) else { return } - self?.dialogService.present(alert, animated: true) { + guard let self = self else { return } + + let alert = dialogService.makeRenameAlert( + titleFormat: String(format: .adamant.chat.actionsBody, address), + initialText: self.addressBook.getName(for: address), + isEnoughMoney: accountService.account?.isEnoughMoneyForTransaction ?? false, + url: accountService.account?.address, + showVC: { [weak self] in + self?.presentBuyAndSell() + } + ) { [weak self] newName in + Task { + await self?.addressBook.set(name: newName, for: address) + } + } + + dialogService.present(alert, animated: true) { [weak self] in self?.dialogService.selectAllTextFields(in: alert) completion?() } } } - - private func makeRenameAlert(for address: String) -> UIAlertController? { - let alert = UIAlertController( - title: .init(format: .adamant.chat.actionsBody, address), - message: nil, - preferredStyleSafe: .alert, - source: nil - ) - - alert.addTextField { [weak self] textField in - textField.placeholder = .adamant.chat.name - textField.autocapitalizationType = .words - textField.text = self?.addressBook.getName(for: address) - } - - let renameAction = UIAlertAction( - title: .adamant.chat.rename, - style: .default - ) { [weak self] _ in - guard - let textField = alert.textFields?.first, - let newName = textField.text - else { return } - - Task { - await self?.addressBook.set(name: newName, for: address) - } - } - - alert.addAction(renameAction) - alert.addAction(makeCancelAction()) - alert.modalPresentationStyle = .overFullScreen - return alert - } - + private func makeCancelAction(completion: (() -> Void)? = nil) -> UIAlertAction { .init( title: .adamant.alert.cancel, @@ -1344,6 +1456,12 @@ extension ChatListViewController { completion?() } } + + private func removeManualAdress(adress: String?) { + if let adress { + chatsManuallyMarkedAsUnread.remove(adress) + } + } } // MARK: - Tools @@ -1356,33 +1474,35 @@ extension ChatListViewController { } else { item = tabBarItem } - - if let value = value, value > 0 { - item.badgeValue = String(value) - notificationsService.setBadge(number: value) + + let adjustedValue = (value ?? 0) + chatsManuallyMarkedAsUnread.count + + if adjustedValue > 0 { + item.badgeValue = String(adjustedValue) + notificationsService.setBadge(number: adjustedValue) } else { item.badgeValue = nil notificationsService.setBadge(number: nil) } } - + /// Current chat func presentedChatroom() -> Chatroom? { guard let vc = navigationController?.visibleViewController as? ChatViewController else { return nil } - + return vc.viewModel.chatroom } - + /// First system botoom chat index func getBottomSystemChatIndex() -> Int { var index = 0 try? chatsController?.performFetch() chatsController?.fetchedObjects?.enumerated().forEach({ (i, room) in guard index == 0, - let date = room.updatedAt as? Date, - date == .adamantNullDate + let date = room.updatedAt as? Date, + date == .adamantNullDate else { return } @@ -1391,38 +1511,38 @@ extension ChatListViewController { return index } - + func selectChatroomRow(chatroom: Chatroom) { guard let chatsControllerIndexPath = chatsController?.indexPath(forObject: chatroom) else { return } let tableViewIndexPath = tableViewIndexPath(chatControllerIndexPath: chatsControllerIndexPath) tableView.selectRow(at: tableViewIndexPath, animated: true, scrollPosition: .none) tableView.scrollToRow(at: tableViewIndexPath, at: .top, animated: true) } - + func performOnMessagesLoaded(action: @escaping () -> Void) { onMessagesLoadedActions.append(action) - + guard areMessagesLoaded else { return } performOnMessagesLoadedActions() } - + private func chatControllerIndexPath(tableViewIndexPath: IndexPath) -> IndexPath { isBusy && tableViewIndexPath.row >= (lastSystemChatPositionRow ?? 0) ? IndexPath(row: tableViewIndexPath.row - 1, section: 0) : tableViewIndexPath } - + private func tableViewIndexPath(chatControllerIndexPath: IndexPath) -> IndexPath { isBusy && chatControllerIndexPath.row == (lastSystemChatPositionRow ?? 0) ? IndexPath(row: chatControllerIndexPath.row + 1, section: 0) : chatControllerIndexPath } - + private func performOnMessagesLoadedActions() { onMessagesLoadedActions.forEach { $0() } onMessagesLoadedActions = [] } - + @objc private func showDefaultScreen() { splitViewController?.showDetailViewController(WelcomeViewController(), sender: self) } @@ -1435,77 +1555,77 @@ extension ChatListViewController: UISearchBarDelegate, UISearchResultsUpdating, func beginSearch() { searchController?.searchBar.becomeFirstResponder() } - + @MainActor func updateSearchResults(for searchController: UISearchController) { guard let vc = searchController.searchResultsController as? SearchResultsViewController, let searchString = searchController.searchBar.text else { return } - + let contacts = chatsController?.fetchedObjects?.filter { (chatroom) -> Bool in guard let partner = chatroom.partner, !partner.isSystem else { return false } - + if let address = partner.address { if let name = self.addressBook.getName(for: address) { return name.localizedCaseInsensitiveContains(searchString) || address.localizedCaseInsensitiveContains(searchString) } return address.localizedCaseInsensitiveContains(searchString) } - + return false } - + Task { let messages = await chatsProvider.getMessages(containing: searchString, in: nil) - + vc.updateResult(contacts: contacts, messages: messages, searchText: searchString) } } - + func didSelected(_ message: MessageTransaction) { guard let chatroom = message.chatroom else { dialogService.showError(withMessage: "Error getting chatroom in SearchController result. Please, report an error", supportEmail: true, error: nil) searchController?.dismiss(animated: true, completion: nil) return } - + searchController?.dismiss(animated: true) { [weak self] in guard let presenter = self, let tableView = presenter.tableView else { return } - + if let indexPath = tableView.indexPathForSelectedRow { tableView.deselectRow(at: indexPath, animated: true) } - + if let indexPath = self?.chatsController?.indexPath(forObject: chatroom) { tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none) } - - presenter.presentChatroom(chatroom, with: message) + + presenter.presentChatroom(chatroom, with: message.transactionId) } } - + func didSelected(_ chatroom: Chatroom) { searchController?.dismiss(animated: true) { [weak self] in guard let presenter = self, let tableView = presenter.tableView else { return } - + if let indexPath = tableView.indexPathForSelectedRow { tableView.deselectRow(at: indexPath, animated: true) } - + if let indexPath = self?.chatsController?.indexPath(forObject: chatroom) { tableView.selectRow(at: indexPath, animated: true, scrollPosition: .none) } - + presenter.presentChatroom(chatroom) } } - + func didSelected(_ account: CoreDataAccount) { account.chatroom?.isForcedVisible = true newChatController(didSelectAccount: account, preMessage: nil, name: nil) @@ -1528,11 +1648,18 @@ extension ChatListViewController { } } -private extension State { - var isUpdating: Bool { - switch self { - case .updating: true - case .failedToUpdate, .upToDate, .empty: false +extension UITableView { + fileprivate func reloadRowsAndPreserveSelection(at indexPaths: [IndexPath]) { + let selectedRowIndexPath = indexPathForSelectedRow + reloadRows(at: indexPaths, with: .none) + selectRow(at: selectedRowIndexPath, animated: false, scrollPosition: .none) + } + + fileprivate func reloadDataPreservingSelection() { + let selectedRow = indexPathForSelectedRow + reloadData() + if let selectedRow = selectedRow { + selectRow(at: selectedRow, animated: false, scrollPosition: .none) } } } diff --git a/Adamant/Modules/ChatsList/ChatTableViewCell.swift b/Adamant/Modules/ChatsList/ChatTableViewCell.swift index fd1fd98bb..b91f574ca 100644 --- a/Adamant/Modules/ChatsList/ChatTableViewCell.swift +++ b/Adamant/Modules/ChatsList/ChatTableViewCell.swift @@ -6,26 +6,29 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import FreakingSimpleRoundImageView import CommonKit +import FreakingSimpleRoundImageView +import UIKit final class ChatTableViewCell: UITableViewCell { - static var defaultAvatar: UIImage = .asset(named: "avatar-chat-placeholder") ?? .init() static let shortDescriptionTextSize: CGFloat = 15.0 - + // MARK: - IBOutlets @IBOutlet weak var avatarImageView: RoundImageView! @IBOutlet weak var accountLabel: UILabel! @IBOutlet weak var lastMessageLabel: UILabel! @IBOutlet weak var dateLabel: UILabel! @IBOutlet weak var badgeView: UIView! - + @IBOutlet weak var clockView: UIImageView! + @IBOutlet weak var lastMessageLeadingAnchor: NSLayoutConstraint! + @IBOutlet weak var macOsImage: UIImageView! + override func awakeFromNib() { - Task { @MainActor in badgeView.layer.cornerRadius = badgeView.bounds.height / 2 } + badgeView.layer.cornerRadius = badgeView.bounds.height / 2 + clockView.contentMode = .scaleAspectFit } - + var avatarImage: UIImage? { get { return avatarImageView.image @@ -38,7 +41,7 @@ final class ChatTableViewCell: UITableViewCell { } } } - + var borderWidth: CGFloat { get { return avatarImageView.borderWidth @@ -47,7 +50,7 @@ final class ChatTableViewCell: UITableViewCell { avatarImageView.borderWidth = newValue } } - + var borderColor: UIColor? { get { return avatarImageView.borderColor @@ -56,13 +59,13 @@ final class ChatTableViewCell: UITableViewCell { avatarImageView.borderColor = newValue } } - + var hasUnreadMessages: Bool = false { didSet { badgeView.isHidden = !hasUnreadMessages } } - + var badgeColor: UIColor? { get { return badgeView.backgroundColor @@ -71,4 +74,45 @@ final class ChatTableViewCell: UITableViewCell { badgeView.backgroundColor = newValue } } + + var messageStatus: MessageStatus = .delivered { + didSet { + let isPhone = UIDevice.current.userInterfaceIdiom == .phone + + switch messageStatus { + case .pending: + if isPhone { + clockView.isHidden = false + clockView.image = .asset(named: "status_pending") + clockView.tintColor = .adamant.secondary + macOsImage.isHidden = true + lastMessageLeadingAnchor.constant = 27 + } else { + macOsImage.isHidden = false + macOsImage.image = .asset(named: "status_pending") + macOsImage.tintColor = .adamant.secondary + clockView.isHidden = true + } + + case .failed: + if isPhone { + clockView.isHidden = false + clockView.image = .asset(named: "status_failed") + clockView.tintColor = .adamant.attention + macOsImage.isHidden = true + lastMessageLeadingAnchor.constant = 27 + } else { + macOsImage.isHidden = false + macOsImage.image = .asset(named: "status_failed") + macOsImage.tintColor = .adamant.attention + clockView.isHidden = true + } + + case .delivered: + clockView.isHidden = true + macOsImage.isHidden = true + lastMessageLeadingAnchor.constant = 10 + } + } + } } diff --git a/Adamant/Modules/ChatsList/ChatTableViewCell.xib b/Adamant/Modules/ChatsList/ChatTableViewCell.xib index e37ae780b..39e36ae69 100644 --- a/Adamant/Modules/ChatsList/ChatTableViewCell.xib +++ b/Adamant/Modules/ChatsList/ChatTableViewCell.xib @@ -1,9 +1,9 @@ - + - + @@ -51,6 +51,20 @@ + + + + + + + + + + + + + + @@ -58,10 +72,14 @@ - + + + + + @@ -74,17 +92,15 @@ + + + - - - - - diff --git a/Adamant/Modules/ChatsList/ComplexTransferViewController.swift b/Adamant/Modules/ChatsList/ComplexTransferViewController.swift index fc5c6b78b..60f974088 100644 --- a/Adamant/Modules/ChatsList/ComplexTransferViewController.swift +++ b/Adamant/Modules/ChatsList/ComplexTransferViewController.swift @@ -6,28 +6,32 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit +import CommonKit @preconcurrency import Parchment import SnapKit -import CommonKit +import UIKit @MainActor protocol ComplexTransferViewControllerDelegate: AnyObject { - func complexTransferViewController(_ viewController: ComplexTransferViewController, didFinishWithTransfer: TransactionDetails?, detailsViewController: UIViewController?) + func complexTransferViewController( + _ viewController: ComplexTransferViewController, + didFinishWithTransfer: TransactionDetails?, + detailsViewController: UIViewController? + ) } final class ComplexTransferViewController: UIViewController { // MARK: - Dependencies - - private let visibleWalletsService: VisibleWalletsService + + private let walletsStoreService: WalletStoreServiceProtocol private let addressBookService: AddressBookService private let screensFactory: ScreensFactory private let walletServiceCompose: WalletServiceCompose private let nodesStorage: NodesStorageProtocol - + // MARK: - Properties var pagingViewController: PagingViewController! - + weak var transferDelegate: ComplexTransferViewControllerDelegate? var services: [WalletService] = [] var partner: CoreDataAccount? { @@ -36,76 +40,76 @@ final class ComplexTransferViewController: UIViewController { } } var replyToMessageId: String? - + // MARK: Init - + init( - visibleWalletsService: VisibleWalletsService, + walletsStoreService: WalletStoreServiceProtocol, addressBookService: AddressBookService, screensFactory: ScreensFactory, walletServiceCompose: WalletServiceCompose, nodesStorage: NodesStorageProtocol ) { - self.visibleWalletsService = visibleWalletsService + self.walletsStoreService = walletsStoreService self.addressBookService = addressBookService self.screensFactory = screensFactory self.walletServiceCompose = walletServiceCompose self.nodesStorage = nodesStorage - + super.init(nibName: nil, bundle: nil) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func viewDidLoad() { super.viewDidLoad() navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancel)) - + // MARK: Services setupServices() - + // MARK: PagingViewController pagingViewController = PagingViewController() - pagingViewController.register(UINib(nibName: "WalletCollectionViewCell", bundle: nil), for: WalletItemModel.self) + pagingViewController.register(UINib(nibName: "WalletCollectionViewCell", bundle: nil), for: WalletCollectionViewCell.Model.self) pagingViewController.menuItemSize = .fixed(width: 110, height: 114) pagingViewController.indicatorColor = UIColor.adamant.primary pagingViewController.indicatorOptions = .visible(height: 2, zIndex: Int.max, spacing: UIEdgeInsets.zero, insets: UIEdgeInsets.zero) - + pagingViewController.dataSource = self pagingViewController.select(index: 0) - + pagingViewController.borderColor = UIColor.clear - + view.addSubview(pagingViewController.view) pagingViewController.view.snp.makeConstraints { $0.directionalEdges.equalTo(view.safeAreaLayoutGuide) } - + addChild(pagingViewController) - + setColors() } - + deinit { NotificationCenter.default.removeObserver(self) } - + // MARK: - Other - + private func setupServices() { services.removeAll() - let availableServices: [WalletService] = visibleWalletsService.sorted(includeInvisible: false) + let availableServices: [WalletService] = walletsStoreService.sorted(includeInvisible: false) services = availableServices } - + func setColors() { view.backgroundColor = UIColor.adamant.backgroundColor pagingViewController.backgroundColor = UIColor.adamant.backgroundColor pagingViewController.menuBackgroundColor = UIColor.adamant.backgroundColor } - + @objc func cancel() { transferDelegate?.complexTransferViewController(self, didFinishWithTransfer: nil, detailsViewController: nil) } @@ -114,35 +118,35 @@ final class ComplexTransferViewController: UIViewController { extension ComplexTransferViewController: PagingViewControllerDataSource { nonisolated func numberOfViewControllers(in pagingViewController: PagingViewController) -> Int { MainActor.assertIsolated() - + return DispatchQueue.onMainThreadSyncSafe { services.count } } - + nonisolated func pagingViewController( _ pagingViewController: PagingViewController, viewControllerAt index: Int ) -> UIViewController { MainActor.assertIsolated() - + return DispatchQueue.onMainThreadSyncSafe { let service = services[index] let admService = services.first { $0.core.nodeGroups.contains(.adm) } let vc = screensFactory.makeTransferVC(service: service) - + vc.delegate = self - + guard let address = partner?.address else { return vc } - + let name = partner?.chatroom?.getName(addressBookService: addressBookService) - + vc.replyToMessageId = replyToMessageId vc.admReportRecipient = address vc.recipientIsReadonly = true vc.commentsEnabled = service.core.commentsEnabledForRichMessages && partner?.isDummy != true vc.showProgressView(animated: false) - + Task { guard service.core.hasEnabledNode else { vc.showAlertView( @@ -153,7 +157,7 @@ extension ComplexTransferViewController: PagingViewControllerDataSource { ) return } - + guard admService?.core.hasEnabledNode ?? false else { vc.showAlertView( message: .adamant.sharedErrors.admNodeErrorMessage(service.core.tokenSymbol), @@ -161,7 +165,7 @@ extension ComplexTransferViewController: PagingViewControllerDataSource { ) return } - + do { let walletAddress = try await service.core .getWalletAddress( @@ -171,7 +175,7 @@ extension ComplexTransferViewController: PagingViewControllerDataSource { vc.recipientAddress = walletAddress vc.recipientName = name vc.hideProgress(animated: true) - + if ERC20Token.supportedTokens.contains( where: { token in return token.symbol == service.core.tokenSymbol @@ -180,7 +184,7 @@ extension ComplexTransferViewController: PagingViewControllerDataSource { let ethWallet = walletServiceCompose.getWallet( by: EthWalletService.richMessageType )?.core - + vc.rootCoinBalance = ethWallet?.wallet?.balance } } catch let error as WalletServiceError { @@ -195,44 +199,69 @@ extension ComplexTransferViewController: PagingViewControllerDataSource { ) } } - + return vc } - } - + } + nonisolated func pagingViewController(_: PagingViewController, pagingItemAt index: Int) -> PagingItem { MainActor.assertIsolated() - + return DispatchQueue.onMainThreadSyncSafe { let service = services[index].core - + guard let wallet = service.wallet else { - return WalletItemModel(model: .default) + return WalletCollectionViewCell.Model.default } - + var network: String? if ERC20Token.supportedTokens.contains(where: { token in return token.symbol == service.tokenSymbol }) { network = type(of: service).tokenNetworkSymbol } - - let item = WalletItem( + + let item = WalletCollectionViewCell.Model( index: index, + coinID: service.tokenUniqueID, currencySymbol: service.tokenSymbol, currencyImage: service.tokenLogo, - isBalanceInitialized: wallet.isBalanceInitialized, currencyNetwork: network ?? type(of: service).tokenNetworkSymbol, - balance: wallet.balance + isBalanceInitialized: wallet.isBalanceInitialized, + balance: wallet.balance, + notificationBadgeCount: 0 ) - - return WalletItemModel(model: item) + + return item } - } + } } extension ComplexTransferViewController: TransferViewControllerDelegate { - func transferViewController(_ viewController: TransferViewControllerBase, didFinishWithTransfer transfer: TransactionDetails?, detailsViewController: UIViewController?) { + func transferViewController( + _ viewController: TransferViewControllerBase, + didFinishWithTransfer transfer: TransactionDetails?, + detailsViewController: UIViewController? + ) { transferDelegate?.complexTransferViewController(self, didFinishWithTransfer: transfer, detailsViewController: detailsViewController) } } + +// MARK: - Hardware keyboard handling + +extension ComplexTransferViewController { + override var canBecomeFirstResponder: Bool { + return true + } + + override func pressesBegan(_ presses: Set, with event: UIPressesEvent?) { + for press in presses { + guard let key = press.key else { continue } + if key.keyCode == UIKeyboardHIDUsage.keyboardEscape { + return cancel() + } + } + + super.pressesBegan(presses, with: event) + } +} diff --git a/Adamant/Modules/ChatsList/NewChatViewController.swift b/Adamant/Modules/ChatsList/NewChatViewController.swift index 4f8afc5e8..a74eef3c7 100644 --- a/Adamant/Modules/ChatsList/NewChatViewController.swift +++ b/Adamant/Modules/ChatsList/NewChatViewController.swift @@ -6,14 +6,14 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import Eureka -@preconcurrency import QRCodeReader -import EFQRCode import AVFoundation +import CommonKit +import EFQRCode +import Eureka import Photos +@preconcurrency import QRCodeReader import SafariServices -import CommonKit +import UIKit // MARK: - Localization extension String.adamant { @@ -22,7 +22,10 @@ extension String.adamant { String.localized("NewChatScene.Title", comment: "New chat: scene title") } static var addressPlaceholder: String { - String.localized("NewChatScene.Address.Placeholder", comment: "New chat: Recipient address placeholder. Note that address text field always shows U letter, so you can left this line blank.") + String.localized( + "NewChatScene.Address.Placeholder", + comment: "New chat: Recipient address placeholder. Note that address text field always shows U letter, so you can left this line blank." + ) } static var specifyValidAddressMessage: String { String.localized("NewChatScene.Error.InvalidAddress", comment: "New chat: Notify user that he did enter invalid address") @@ -34,7 +37,10 @@ extension String.adamant { String.localized("NewChatScene.Error.WrongQr", comment: "New Chat: Notify user that scanned QR doesn't contains an address") } static var whatDoesItMean: String { - String.localized("NewChatScene.NotInitialized.HelpButton", comment: "New Chat: 'What does it mean?', a help button for info about uninitialized accounts.") + String.localized( + "NewChatScene.NotInitialized.HelpButton", + comment: "New Chat: 'What does it mean?', a help button for info about uninitialized accounts." + ) } } } @@ -52,25 +58,25 @@ protocol NewChatViewControllerDelegate: AnyObject { // MARK: - final class NewChatViewController: FormViewController { static let faqUrl = "https://medium.com/adamant-im/chats-and-uninitialized-accounts-in-adamant-5035438e2fcd" - + private enum Rows { case addressField case scanQr case myQr - + var tag: String { switch self { case .addressField: return "a" - + case .scanQr: return "b" - + case .myQr: return "m" } } - + var localized: String? { switch self { case .addressField: return nil @@ -79,119 +85,122 @@ final class NewChatViewController: FormViewController { } } } - + // MARK: Dependencies var dialogService: DialogService! var accountService: AccountService! var accountsProvider: AccountsProvider! var screensFactory: ScreensFactory! - + // MARK: Properties private var skipValueChange = false - + weak var delegate: NewChatViewControllerDelegate? var addressFormatter = NumberFormatter() static let invalidCharacters = CharacterSet.decimalDigits.inverted - + lazy var qrReader: QRCodeReaderViewController = { let builder = QRCodeReaderViewControllerBuilder { - $0.reader = QRCodeReader(metadataObjectTypes: [.qr ], captureDevicePosition: .back) + $0.reader = QRCodeReader(metadataObjectTypes: [.qr], captureDevicePosition: .back) $0.cancelButtonTitle = String.adamant.alert.cancel $0.showSwitchCameraButton = false } - + let vc = QRCodeReaderViewController(builder: builder) vc.delegate = self return vc }() - + // MARK: - Lifecycle - + override func viewDidLoad() { super.viewDidLoad() - + navigationItem.largeTitleDisplayMode = .never tableView.keyboardDismissMode = .none - + navigationItem.title = String.adamant.newChat.title let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(done)) doneButton.isEnabled = false navigationItem.rightBarButtonItem = doneButton - + navigationOptions = .Disabled - - form +++ Section { - $0.footer = { [weak self] in - var footer = HeaderFooterView(.callback { - let view = ButtonsStripeView.adamantConfigured() - view.stripe = [.qrCameraReader, .qrPhotoReader] - view.delegate = self - return view - }) - - footer.height = { ButtonsStripeView.adamantDefaultHeight } - - return footer - }() - } - - <<< TextRow { - $0.tag = Rows.addressField.tag - $0.cell.textField.placeholder = String.adamant.newChat.addressPlaceholder - $0.cell.textField.setPopupKeyboardType(.numberPad) - - let prefix = UILabel() - prefix.text = "U" - prefix.sizeToFit() - - let view = UIView() - view.addSubview(prefix) - view.frame = prefix.frame - $0.cell.textField.leftView = view - $0.cell.textField.leftViewMode = .always - }.cellUpdate { (cell, _) in - if let text = cell.textField.text { - cell.textField.text = text.components(separatedBy: NewChatViewController.invalidCharacters).joined() - } - }.onChange { [weak self] row in - if let skip = self?.skipValueChange, skip { - self?.skipValueChange = false - return + + form + +++ Section { + $0.footer = { [weak self] in + var footer = HeaderFooterView( + .callback { + let view = ButtonsStripeView.adamantConfigured() + view.stripe = [.qrCameraReader, .qrPhotoReader] + view.delegate = self + return view + } + ) + + footer.height = { ButtonsStripeView.adamantDefaultHeight } + + return footer + }() } - - if let text = row.value { - var trimmed = "" - if let admAddress = text.getAdamantAddress() { - trimmed = admAddress.address.components(separatedBy: AdmTransferViewController.invalidCharactersSet).joined() - } else if let admAddress = text.getLegacyAdamantAddress() { - trimmed = admAddress.address.components(separatedBy: AdmTransferViewController.invalidCharactersSet).joined() - } else { - trimmed = text.components(separatedBy: AdmTransferViewController.invalidCharactersSet).joined() + + <<< TextRow { + $0.tag = Rows.addressField.tag + $0.cell.textField.placeholder = String.adamant.newChat.addressPlaceholder + $0.cell.textField.setPopupKeyboardType(.numberPad) + + let prefix = UILabel() + prefix.text = "U" + prefix.sizeToFit() + + let view = UIView() + view.addSubview(prefix) + view.frame = prefix.frame + $0.cell.textField.leftView = view + $0.cell.textField.leftViewMode = .always + }.cellUpdate { (cell, _) in + if let text = cell.textField.text { + cell.textField.text = text.components(separatedBy: NewChatViewController.invalidCharacters).joined() } - - if text != trimmed { - self?.skipValueChange = true - - DispatchQueue.main.async { - row.value = trimmed - row.updateCell() - } + }.onChange { [weak self] row in + if let skip = self?.skipValueChange, skip { + self?.skipValueChange = false + return } - - if let done = self?.navigationItem.rightBarButtonItem { - DispatchQueue.onMainAsync { - done.isEnabled = text.count > 6 + + if let text = row.value { + var trimmed = "" + if let admAddress = text.getAdamantAddress() { + trimmed = admAddress.address.components(separatedBy: AdmTransferViewController.invalidCharactersSet).joined() + } else if let admAddress = text.getLegacyAdamantAddress() { + trimmed = admAddress.address.components(separatedBy: AdmTransferViewController.invalidCharactersSet).joined() + } else { + trimmed = text.components(separatedBy: AdmTransferViewController.invalidCharactersSet).joined() } + + if text != trimmed { + self?.skipValueChange = true + + DispatchQueue.main.async { + row.value = trimmed + row.updateCell() + } + } + + if let done = self?.navigationItem.rightBarButtonItem { + DispatchQueue.onMainAsync { + done.isEnabled = text.count > 6 + } + } + } else { + self?.navigationItem.rightBarButtonItem?.isEnabled = false } - } else { - self?.navigationItem.rightBarButtonItem?.isEnabled = false } - } - + // MARK: My qr if let address = accountService.account?.address { let myQrSection = Section() - + let button = ButtonRow { $0.tag = Rows.myQr.tag $0.title = Rows.myQr.localized @@ -199,11 +208,13 @@ final class NewChatViewController: FormViewController { cell.textLabel?.textColor = UIColor.adamant.primary }.onCellSelection { [weak self] (_, _) in guard let self = self else { return } - let encodedAddress = AdamantUriTools.encode(request: AdamantUri.address( - address: address, - params: nil - )) - + let encodedAddress = AdamantUriTools.encode( + request: AdamantUri.address( + address: address, + params: nil + ) + ) + switch AdamantQRTools.generateQrFrom(string: encodedAddress, withLogo: true) { case .success(let qr): let vc = screensFactory.makeShareQr() @@ -212,8 +223,8 @@ final class NewChatViewController: FormViewController { vc.excludedActivityTypes = ShareContentType.address.excludedActivityTypes vc.modalPresentationStyle = .overFullScreen present(vc, animated: true, completion: nil) - - case .failure(error: let error): + + case .failure(let error): dialogService.showError( withMessage: error.localizedDescription, supportEmail: true, @@ -221,106 +232,107 @@ final class NewChatViewController: FormViewController { ) } } - + myQrSection.append(button) form.append(myQrSection) } - + setColors() } - + override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - + if let row: TextRow = form.rowBy(tag: Rows.addressField.tag) { row.cell.textField.resignFirstResponder() } } - + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - + if let row: TextRow = form.rowBy(tag: Rows.addressField.tag) { row.cell.textField.becomeFirstResponder() } } - + // MARK: - IBActions - - @IBAction func done(_ sender: Any) { + + @IBAction func done() { guard let row: TextRow = form.rowBy(tag: Rows.addressField.tag), let nums = row.value, nums.count > 0 else { dialogService.showToastMessage(String.adamant.newChat.specifyValidAddressMessage) return } - + var address = nums.uppercased() if !address.starts(with: "U") { address = "U\(address)" } - + startNewChat(with: address, message: nil) } - + // MARK: - Other - + func setColors() { view.backgroundColor = UIColor.adamant.secondBackgroundColor tableView.backgroundColor = .clear } - + @MainActor func startNewChat(with address: String, name: String? = nil, message: String?) { switch AdamantUtilities.validateAdamantAddress(address: address) { case .valid: break - + case .system, .invalid: dialogService.showToastMessage(String.adamant.newChat.specifyValidAddressMessage) return } - + if let loggedAccount = accountService.account, loggedAccount.address == address { dialogService.showToastMessage(String.adamant.newChat.loggedUserAddressMessage) return } - + dialogService.showProgress(withMessage: nil, userInteractionEnable: false) - + Task { do { let account = try await accountsProvider.getAccount(byAddress: address) account.chatroom?.isForcedVisible = true - + self.delegate?.newChatController( didSelectAccount: account, preMessage: message, name: name ) - + self.dialogService.dismissProgress() } catch let error as AccountsProviderError { switch error { case .dummy, .notFound, .notInitiated: self.dialogService.dismissProgress() - + dialogService.presentDummyChatAlert( for: address, from: nil, canSend: false, sendCompletion: nil ) - + case .invalidAddress, .networkError: self.dialogService.showWarning(withMessage: error.localized) - + case .serverError(let apiError): if let apiError = apiError as? ApiServiceError, - case .internalError(let message, _) = apiError, - message == String.adamant.sharedErrors.unknownError { + case .internalError(let message, _) = apiError, + message == String.adamant.sharedErrors.unknownError + { self.dialogService.showWarning(withMessage: AccountsProviderError.notFound(address: address).localized) return } - + self.dialogService.showError(withMessage: error.localized, supportEmail: false, error: error) } } catch { @@ -328,13 +340,13 @@ final class NewChatViewController: FormViewController { } } } - + func startNewChat(with uri: AdamantUri) -> Bool { switch uri { - case .address(address: let addr, params: let params): + case .address(address: let addr, let params): if let params = params?.first { switch params { - case .label(label: let label): + case .label(let label): startNewChat(with: addr, name: label, message: nil) case .address, .message, .amount: break @@ -342,13 +354,43 @@ final class NewChatViewController: FormViewController { } else { startNewChat(with: addr, message: nil) } - + return true - + default: return false } } + + // MARK: - FormViewController + + override func textInputShouldReturn(_ textInput: UITextInput, cell: Cell) -> Bool { + let result = super.textInputShouldReturn(textInput, cell: cell) + if cell.row.tag == Rows.addressField.tag { + done() + } + + return result + } +} + +// MARK: - Hardware keyboard handling + +extension NewChatViewController { + override var canBecomeFirstResponder: Bool { + return true + } + + override func pressesBegan(_ presses: Set, with event: UIPressesEvent?) { + for press in presses { + guard let key = press.key else { continue } + if key.keyCode == UIKeyboardHIDUsage.keyboardReturnOrEnter { + return done() + } + } + + super.pressesBegan(presses, with: event) + } } // MARK: - QR @@ -372,18 +414,20 @@ extension NewChatViewController { alert.addAction(UIAlertAction(title: String.adamant.alert.ok, style: .cancel, handler: nil)) alert.modalPresentationStyle = .overFullScreen present(alert, animated: true, completion: nil) - + case .denied: let alert = UIAlertController(title: nil, message: String.adamant.login.cameraNotAuthorized, preferredStyleSafe: .alert, source: nil) - - alert.addAction(UIAlertAction(title: String.adamant.alert.settings, style: .default) { _ in - DispatchQueue.main.async { - if let settingsURL = URL(string: UIApplication.openSettingsURLString) { - UIApplication.shared.open(settingsURL) + + alert.addAction( + UIAlertAction(title: String.adamant.alert.settings, style: .default) { _ in + DispatchQueue.main.async { + if let settingsURL = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(settingsURL) + } } } - }) - + ) + alert.addAction(UIAlertAction(title: String.adamant.alert.cancel, style: .cancel, handler: nil)) alert.modalPresentationStyle = .overFullScreen present(alert, animated: true, completion: nil) @@ -391,7 +435,7 @@ extension NewChatViewController { break } } - + func loadQr() { let presenter: () -> Void = { [weak self] in let picker = UIImagePickerController() @@ -403,7 +447,7 @@ extension NewChatViewController { picker.overrideUserInterfaceStyle = .light self?.present(picker, animated: true, completion: nil) } - + presenter() } } @@ -426,7 +470,7 @@ extension NewChatViewController: QRCodeReaderViewControllerDelegate { } } } - + nonisolated func readerDidCancel(_ reader: QRCodeReaderViewController) { MainActor.assumeIsolatedSafe { reader.dismiss(animated: true, completion: nil) @@ -436,15 +480,15 @@ extension NewChatViewController: QRCodeReaderViewControllerDelegate { // MARK: - UIImagePickerControllerDelegate extension NewChatViewController: UINavigationControllerDelegate, UIImagePickerControllerDelegate { - func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { dismiss(animated: true, completion: nil) - + guard let image = info[.originalImage] as? UIImage, let cgImage = image.cgImage else { return } - + let codes = EFQRCode.recognize(cgImage) - + if codes.count > 0 { for aCode in codes { if let admAddress = aCode.getAdamantAddress() { @@ -455,7 +499,7 @@ extension NewChatViewController: UINavigationControllerDelegate, UIImagePickerCo return } } - + dialogService.showWarning(withMessage: String.adamant.newChat.wrongQrError) } else { dialogService.showWarning(withMessage: String.adamant.login.noQrError) @@ -469,10 +513,10 @@ extension NewChatViewController: ButtonsStripeViewDelegate { switch button { case .qrCameraReader: scanQr() - + case .qrPhotoReader: loadQr() - + default: return } diff --git a/Adamant/Modules/ChatsList/SearchResultsViewController.swift b/Adamant/Modules/ChatsList/SearchResultsViewController.swift index 24c2f2092..5af28f0f9 100644 --- a/Adamant/Modules/ChatsList/SearchResultsViewController.swift +++ b/Adamant/Modules/ChatsList/SearchResultsViewController.swift @@ -6,10 +6,10 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import Swinject -import MarkdownKit import CommonKit +import MarkdownKit +import Swinject +import UIKit extension String.adamant { enum search { @@ -36,25 +36,25 @@ protocol SearchResultDelegate: AnyObject { } final class SearchResultsViewController: UITableViewController { - + // MARK: - Dependencies let screensFactory: ScreensFactory let avatarService: AvatarService let addressBookService: AddressBookService let accountsProvider: AccountsProvider - + // MARK: Properties private var contacts: [Chatroom] = [Chatroom]() private var messages: [MessageTransaction] = [MessageTransaction]() private var searchText: String = "" private var newAccount: CoreDataAccount? - + private let markdownParser = MarkdownParser(font: UIFont.systemFont(ofSize: ChatTableViewCell.shortDescriptionTextSize)) - + weak var delegate: SearchResultDelegate? // MARK: Init - + init( screensFactory: ScreensFactory, avatarService: AvatarService, @@ -67,34 +67,34 @@ final class SearchResultsViewController: UITableViewController { self.accountsProvider = accountsProvider super.init(nibName: String(describing: Self.self), bundle: nil) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: Lifecycle - + override func viewDidLoad() { super.viewDidLoad() navigationItem.largeTitleDisplayMode = .never tableView.register(UINib(nibName: "ChatTableViewCell", bundle: nil), forCellReuseIdentifier: "resultCell") setColors() } - + // MARK: - Other - + func setColors() { view.backgroundColor = UIColor.adamant.backgroundColor } - + func updateResult(contacts: [Chatroom]?, messages: [MessageTransaction]?, searchText: String) { self.contacts = contacts ?? [Chatroom]() self.messages = messages ?? [MessageTransaction]() self.searchText = searchText self.newAccount = nil - + findAccountIfNeeded() - + tableView.reloadData() } @@ -122,31 +122,31 @@ final class SearchResultsViewController: UITableViewController { case .none: return 0 } } - + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "resultCell", for: indexPath) as! ChatTableViewCell - + switch defineSection(for: indexPath) { case .contacts: let contact = contacts[indexPath.row] cell.lastMessageLabel.textColor = UIColor.adamant.primary configureCell(cell, for: contact) - + case .messages: let message = messages[indexPath.row] - cell.lastMessageLabel.textColor = nil // Managed by NSAttributedText + cell.lastMessageLabel.textColor = nil // Managed by NSAttributedText configureCell(cell, for: message) - + case .new: configureCell(cell, for: newAccount) - + case .none: break } - + return cell } - + private func configureCell(_ cell: ChatTableViewCell, for chatroom: Chatroom) { if let partner = chatroom.partner { cell.lastMessageLabel.text = partner.address @@ -154,7 +154,7 @@ final class SearchResultsViewController: UITableViewController { cell.avatarImageView.roundingMode = .round cell.avatarImageView.clipsToBounds = true cell.borderWidth = 0 - + if let avatarName = partner.avatar, let avatar = UIImage.asset(named: avatarName) { cell.avatarImage = avatar } else if let publicKey = partner.publicKey { @@ -166,15 +166,15 @@ final class SearchResultsViewController: UITableViewController { } else if let title = chatroom.title { cell.lastMessageLabel.text = title } - + cell.accountLabel.text = chatroom.getName( addressBookService: addressBookService ) - + cell.hasUnreadMessages = false cell.dateLabel.text = nil } - + private func configureCell(_ cell: ChatTableViewCell, for message: MessageTransaction) { if let partner = message.chatroom?.partner { if let avatarName = partner.avatar, let avatar = UIImage.asset(named: avatarName) { @@ -183,7 +183,7 @@ final class SearchResultsViewController: UITableViewController { } else { if let address = partner.publicKey { let image = self.avatarService.avatar(for: address, size: 200) - + cell.avatarImage = image cell.avatarImageView.roundingMode = .round cell.avatarImageView.clipsToBounds = true @@ -193,32 +193,32 @@ final class SearchResultsViewController: UITableViewController { cell.borderWidth = 0 } } - + cell.accountLabel.text = message.chatroom?.getName( addressBookService: addressBookService ) - + cell.hasUnreadMessages = false - + cell.lastMessageLabel.attributedText = shortDescription(for: message) - + if let date = message.dateValue, date != .adamantNullDate { cell.dateLabel.text = date.humanizedDay(useTimeFormat: false) } else { cell.dateLabel.text = nil } } - + private func configureCell(_ cell: ChatTableViewCell, for partner: CoreDataAccount?) { guard let partner = partner else { return } - + if let avatarName = partner.avatar, let avatar = UIImage.asset(named: avatarName) { cell.avatarImage = avatar cell.avatarImageView.tintColor = UIColor.adamant.primary } else { if let address = partner.publicKey { let image = self.avatarService.avatar(for: address, size: 200) - + cell.avatarImage = image cell.avatarImageView.roundingMode = .round cell.avatarImageView.clipsToBounds = true @@ -227,69 +227,73 @@ final class SearchResultsViewController: UITableViewController { } cell.borderWidth = 0 } - + cell.lastMessageLabel.text = partner.address cell.accountLabel.text = .adamant.search.newContact cell.hasUnreadMessages = false cell.dateLabel.text = nil } - + private func shortDescription(for transaction: ChatTransaction) -> NSAttributedString? { switch transaction { case let message as MessageTransaction: guard let text = message.message else { return nil } - + let raw: String if message.isOutgoing { raw = "\(String.adamant.chatList.sentMessagePrefix)\(text)" } else { raw = text } - + let attributedString = markdownParser.parse(raw).mutableCopy() as! NSMutableAttributedString - attributedString.addAttribute(.foregroundColor, - value: UIColor.adamant.primary, - range: NSRange(location: 0, length: attributedString.length)) - + attributedString.addAttribute( + .foregroundColor, + value: UIColor.adamant.primary, + range: NSRange(location: 0, length: attributedString.length) + ) + if let ranges = attributedString.string.range(of: searchText, options: .caseInsensitive) { - attributedString.addAttribute(.foregroundColor, - value: UIColor.adamant.active, - range: NSRange(ranges, in: attributedString.string)) + attributedString.addAttribute( + .foregroundColor, + value: UIColor.adamant.active, + range: NSRange(ranges, in: attributedString.string) + ) } - + return attributedString - + default: return nil } } - + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) guard let delegate = delegate else { return } - + switch defineSection(for: indexPath) { case .contacts: let contact = contacts[indexPath.row] delegate.didSelected(contact) - + case .messages: let message = messages[indexPath.row] delegate.didSelected(message) - + case .new: guard let account = newAccount else { return } delegate.didSelected(account) - + case .none: return } } - + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { switch defineSection(for: section) { case .contacts: return String.adamant.search.contacts @@ -305,7 +309,7 @@ final class SearchResultsViewController: UITableViewController { case .messages: return 80 } } - + // MARK: - Working with sections private enum Section { case contacts @@ -313,11 +317,11 @@ final class SearchResultsViewController: UITableViewController { case none case new } - + private func defineSection(for indexPath: IndexPath) -> Section { return defineSection(for: indexPath.section) } - + private func defineSection(for section: Int) -> Section { if self.contacts.count > 0, self.messages.count > 0 { if section == 0 { @@ -335,19 +339,19 @@ final class SearchResultsViewController: UITableViewController { return .none } } - + // MARK: Other - + @MainActor private func findAccountIfNeeded() { guard case .valid = AdamantUtilities.validateAdamantAddress(address: searchText), - contacts.count == 0, - messages.count == 0 + contacts.count == 0, + messages.count == 0 else { return } - + Task { let account = try await accountsProvider.getAccount(byAddress: searchText) newAccount = account - + tableView.reloadData() } } diff --git a/Adamant/Modules/CoinsNodesList/CoinsNodesListFactory.swift b/Adamant/Modules/CoinsNodesList/CoinsNodesListFactory.swift index ea8641fbf..c820cc779 100644 --- a/Adamant/Modules/CoinsNodesList/CoinsNodesListFactory.swift +++ b/Adamant/Modules/CoinsNodesList/CoinsNodesListFactory.swift @@ -6,9 +6,9 @@ // Copyright © 2023 Adamant. All rights reserved. // -import Swinject -import SwiftUI import CommonKit +import SwiftUI +import Swinject enum CoinsNodesListContext { case login @@ -19,17 +19,17 @@ enum CoinsNodesListContext { struct CoinsNodesListFactory { private let parent: Assembler private let assemblies = [CoinsNodesListAssembly()] - + init(parent: Assembler) { self.parent = parent } - + @MainActor func makeViewController(context: CoinsNodesListContext) -> UIViewController { let assembler = Assembler(assemblies, parent: parent) let viewModel = { assembler.resolver.resolve(CoinsNodesListViewModel.self)! } let view = CoinsNodesListView(viewModel: viewModel) - + switch context { case .login: return SelfRemovableHostingController(rootView: view) @@ -43,7 +43,7 @@ private struct CoinsNodesListAssembly: MainThreadAssembly { func assembleOnMainThread(container: Container) { container.register(CoinsNodesListViewModel.self) { let processedGroups = NodeGroup.allCases.filter { $0 != .adm } - + return .init( mapper: .init(), nodesStorage: $0.resolve(NodesStorageProtocol.self)!, diff --git a/Adamant/Modules/CoinsNodesList/View/CoinsNodesListView+Row.swift b/Adamant/Modules/CoinsNodesList/View/CoinsNodesListView+Row.swift index e86d0977d..39e8fe5d5 100644 --- a/Adamant/Modules/CoinsNodesList/View/CoinsNodesListView+Row.swift +++ b/Adamant/Modules/CoinsNodesList/View/CoinsNodesListView+Row.swift @@ -6,14 +6,14 @@ // Copyright © 2023 Adamant. All rights reserved. // -import SwiftUI import CommonKit +import SwiftUI extension CoinsNodesListView { struct Row: View { let model: CoinsNodesListState.Section.Row let setIsEnabled: (Bool) -> Void - + var body: some View { HStack(spacing: 10) { CheckmarkView( @@ -22,13 +22,13 @@ extension CoinsNodesListView { ) .frame(squareSize: 24) .animation(.easeInOut(duration: 0.1), value: model.isEnabled) - + VStack(alignment: .leading, spacing: 4) { Text(model.title).font(titleFont).lineLimit(1) - + HStack(spacing: 6) { Text(model.connectionStatus).font(captionFont) - + Text(model.subtitle).font(subtitleFont) .lineLimit(1) .frame(height: 10) @@ -39,11 +39,11 @@ extension CoinsNodesListView { } } -private extension CoinsNodesListView.Row { - struct CheckmarkView: View { +extension CoinsNodesListView.Row { + fileprivate struct CheckmarkView: View { let isEnabled: Bool let setIsEnabled: (Bool) -> Void - + var body: some View { ZStack { if isEnabled { @@ -52,7 +52,7 @@ private extension CoinsNodesListView.Row { .scaledToFit() .transition(.scale) } - + if !isEnabled { Circle().strokeBorder( Color(uiColor: .adamant.secondary), diff --git a/Adamant/Modules/CoinsNodesList/View/CoinsNodesListView.swift b/Adamant/Modules/CoinsNodesList/View/CoinsNodesListView.swift index 7b6b0a74a..7fb859840 100644 --- a/Adamant/Modules/CoinsNodesList/View/CoinsNodesListView.swift +++ b/Adamant/Modules/CoinsNodesList/View/CoinsNodesListView.swift @@ -6,12 +6,12 @@ // Copyright © 2023 Adamant. All rights reserved. // -import SwiftUI import CommonKit +import SwiftUI struct CoinsNodesListView: View { @StateObject private var viewModel: CoinsNodesListViewModel - + var body: some View { List { ForEach(viewModel.state.sections, content: makeSection) @@ -26,18 +26,18 @@ struct CoinsNodesListView: View { isPresented: $viewModel.state.isAlertShown ) { Button(String.adamant.alert.cancel, role: .cancel) {} - Button(String.adamant.coinsNodesList.reset) { viewModel.reset() } + Button(String.adamant.coinsNodesList.reset, role: .destructive) { viewModel.reset() } } .navigationTitle(String.adamant.coinsNodesList.title) } - + init(viewModel: @escaping () -> CoinsNodesListViewModel) { _viewModel = .init(wrappedValue: viewModel()) } } -private extension CoinsNodesListView { - func makeSection(_ model: CoinsNodesListState.Section) -> some View { +extension CoinsNodesListView { + fileprivate func makeSection(_ model: CoinsNodesListState.Section) -> some View { Section( header: Text(model.title), content: { @@ -56,8 +56,8 @@ private extension CoinsNodesListView { } ) } - - func makeFastestNodeModeSection() -> some View { + + fileprivate func makeFastestNodeModeSection() -> some View { Section( content: { Toggle( @@ -70,8 +70,8 @@ private extension CoinsNodesListView { footer: { Text(String.adamant.coinsNodesList.fastestNodeTip) } ) } - - func makeResetSection() -> some View { + + fileprivate func makeResetSection() -> some View { Section { Button(action: showResetAlert) { Text(String.adamant.coinsNodesList.reset) @@ -80,8 +80,8 @@ private extension CoinsNodesListView { }.listRowBackground(Color(uiColor: .adamant.cellColor)) } } - - func showResetAlert() { + + fileprivate func showResetAlert() { viewModel.state.isAlertShown = true } } diff --git a/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListMapper.swift b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListMapper.swift index d0d57cf97..7e45e05a1 100644 --- a/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListMapper.swift +++ b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListMapper.swift @@ -28,8 +28,8 @@ struct CoinsNodesListMapper { } } -private extension CoinsNodesListMapper { - func map( +extension CoinsNodesListMapper { + fileprivate func map( node: Node, group: NodeGroup, isRest: Bool @@ -37,18 +37,19 @@ private extension CoinsNodesListMapper { let indicatorString = node.indicatorString(isRest: isRest, isWs: false) var indicatorAttrString = AttributedString(stringLiteral: indicatorString) indicatorAttrString.foregroundColor = .init(uiColor: node.indicatorColor) - + var titleAttrString = AttributedString(stringLiteral: node.title) titleAttrString.foregroundColor = .init(uiColor: node.titleColor) - - let subtitleString = node.statusString( - showVersion: true, - heightType: group.heightType - ) ?? .empty - + + let subtitleString = + node.statusString( + showVersion: true, + heightType: group.heightType + ) ?? .empty + var subtitleAttrString = AttributedString(stringLiteral: subtitleString) subtitleAttrString.foregroundColor = .init(uiColor: node.statusStringColor) - + return .init( id: node.id, group: group, diff --git a/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListState.swift b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListState.swift index 01f98bc74..2a9d84a5a 100644 --- a/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListState.swift +++ b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListState.swift @@ -6,14 +6,14 @@ // Copyright © 2023 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation struct CoinsNodesListState: Equatable { var sections: [Section] var fastestNodeMode: Bool var isAlertShown: Bool - + static var `default`: Self { Self( sections: .init(), diff --git a/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListStrings.swift b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListStrings.swift index e2283320f..3947a0620 100644 --- a/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListStrings.swift +++ b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListStrings.swift @@ -22,14 +22,14 @@ extension String.adamant { static var resetAlert: String { String.localized("NodesList.ResetNodeListAlert", comment: .empty) } - + static var preferTheFastestNode: String { String.localized( "NodesList.PreferTheFastestNode", comment: .empty ) } - + static var fastestNodeTip: String { String.localized( "NodesList.PreferTheFastestNode.Footer", diff --git a/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListViewModel.swift b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListViewModel.swift index 1cca86954..994f423e0 100644 --- a/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListViewModel.swift +++ b/Adamant/Modules/CoinsNodesList/ViewModel/CoinsNodesListViewModel.swift @@ -6,21 +6,21 @@ // Copyright © 2023 Adamant. All rights reserved. // -import SwiftUI import Combine import CommonKit +import SwiftUI @MainActor final class CoinsNodesListViewModel: ObservableObject { @Published var state: CoinsNodesListState = .default - + private let mapper: CoinsNodesListMapper private let nodesStorage: NodesStorageProtocol private let nodesAdditionalParamsStorage: NodesAdditionalParamsStorageProtocol private let processedGroups: [NodeGroup] private let apiServiceCompose: ApiServiceComposeProtocol private var subscriptions = Set() - + init( mapper: CoinsNodesListMapper, nodesStorage: NodesStorageProtocol, @@ -35,63 +35,68 @@ final class CoinsNodesListViewModel: ObservableObject { self.apiServiceCompose = apiServiceCompose setup() } - + func setIsEnabled(id: UUID, group: NodeGroup, value: Bool) { nodesStorage.updateNode(id: id, group: group) { $0.isEnabled = value } } - + func reset() { nodesStorage.resetNodes(.init(processedGroups)) } } -private extension CoinsNodesListViewModel { - func setup() { +extension CoinsNodesListViewModel { + fileprivate func setup() { state.sections = processedGroups.compactMap { guard let info = apiServiceCompose.get($0)?.nodesInfo else { return nil } return mapper.map(group: $0, nodesInfo: info) } - - $state - .map(\.fastestNodeMode) - .removeDuplicates() - .sink { [weak self] in self?.saveFastestNodeMode($0) } - .store(in: &subscriptions) - + processedGroups.forEach { group in apiServiceCompose.get(group)? .nodesInfoPublisher .sink { [weak self] in self?.updateSections(group: group, nodesInfo: $0) } .store(in: &subscriptions) } - - guard let someGroup = processedGroups.first else { return } - state.fastestNodeMode = nodesAdditionalParamsStorage.isFastestNodeMode(group: someGroup) - - Timer - .publish(every: someGroup.onScreenUpdateInterval, on: .main, in: .default) - .autoconnect() - .sink { [weak self] _ in self?.healthCheck() } - .store(in: &subscriptions) - + + if let someGroup = processedGroups.first { + state.fastestNodeMode = nodesAdditionalParamsStorage.isFastestNodeMode(group: someGroup) + + Timer + .publish(every: someGroup.onScreenUpdateInterval, on: .main, in: .default) + .autoconnect() + .sink { [weak self] _ in self?.healthCheck() } + .store(in: &subscriptions) + } + + setStateObservation() healthCheck() } - - func updateSections(group: NodeGroup, nodesInfo: NodesListInfo) { + + fileprivate func updateSections(group: NodeGroup, nodesInfo: NodesListInfo) { guard let index = state.sections.firstIndex(where: { $0.id == group }) else { return } state.sections[index] = mapper.map(group: group, nodesInfo: nodesInfo) } - - func saveFastestNodeMode(_ value: Bool) { + + fileprivate func saveFastestNodeMode(_ value: Bool) { nodesAdditionalParamsStorage.setFastestNodeMode( groups: .init(processedGroups), value: value ) } - - func healthCheck() { + + fileprivate func healthCheck() { processedGroups.forEach { apiServiceCompose.get($0)?.healthCheck() } } + + fileprivate func setStateObservation() { + $state + .map(\.fastestNodeMode) + .removeDuplicates() + .sink { [weak self] in self?.saveFastestNodeMode($0) } + .store(in: &subscriptions) + + } } diff --git a/Adamant/Modules/Delegates/AdamantDelegateCell.swift b/Adamant/Modules/Delegates/AdamantDelegateCell.swift index 79aae6597..8ab99850e 100644 --- a/Adamant/Modules/Delegates/AdamantDelegateCell.swift +++ b/Adamant/Modules/Delegates/AdamantDelegateCell.swift @@ -6,8 +6,8 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit import SnapKit +import UIKit // MARK: Cell's Delegate @MainActor @@ -18,7 +18,7 @@ protocol AdamantDelegateCellDelegate: AnyObject { // MARK: - final class AdamantDelegateCell: UITableViewCell { private let checkmarkRowView = CheckmarkRowView() - + weak var delegate: AdamantDelegateCellDelegate? { didSet { checkmarkRowView.onCheckmarkTap = { [weak self] in @@ -29,33 +29,33 @@ final class AdamantDelegateCell: UITableViewCell { } } } - + var title: String? { get { checkmarkRowView.title } set { checkmarkRowView.title = newValue } } - + var subtitle: String? { get { checkmarkRowView.subtitle } set { checkmarkRowView.subtitle = newValue } } - + var isChecked: Bool { get { checkmarkRowView.isChecked } set { checkmarkRowView.setIsChecked(newValue, animated: false) } } - + var delegateIsActive: Bool = false { didSet { checkmarkRowView.caption = delegateIsActive ? "●" : "○" } } - + var isUpdating: Bool { get { checkmarkRowView.isUpdating } set { checkmarkRowView.setIsUpdating(newValue, animated: false) } } - + var isUpvoted: Bool = false { didSet { checkmarkRowView.checkmarkImage = isUpvoted ? .asset(named: "Downvote") : .asset(named: "Upvote") @@ -63,21 +63,21 @@ final class AdamantDelegateCell: UITableViewCell { checkmarkRowView.checkmarkImageTintColor = isUpvoted ? .adamant.warning : .adamant.success } } - + required override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupView() } - + required init?(coder: NSCoder) { super.init(coder: coder) setupView() } - + func setupView() { accessoryType = .disclosureIndicator checkmarkRowView.captionColor = .lightGray - + contentView.addSubview(checkmarkRowView) checkmarkRowView.snp.makeConstraints { $0.directionalEdges.equalToSuperview() diff --git a/Adamant/Modules/Delegates/DelegateDetailsViewController.swift b/Adamant/Modules/Delegates/DelegateDetailsViewController.swift index 2511c1c98..5f191964b 100644 --- a/Adamant/Modules/Delegates/DelegateDetailsViewController.swift +++ b/Adamant/Modules/Delegates/DelegateDetailsViewController.swift @@ -6,10 +6,10 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import SafariServices -import DateToolsSwift import CommonKit +import DateToolsSwift +import SafariServices +import UIKit // MARK: - Localization extension String.adamant { @@ -22,7 +22,7 @@ extension String.adamant { // MARK: - final class DelegateDetailsViewController: UIViewController { - + // MARK: - Rows fileprivate enum Row: Int { case username = 0 @@ -32,15 +32,15 @@ final class DelegateDetailsViewController: UIViewController { case vote case producedblocks case missedblocks -// case rate + // case rate case approval case productivity case forgingTime case forged case openInExplorer - + static let total = 12 - + var localized: String { switch self { case .username: return .localized("DelegateDetails.Row.Username", comment: "Delegate Details Screen: Rows title for 'Username'") @@ -55,39 +55,39 @@ final class DelegateDetailsViewController: UIViewController { case .forgingTime: return .localized("DelegateDetails.Row.ForgingTime", comment: "Delegate Details Screen: Rows title for 'Forging time'") case .forged: return .localized("DelegateDetails.Row.Forged", comment: "Delegate Details Screen: Rows title for 'Forged'") case .openInExplorer: return .localized("TransactionDetailsScene.Row.Explorer", comment: "Transaction details: 'Open transaction in explorer' row.") -// case .rate: return .localized("DelegateDetails.Row.Rate", comment: "Delegate Details Screen: Rows title for 'Rate'") + // case .rate: return .localized("DelegateDetails.Row.Rate", comment: "Delegate Details Screen: Rows title for 'Rate'") } } - + func indexPathFor(section: Int) -> IndexPath { return IndexPath(item: rawValue, section: section) } - + var image: UIImage? { switch self { case .openInExplorer: return .asset(named: "row_explorer") - + default: return nil } } } - + // MARK: - Dependencies var apiService: AdamantApiServiceProtocol! var accountService: AccountService! var dialogService: DialogService! - + // MARK: - IBOutlets @IBOutlet weak var tableView: UITableView! - + // MARK: - Properties private let delegateUrl = "https://explorer.adamant.im/delegate/" private let cellIdentifier = "cell" - + var delegate: Delegate? - + private let autoupdateInterval: TimeInterval = 5.0 - + weak var timer: Timer? lazy var percentFormatter: NumberFormatter = { @@ -97,10 +97,10 @@ final class DelegateDetailsViewController: UIViewController { formatter.maximumFractionDigits = 2 return formatter }() - + lazy var durationFormatter: DateComponentsFormatter = { let formatter = DateComponentsFormatter() - formatter.allowedUnits = [ .hour, .minute, .second ] + formatter.allowedUnits = [.hour, .minute, .second] formatter.unitsStyle = .brief formatter.zeroFormattingBehavior = .dropLeading if let localeRaw = UserDefaults.standard.string(forKey: StoreKey.language.languageLocale) { @@ -108,38 +108,38 @@ final class DelegateDetailsViewController: UIViewController { } return formatter }() - + private var forged: Decimal? private var forgingTime: TimeInterval? - + // Double error fix private var prevApiError: (date: Date, error: ApiServiceError)? - + // MARK: - Lifecycle - + override func viewDidLoad() { super.viewDidLoad() - + if let delegate = delegate { refreshData(with: delegate) navigationItem.title = delegate.username } else { navigationItem.title = String.adamant.delegateDetails.title } - + setColors() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - + if let indexPath = tableView.indexPathForSelectedRow { tableView.deselectRow(at: indexPath, animated: true) } } - + // MARK: - Other - + private func setColors() { view.backgroundColor = UIColor.adamant.secondBackgroundColor tableView.backgroundColor = .clear @@ -155,49 +155,52 @@ extension DelegateDetailsViewController: UITableViewDelegate, UITableViewDataSou return 0 } } - + func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { return UIView() } - + func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool { return true } - + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let row = Row(rawValue: indexPath.row) else { return } - + switch row { case .openInExplorer: guard let address = delegate?.address, let url = URL(string: delegateUrl + address) else { return } - + let safari = SFSafariViewController(url: url) safari.preferredControlTintColor = UIColor.adamant.primary safari.modalPresentationStyle = .overFullScreen present(safari, animated: true, completion: nil) - + default: guard let cell = tableView.cellForRow(at: indexPath), let value = cell.detailTextLabel?.text, value.count > 0 else { tableView.deselectRow(at: indexPath, animated: true) return } - + let completion = { [weak self] in guard let tableView = self?.tableView, let indexPath = tableView.indexPathForSelectedRow else { return } tableView.deselectRow(at: indexPath, animated: true) } - - dialogService.presentShareAlertFor(string: value, - types: [.copyToPasteboard, .share], - excludedActivityTypes: nil, - animated: true, from: cell, - completion: completion) + + dialogService.presentShareAlertFor( + string: value, + types: [.copyToPasteboard, .share], + excludedActivityTypes: nil, + animated: true, + from: cell, + completion: completion + ) } } } @@ -208,7 +211,7 @@ extension DelegateDetailsViewController { guard let delegate = delegate, let row = Row(rawValue: indexPath.row) else { return UITableViewCell(style: .default, reuseIdentifier: cellIdentifier) } - + let cell: UITableViewCell if let c = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) { cell = c @@ -220,42 +223,42 @@ extension DelegateDetailsViewController { cell.textLabel?.text = row.localized cell.accessoryType = .none cell.imageView?.image = row.image - + switch row { case .username: cell.detailTextLabel?.text = delegate.username - + case .address: cell.detailTextLabel?.text = delegate.address - + case .publicKey: cell.detailTextLabel?.text = delegate.publicKey - + case .vote: let weight = Decimal(string: delegate.voteFair)?.shiftedFromAdamant() ?? 0 cell.detailTextLabel?.text = AdamantBalanceFormat.short.format(weight) - + case .producedblocks: cell.detailTextLabel?.text = String(delegate.producedblocks) - + case .missedblocks: cell.detailTextLabel?.text = String(delegate.missedblocks) - + case .rank: cell.detailTextLabel?.text = String(delegate.rank) - + case .approval: let text = percentFormatter.string(for: (delegate.approval / 100.0)) cell.detailTextLabel?.text = text - + case .productivity: let text = percentFormatter.string(for: (delegate.productivity / 100.0)) cell.detailTextLabel?.text = text - + case .openInExplorer: cell.accessoryType = .disclosureIndicator cell.detailTextLabel?.text = nil - + case .forgingTime: if let forgingTime = forgingTime { if forgingTime > 0 { @@ -263,15 +266,15 @@ extension DelegateDetailsViewController { } else { cell.detailTextLabel?.text = Date.now.humanizedTime().string } - + } else { cell.detailTextLabel?.text = "" } - + case .forged: cell.detailTextLabel?.text = AdamantBalanceFormat.short.defaultFormatter.string(for: forged) } - + return cell } } @@ -281,24 +284,24 @@ extension DelegateDetailsViewController { private func refreshData(with delegate: Delegate) { Task { let result = await apiService.getForgedByAccount(publicKey: delegate.publicKey) - + switch result { case .success(let details): forged = details.forged - + guard let tableView = tableView else { return } - + let indexPath = Row.forged.indexPathFor(section: 0) tableView.reloadRows(at: [indexPath], with: .none) case .failure(let error): apiServiceFailed(with: error) } - + // Get forging time let forgingTimeResult = await apiService.getForgingTime(for: delegate) - + switch forgingTimeResult { case .success(let seconds): if seconds >= 0 { @@ -306,26 +309,26 @@ extension DelegateDetailsViewController { } else { forgingTime = nil } - + guard let tableView = tableView else { return } - + let indexPath = Row.forgingTime.indexPathFor(section: 0) tableView.reloadRows(at: [indexPath], with: .none) - + case .failure(let error): apiServiceFailed(with: error) } } } - + private func apiServiceFailed(with error: ApiServiceError) { DispatchQueue.main.async { [unowned self] in - if let prevApiError = self.prevApiError, Date().timeIntervalSince(prevApiError.date) < 1, prevApiError.error == error { // if less than a second ago, return + if let prevApiError = self.prevApiError, Date().timeIntervalSince(prevApiError.date) < 1, prevApiError.error == error { // if less than a second ago, return return } - + self.prevApiError = (date: Date(), error: error) self.dialogService.showRichError(error: error) } diff --git a/Adamant/Modules/Delegates/DelegatesBottomPanel/DelegatesBottomPanel+Model.swift b/Adamant/Modules/Delegates/DelegatesBottomPanel/DelegatesBottomPanel+Model.swift index c2176a9ab..a177ff83f 100644 --- a/Adamant/Modules/Delegates/DelegatesBottomPanel/DelegatesBottomPanel+Model.swift +++ b/Adamant/Modules/Delegates/DelegatesBottomPanel/DelegatesBottomPanel+Model.swift @@ -6,8 +6,8 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit import CommonKit +import UIKit extension DelegatesBottomPanel { struct Model { @@ -20,7 +20,7 @@ extension DelegatesBottomPanel { let newVotesColor: UIColor let totalVotesColor: UIColor let sendAction: () -> Void - + static var `default`: Self { Self( upvotes: .zero, diff --git a/Adamant/Modules/Delegates/DelegatesBottomPanel/DelegatesBottomPanel.swift b/Adamant/Modules/Delegates/DelegatesBottomPanel/DelegatesBottomPanel.swift index b9695e77c..412951e19 100644 --- a/Adamant/Modules/Delegates/DelegatesBottomPanel/DelegatesBottomPanel.swift +++ b/Adamant/Modules/Delegates/DelegatesBottomPanel/DelegatesBottomPanel.swift @@ -6,27 +6,27 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit -import SnapKit import CommonKit +import SnapKit +import UIKit final class DelegatesBottomPanel: UIView { var model: Model = .default { didSet { update() } } - + private let upVotesLabel = UILabel(font: .systemFont(ofSize: 16), textColor: .adamant.textColor) private let downVotesLabel = UILabel(font: .systemFont(ofSize: 16), textColor: .adamant.textColor) private let newVotesLabel = UILabel(font: .systemFont(ofSize: 16), textColor: .adamant.textColor) private let totalVotesLabel = UILabel(font: .systemFont(ofSize: 16), textColor: .adamant.textColor) private let costLabel = UILabel(font: .systemFont(ofSize: 12), textColor: .adamant.textColor) - + private lazy var sendButton: UIButton = { let view = UIButton.systemButton(with: .asset(named: "Arrow") ?? .init(), target: self, action: #selector(send)) view.tintColor = .systemBlue return view }() - + private lazy var leftStack: UIStackView = { let view = UIStackView(arrangedSubviews: [upVotesLabel, downVotesLabel]) view.axis = .vertical @@ -34,7 +34,7 @@ final class DelegatesBottomPanel: UIView { view.spacing = spacing return view }() - + private lazy var centralStack: UIStackView = { let view = UIStackView(arrangedSubviews: [newVotesLabel, totalVotesLabel]) view.axis = .vertical @@ -42,7 +42,7 @@ final class DelegatesBottomPanel: UIView { view.spacing = spacing return view }() - + private lazy var rightStack: UIStackView = { let view = UIStackView(arrangedSubviews: [costLabel, sendButton]) view.axis = .vertical @@ -50,65 +50,65 @@ final class DelegatesBottomPanel: UIView { view.spacing = spacing return view }() - + private lazy var horizontalStack: UIStackView = { let view = UIStackView(arrangedSubviews: [leftStack, centralStack, rightStack]) view.axis = .horizontal view.distribution = .equalSpacing return view }() - + override init(frame: CGRect) { super.init(frame: .zero) setup() } - + required init?(coder: NSCoder) { super.init(frame: .zero) setup() } } -private extension DelegatesBottomPanel { - func setup() { +extension DelegatesBottomPanel { + fileprivate func setup() { backgroundColor = .adamant.secondBackgroundColor - + addSubview(horizontalStack) horizontalStack.snp.makeConstraints { $0.verticalEdges.equalToSuperview().inset(spacing) $0.horizontalEdges.equalToSuperview().inset(horizontalSpacing) } - + update() } - - func update() { + + fileprivate func update() { upVotesLabel.text = "\(upvotesPrefix) \(model.upvotes)" downVotesLabel.text = "\(downvotesPrefix) \(model.downvotes)" costLabel.text = model.cost sendButton.isEnabled = model.isSendingEnabled - + newVotesLabel.attributedText = makeString( prefix: newPrefix, string: "\(model.new.0)/\(model.new.1)", color: model.newVotesColor ) - + totalVotesLabel.attributedText = makeString( prefix: totalPrefix, string: "\(model.total.0)/\(model.total.1)", color: model.totalVotesColor ) } - - func makeString(prefix: String, string: String, color: UIColor) -> NSAttributedString { + + fileprivate func makeString(prefix: String, string: String, color: UIColor) -> NSAttributedString { let attributes: [NSAttributedString.Key: Any] = [.foregroundColor: color] let attrString = NSMutableAttributedString(string: prefix + " ") attrString.append(.init(string: string, attributes: attributes)) return attrString } - - @objc func send() { + + @objc fileprivate func send() { model.sendAction() } } diff --git a/Adamant/Modules/Delegates/DelegatesFactory.swift b/Adamant/Modules/Delegates/DelegatesFactory.swift index 76c6071db..25756c678 100644 --- a/Adamant/Modules/Delegates/DelegatesFactory.swift +++ b/Adamant/Modules/Delegates/DelegatesFactory.swift @@ -6,14 +6,14 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import Swinject import CommonKit +import Swinject +import UIKit @MainActor struct DelegatesFactory { let assembler: Assembler - + func makeDelegatesListVC(screensFactory: ScreensFactory) -> UIViewController { DelegatesListViewController( apiService: assembler.resolve(AdamantApiServiceProtocol.self)!, @@ -22,7 +22,7 @@ struct DelegatesFactory { screensFactory: screensFactory ) } - + func makeDelegateDetails() -> DelegateDetailsViewController { let c = DelegateDetailsViewController(nibName: "DelegateDetailsViewController", bundle: nil) c.apiService = assembler.resolve(AdamantApiServiceProtocol.self) diff --git a/Adamant/Modules/Delegates/DelegatesListViewController.swift b/Adamant/Modules/Delegates/DelegatesListViewController.swift index dcc28db18..fcffeccbc 100644 --- a/Adamant/Modules/Delegates/DelegatesListViewController.swift +++ b/Adamant/Modules/Delegates/DelegatesListViewController.swift @@ -6,12 +6,12 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import SnapKit +import Combine import CommonKit import MarkdownKit import SafariServices -import Combine +import SnapKit +import UIKit // MARK: - Localization extension String.adamant { @@ -37,57 +37,57 @@ final class DelegatesListViewController: KeyboardObservingViewController { var delegate: Delegate var isChecked: Bool = false var isUpdating: Bool = false - + init(delegate: Delegate) { self.delegate = delegate } } - + // MARK: - Dependencies - + private let apiService: AdamantApiServiceProtocol private let accountService: AccountService private let dialogService: DialogService private let screensFactory: ScreensFactory - + // MARK: - Constants - + let votingCost = 50 let activeDelegates = 101 let maxVotes = 33 let maxTotalVotes = 101 private let cellIdentifier = "cell" - + // MARK: - Properties - + private lazy var headerTextView: UITextView = { let textView = UITextView() textView.backgroundColor = .clear textView.isEditable = false textView.delegate = self - + let attributedString = NSMutableAttributedString( attributedString: MarkdownParser( font: UIFont.preferredFont(forTextStyle: .subheadline), - color: .adamant.chatPlaceholderTextColor + color: .adamant.chatPlaceholderTextColor ).parse(.localized("Delegates.HeaderText")) ) - + let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.firstLineHeadIndent = 10 paragraphStyle.headIndent = 10 - + attributedString.addAttribute( .paragraphStyle, value: paragraphStyle, range: .init(location: .zero, length: attributedString.length) ) - + textView.attributedText = attributedString textView.linkTextAttributes = [.foregroundColor: UIColor.adamant.active] return textView }() - + private lazy var tableView: UITableView = { let tableView = UITableView(frame: .zero, style: .insetGrouped) tableView.register(AdamantDelegateCell.self, forCellReuseIdentifier: cellIdentifier) @@ -99,7 +99,7 @@ final class DelegatesListViewController: KeyboardObservingViewController { tableView.refreshControl = refreshControl return tableView }() - + private lazy var searchController: UISearchController = { let controller = UISearchController(searchResultsController: nil) controller.searchResultsUpdater = self @@ -107,28 +107,28 @@ final class DelegatesListViewController: KeyboardObservingViewController { controller.hidesNavigationBarDuringPresentation = true return controller }() - + private lazy var refreshControl: UIRefreshControl = { let refreshControl = UIRefreshControl() refreshControl.transform = CGAffineTransform(scaleX: 0.75, y: 0.75) refreshControl.addTarget(self, action: #selector(handleRefresh(_:)), for: UIControl.Event.valueChanged) return refreshControl }() - + private lazy var bottomPanel = DelegatesBottomPanel() - + private(set) var delegates: [CheckedDelegate] = [CheckedDelegate]() private var filteredDelegates: [Int]? private var timerSubscription: AnyCancellable? private var loadingView: LoadingView? private var originalInsets: UIEdgeInsets? private var didShow: Bool = false - + // Can start with 'u' or 'U', then 1-20 digits private let possibleAddressRegEx = try! NSRegularExpression(pattern: "^[uU]{0,1}\\d{1,20}$", options: []) - + // MARK: - Lifecycle - + init( apiService: AdamantApiServiceProtocol, accountService: AccountService, @@ -141,11 +141,11 @@ final class DelegatesListViewController: KeyboardObservingViewController { self.screensFactory = screensFactory super.init(nibName: nil, bundle: nil) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func viewDidLoad() { super.viewDidLoad() setupNavigationItem() @@ -154,7 +154,7 @@ final class DelegatesListViewController: KeyboardObservingViewController { setupLoadingView() handleRefresh(refreshControl) } - + deinit { NotificationCenter.default.removeObserver(self) } @@ -165,75 +165,75 @@ final class DelegatesListViewController: KeyboardObservingViewController { self.dialogService.showRichError(error: AccountServiceError.userNotLogged) return } - + Task { let result = await apiService.getDelegatesWithVotes( for: address, limit: activeDelegates ) - + switch result { case .success(let delegates): let checkedNames = self.delegates .filter { $0.isChecked } .map { $0.delegate.username } - + let checkedDelegates = delegates.map { CheckedDelegate(delegate: $0) } for name in checkedNames { if let i = delegates.firstIndex(where: { $0.username == name }) { checkedDelegates[i].isChecked = true } } - + self.delegates = checkedDelegates self.tableView.reloadData() case .failure(let error): self.dialogService.showRichError(error: error) } - + refreshControl.endRefreshing() self.updateVotePanel() self.removeLoadingView() } } - + @objc private func activateSearch() { if let bar = navigationItem.searchController?.searchBar, !bar.isFirstResponder { bar.becomeFirstResponder() } } - + private func setColors() { view.backgroundColor = UIColor.adamant.secondBackgroundColor } - + private func setupNavigationItem() { navigationItem.title = String.adamant.delegates.title navigationItem.searchController = searchController - + navigationItem.rightBarButtonItem = .init( barButtonSystemItem: .search, target: self, action: #selector(activateSearch) ) } - + private func openURL(_ url: URL) { let safari = SFSafariViewController(url: url) safari.preferredControlTintColor = UIColor.adamant.primary safari.modalPresentationStyle = .overFullScreen present(safari, animated: true, completion: nil) } - + private func setupViews() { view.addSubview(tableView) view.addSubview(bottomPanel) - + tableView.snp.makeConstraints { $0.top.leading.trailing.equalToSuperview() $0.bottom.equalTo(bottomPanel.snp.top) } - + bottomPanel.snp.makeConstraints { $0.bottom.equalTo(view.safeAreaLayoutGuide) $0.horizontalEdges.equalToSuperview() @@ -246,7 +246,7 @@ extension DelegatesListViewController: UITableViewDataSource, UITableViewDelegat func numberOfSections(in tableView: UITableView) -> Int { return 1 } - + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if let filtered = filteredDelegates { return filtered.count @@ -254,37 +254,39 @@ extension DelegatesListViewController: UITableViewDataSource, UITableViewDelegat return delegates.count } } - + func tableView(_: UITableView, viewForHeaderInSection _: Int) -> UIView? { headerTextView } - + func tableView(_: UITableView, heightForHeaderInSection _: Int) -> CGFloat { - headerTextView.sizeThatFits(.init( - width: tableView.contentSize.width - - tableView.layoutMargins.left - - tableView.layoutMargins.right, - height: .greatestFiniteMagnitude - )).height + headerTextView.sizeThatFits( + .init( + width: tableView.contentSize.width + - tableView.layoutMargins.left + - tableView.layoutMargins.right, + height: .greatestFiniteMagnitude + ) + ).height } - + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let controller = screensFactory.makeDelegateDetails() controller.delegate = checkedDelegateFor(indexPath: indexPath).delegate - + navigationController?.pushViewController(controller, animated: true) } - + // MARK: Cells func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? AdamantDelegateCell else { return UITableViewCell(style: .default, reuseIdentifier: cellIdentifier) } - + let checkedDelegate = checkedDelegateFor(indexPath: indexPath) let delegate = checkedDelegate.delegate cell.backgroundColor = UIColor.adamant.cellColor - + cell.title = [String(delegate.rank), delegate.username].joined(separator: " ") cell.subtitle = delegate.address cell.delegateIsActive = delegate.rank <= activeDelegates @@ -292,7 +294,7 @@ extension DelegatesListViewController: UITableViewDataSource, UITableViewDelegat cell.isUpvoted = delegate.voted cell.isChecked = checkedDelegate.isChecked cell.isUpdating = checkedDelegate.isUpdating - + return cell } } @@ -303,40 +305,40 @@ extension DelegatesListViewController: AdamantDelegateCellDelegate { guard let indexPath = tableView.indexPath(for: cell) else { return } - + checkedDelegateFor(indexPath: indexPath).isChecked = state updateVotePanel() } } // MARK: - Voting -private extension DelegatesListViewController { - func vote() { +extension DelegatesListViewController { + fileprivate func vote() { if timerSubscription != nil { self.dialogService.showWarning(withMessage: String.adamant.delegates.timeOutBeforeNewVote) return } - + // MARK: Prepare let checkedDelegates = delegates.enumerated().filter { $1.isChecked } guard checkedDelegates.count > 0 else { return } - + guard let account = accountService.account, let keypair = accountService.keypair else { self.dialogService.showRichError(error: AccountServiceError.userNotLogged) return } - + guard account.balance > Decimal(votingCost) else { self.dialogService.showWarning(withMessage: String.adamant.delegates.notEnoughtTokensForVote) return } - + // MARK: Build request and update UI - + var votes = [DelegateVote]() - + for checked in checkedDelegates { let delegate = checked.element.delegate let vote: DelegateVote = delegate.voted ? .downvote(publicKey: delegate.publicKey) : .upvote(publicKey: delegate.publicKey) @@ -344,18 +346,19 @@ private extension DelegatesListViewController { } // MARK: Send - + dialogService.showProgress(withMessage: nil, userInteractionEnable: false) Task { let result = await apiService.voteForDelegates( from: account.address, keypair: keypair, - votes: votes + votes: votes, + date: AdmWalletService.correctedDate ) - + dialogService.dismissProgress() - + switch result { case .success: dialogService.showSuccess(withMessage: String.adamant.delegates.success) @@ -365,7 +368,7 @@ private extension DelegatesListViewController { $1.delegate.voted = !$1.delegate.voted $1.isUpdating = true } - + tableView.reloadData() updateVotePanel() scheduleUpdate() @@ -382,48 +385,48 @@ extension DelegatesListViewController: UISearchResultsUpdating { func updateSearchResults(for searchController: UISearchController) { if let search = searchController.searchBar.text?.lowercased(), search.count > 0 { let searchAddress = possibleAddressRegEx.matches(in: search, options: [], range: NSRange(location: 0, length: search.count)).count == 1 - + let filter: ((Int, CheckedDelegate) -> Bool) if searchAddress { filter = { $1.delegate.username.lowercased().contains(search) || $1.delegate.address.lowercased().contains(search) } } else { filter = { $1.delegate.username.lowercased().contains(search) } } - + filteredDelegates = delegates.enumerated().filter(filter).map { $0.offset } } else { filteredDelegates = nil } - + tableView.reloadData() } } // MARK: - Private -private extension DelegatesListViewController { - func checkedDelegateFor(indexPath: IndexPath) -> CheckedDelegate { +extension DelegatesListViewController { + fileprivate func checkedDelegateFor(indexPath: IndexPath) -> CheckedDelegate { if let filtered = filteredDelegates { return delegates[filtered[indexPath.row]] } else { return delegates[indexPath.row] } } - - func scheduleUpdate() { + + fileprivate func scheduleUpdate() { timerSubscription = Timer.publish(every: 20, on: .main, in: .default) .autoconnect() .first() .sink { [weak self] _ in self?.updateTimerCallback() } } - - func updateTimerCallback() { + + fileprivate func updateTimerCallback() { handleRefresh(refreshControl) timerSubscription = nil } - - func updateVotePanel() { + + fileprivate func updateVotePanel() { let changes = delegates.filter { $0.isChecked }.map { $0.delegate } - + var upvoted = 0 var downvoted = 0 for delegate in changes { @@ -433,13 +436,13 @@ private extension DelegatesListViewController { upvoted += 1 } } - + let totalVoted = delegates.reduce(0) { $0 + ($1.delegate.voted ? 1 : 0) } + upvoted - downvoted - + let votingEnabled = changes.count > 0 && changes.count <= maxVotes && totalVoted <= maxTotalVotes let newVotesColor = changes.count > maxVotes ? UIColor.adamant.attention : UIColor.adamant.primary let totalVotesColor = totalVoted > maxTotalVotes ? UIColor.adamant.attention : UIColor.adamant.primary - + DispatchQueue.onMainAsync { [self] in bottomPanel.model = .init( upvotes: upvoted, @@ -454,21 +457,21 @@ private extension DelegatesListViewController { ) } } - - func setupLoadingView() { + + fileprivate func setupLoadingView() { let loadingView = LoadingView() view.addSubview(loadingView) loadingView.snp.makeConstraints { $0.directionalEdges.equalToSuperview() } loadingView.startAnimating() - + self.loadingView = loadingView } - - func removeLoadingView() { + + fileprivate func removeLoadingView() { guard loadingView != nil else { return } - + UIView.animate( withDuration: 0.25, animations: { [weak loadingView] in loadingView?.alpha = .zero }, diff --git a/Adamant/Modules/InfoService/InfoService+Constants.swift b/Adamant/Modules/InfoService/InfoService+Constants.swift index d81cdeb88..dcb54f2eb 100644 --- a/Adamant/Modules/InfoService/InfoService+Constants.swift +++ b/Adamant/Modules/InfoService/InfoService+Constants.swift @@ -10,7 +10,7 @@ import CommonKit extension InfoService { nonisolated static let threshold = 1800 - + nonisolated static var name: String { .localized("InfoService.InfoService") } diff --git a/Adamant/Modules/InfoService/InfoServiceAssembly.swift b/Adamant/Modules/InfoService/InfoServiceAssembly.swift index 2c9f73edf..dc429176a 100644 --- a/Adamant/Modules/InfoService/InfoServiceAssembly.swift +++ b/Adamant/Modules/InfoService/InfoServiceAssembly.swift @@ -6,25 +6,26 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Swinject import CommonKit +import Swinject struct InfoServiceAssembly: MainThreadAssembly { func assembleOnMainThread(container: Container) { container.register(InfoServiceProtocol.self) { r in InfoService( - securedStore: r.resolve(SecuredStore.self)!, + SecureStore: r.resolve(SecureStore.self)!, walletServiceCompose: r.resolve(WalletServiceCompose.self)!, api: r.resolve(InfoServiceApiServiceProtocol.self)! ) }.inObjectScope(.container) - + container.register(InfoServiceApiServiceProtocol.self) { r in InfoServiceApiService( core: .init( service: .init( apiCore: r.resolve(APICoreProtocol.self)!, - mapper: r.resolve(InfoServiceMapperProtocol.self)!), + mapper: r.resolve(InfoServiceMapperProtocol.self)! + ), nodesStorage: r.resolve(NodesStorageProtocol.self)!, nodesAdditionalParamsStorage: r.resolve(NodesAdditionalParamsStorageProtocol.self)!, isActive: true, @@ -34,11 +35,11 @@ struct InfoServiceAssembly: MainThreadAssembly { mapper: r.resolve(InfoServiceMapperProtocol.self)! ) }.inObjectScope(.container) - + container.register(InfoServiceMapperProtocol.self) { _ in InfoServiceMapper() }.inObjectScope(.transient) - + container.register(InfoServiceApiCore.self) { r in InfoServiceApiCore( apiCore: r.resolve(APICoreProtocol.self)!, diff --git a/Adamant/Modules/InfoService/Models/InfoServiceApiError.swift b/Adamant/Modules/InfoService/Models/InfoServiceApiError.swift index 2d87d8817..b80d05d59 100644 --- a/Adamant/Modules/InfoService/Models/InfoServiceApiError.swift +++ b/Adamant/Modules/InfoService/Models/InfoServiceApiError.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation enum InfoServiceApiError: Error, Sendable { case unknown @@ -29,7 +29,7 @@ extension InfoServiceApiError: RichError { error.message } } - + var internalError: Error? { switch self { case .unknown, .parsingError, .inconsistentData: @@ -38,7 +38,7 @@ extension InfoServiceApiError: RichError { error } } - + var level: ErrorLevel { switch self { case .unknown, .parsingError, .inconsistentData: diff --git a/Adamant/Modules/InfoService/Models/InfoServiceStatus.swift b/Adamant/Modules/InfoService/Models/InfoServiceStatus.swift index 0c67ad745..6437e922d 100644 --- a/Adamant/Modules/InfoService/Models/InfoServiceStatus.swift +++ b/Adamant/Modules/InfoService/Models/InfoServiceStatus.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation struct InfoServiceStatus { let lastUpdated: Date diff --git a/Adamant/Modules/InfoService/Models/InfoServiceTicker.swift b/Adamant/Modules/InfoService/Models/InfoServiceTicker.swift index d8e4ed90d..b2ccddce4 100644 --- a/Adamant/Modules/InfoService/Models/InfoServiceTicker.swift +++ b/Adamant/Modules/InfoService/Models/InfoServiceTicker.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation struct InfoServiceTicker: Hashable, Sendable { let crypto: String diff --git a/Adamant/Modules/InfoService/Protocols/InfoServiceApiServiceProtocol.swift b/Adamant/Modules/InfoService/Protocols/InfoServiceApiServiceProtocol.swift index 7e74c1d0e..c18d5e9f9 100644 --- a/Adamant/Modules/InfoService/Protocols/InfoServiceApiServiceProtocol.swift +++ b/Adamant/Modules/InfoService/Protocols/InfoServiceApiServiceProtocol.swift @@ -6,14 +6,14 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation protocol InfoServiceApiServiceProtocol: ApiServiceProtocol { func loadRates( coins: [String] ) async -> InfoServiceApiResult<[InfoServiceTicker: Decimal]> - + func getHistory( coin: String, date: Date diff --git a/Adamant/Modules/InfoService/Protocols/InfoServiceMapperProtocol.swift b/Adamant/Modules/InfoService/Protocols/InfoServiceMapperProtocol.swift index 70f3b0da3..5d65b3cb5 100644 --- a/Adamant/Modules/InfoService/Protocols/InfoServiceMapperProtocol.swift +++ b/Adamant/Modules/InfoService/Protocols/InfoServiceMapperProtocol.swift @@ -6,27 +6,27 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation protocol InfoServiceMapperProtocol: Sendable { func mapToModel(_ dto: InfoServiceStatusDTO) -> InfoServiceStatus - + func mapRatesToModel( _ dto: InfoServiceResponseDTO<[String: Decimal]> ) -> InfoServiceApiResult<[InfoServiceTicker: Decimal]> - + func mapToModel( _ dto: InfoServiceResponseDTO<[InfoServiceHistoryItemDTO]> ) -> InfoServiceApiResult - + func mapToNodeStatusInfo( ping: TimeInterval, status: InfoServiceStatus ) -> NodeStatusInfo - + func mapToRatesRequestDTO(_ coins: [String]) -> InfoServiceRatesRequestDTO - + func mapToHistoryRequestDTO( date: Date, coin: String diff --git a/Adamant/Modules/InfoService/Protocols/InfoServiceProtocol.swift b/Adamant/Modules/InfoService/Protocols/InfoServiceProtocol.swift index 68ba4e6ab..68ba22c8a 100644 --- a/Adamant/Modules/InfoService/Protocols/InfoServiceProtocol.swift +++ b/Adamant/Modules/InfoService/Protocols/InfoServiceProtocol.swift @@ -6,8 +6,8 @@ // Copyright © 2019 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation extension Notification.Name { struct AdamantCurrencyInfoService { @@ -28,7 +28,7 @@ enum Currency: String, CaseIterable { case EUR case CNY case JPY - + var symbol: String { switch self { case .RUB: return "₽" @@ -38,7 +38,7 @@ enum Currency: String, CaseIterable { case .JPY: return "¥" } } - + static let `default` = Currency.USD } @@ -46,13 +46,13 @@ enum Currency: String, CaseIterable { @MainActor protocol InfoServiceProtocol: AnyObject, Sendable { var currentCurrency: Currency { get set } - + // Check rates for list of coins func update() - + // Get rate for pair Crypto / Fiat currencies func getRate(for coin: String) -> Decimal? - + func getHistory( for coin: String, date: Date diff --git a/Adamant/Modules/InfoService/Services/ApiService/InfoServiceApiService+Extension.swift b/Adamant/Modules/InfoService/Services/ApiService/InfoServiceApiService+Extension.swift index bd5f54cf6..f4a2f8457 100644 --- a/Adamant/Modules/InfoService/Services/ApiService/InfoServiceApiService+Extension.swift +++ b/Adamant/Modules/InfoService/Services/ApiService/InfoServiceApiService+Extension.swift @@ -6,16 +6,16 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation extension InfoServiceApiService: ApiServiceProtocol { @MainActor var nodesInfoPublisher: AnyObservable { core.nodesInfoPublisher } - + @MainActor var nodesInfo: NodesListInfo { core.nodesInfo } - + func healthCheck() { core.healthCheck() } } @@ -33,7 +33,7 @@ extension InfoServiceApiService: InfoServiceApiServiceProtocol { ) }.flatMap { mapper.mapRatesToModel($0) } } - + func getHistory( coin: String, date: Date diff --git a/Adamant/Modules/InfoService/Services/ApiService/InfoServiceApiService.swift b/Adamant/Modules/InfoService/Services/ApiService/InfoServiceApiService.swift index 16a83d66e..e1755702a 100644 --- a/Adamant/Modules/InfoService/Services/ApiService/InfoServiceApiService.swift +++ b/Adamant/Modules/InfoService/Services/ApiService/InfoServiceApiService.swift @@ -6,13 +6,13 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation final class InfoServiceApiService: Sendable { let core: BlockchainHealthCheckWrapper let mapper: InfoServiceMapperProtocol - + func request( _ request: @Sendable ( APICoreProtocol, @@ -23,7 +23,7 @@ final class InfoServiceApiService: Sendable { await request(core.apiCore, origin) }.mapError { .apiError($0) } } - + init(core: BlockchainHealthCheckWrapper, mapper: InfoServiceMapperProtocol) { self.core = core self.mapper = mapper diff --git a/Adamant/Modules/InfoService/Services/InfoService.swift b/Adamant/Modules/InfoService/Services/InfoService.swift index 4c7c351c9..9e8bd44ac 100644 --- a/Adamant/Modules/InfoService/Services/InfoService.swift +++ b/Adamant/Modules/InfoService/Services/InfoService.swift @@ -13,48 +13,48 @@ import UIKit @MainActor final class InfoService: InfoServiceProtocol { typealias Rates = [InfoServiceTicker: Decimal] - - private let securedStore: SecuredStore + + private let SecureStore: SecureStore private let api: InfoServiceApiServiceProtocol private let rateCoins: [String] - + private var rates = Rates() private var currentCurrencyValue: Currency = .default private var subscriptions = Set() private var isUpdating = false - + var currentCurrency: Currency { get { currentCurrencyValue } set { updateCurrency(newValue) } } - + init( - securedStore: SecuredStore, + SecureStore: SecureStore, walletServiceCompose: WalletServiceCompose, api: InfoServiceApiServiceProtocol ) { - self.securedStore = securedStore + self.SecureStore = SecureStore self.api = api rateCoins = walletServiceCompose.getWallets().map { $0.core.tokenSymbol } configure() } - + func update() { Task { guard !isUpdating else { return } isUpdating = true defer { isUpdating = false } - + guard let newRates = try? await api.loadRates(coins: rateCoins).get() else { return } rates = newRates sendRatesChangedNotification() } } - + func getRate(for coin: String) -> Decimal? { rates[.init(crypto: coin, fiat: currentCurrency.rawValue)] } - + func getHistory( for coin: String, date: Date @@ -67,33 +67,32 @@ final class InfoService: InfoServiceProtocol { } } -private extension InfoService { - func configure() { +extension InfoService { + fileprivate func configure() { setupCurrency() - + NotificationCenter.default .notifications(named: UIApplication.didBecomeActiveNotification) .sink { [weak self] _ in await self?.update() } .store(in: &subscriptions) } - - func sendRatesChangedNotification() { + + fileprivate func sendRatesChangedNotification() { NotificationCenter.default.post( name: .AdamantCurrencyInfoService.currencyRatesUpdated, object: nil ) } - - func updateCurrency(_ newValue: Currency) { + + fileprivate func updateCurrency(_ newValue: Currency) { guard newValue != currentCurrencyValue else { return } currentCurrencyValue = newValue - securedStore.set(currentCurrencyValue.rawValue, for: StoreKey.CoinInfo.selectedCurrency) + SecureStore.set(currentCurrencyValue.rawValue, for: StoreKey.CoinInfo.selectedCurrency) sendRatesChangedNotification() } - - func setupCurrency() { - if - let id: String = securedStore.get(StoreKey.CoinInfo.selectedCurrency), + + fileprivate func setupCurrency() { + if let id: String = SecureStore.get(StoreKey.CoinInfo.selectedCurrency), let currency = Currency(rawValue: id) { currentCurrency = currency diff --git a/Adamant/Modules/InfoService/Services/InfoServiceApiCore.swift b/Adamant/Modules/InfoService/Services/InfoServiceApiCore.swift index 754724e78..9b6f09830 100644 --- a/Adamant/Modules/InfoService/Services/InfoServiceApiCore.swift +++ b/Adamant/Modules/InfoService/Services/InfoServiceApiCore.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation struct InfoServiceApiCore { let apiCore: APICoreProtocol @@ -30,7 +30,7 @@ extension InfoServiceApiCore: BlockchainHealthCheckableService { let startTimestamp = Date.now.timeIntervalSince1970 let statusResponse = await getNodeStatus(origin: origin) let ping = Date.now.timeIntervalSince1970 - startTimestamp - + return statusResponse.map { statusDto in mapper.mapToNodeStatusInfo( ping: ping, diff --git a/Adamant/Modules/InfoService/Services/InfoServiceMapper.swift b/Adamant/Modules/InfoService/Services/InfoServiceMapper.swift index 29f0ba3d3..a2a475d22 100644 --- a/Adamant/Modules/InfoService/Services/InfoServiceMapper.swift +++ b/Adamant/Modules/InfoService/Services/InfoServiceMapper.swift @@ -6,12 +6,12 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation struct InfoServiceMapper: InfoServiceMapperProtocol { private let currencies = Set(Currency.allCases.map { $0.rawValue }) - + func mapToModel(_ dto: InfoServiceStatusDTO) -> InfoServiceStatus { .init( lastUpdated: dto.last_updated.map { @@ -20,13 +20,13 @@ struct InfoServiceMapper: InfoServiceMapperProtocol { version: .init(dto.version) ?? .zero ) } - + func mapRatesToModel( _ dto: InfoServiceResponseDTO<[String: Decimal]> ) -> InfoServiceApiResult<[InfoServiceTicker: Decimal]> { mapResponseDTO(dto).map { mapToTickers($0) } } - + func mapToModel( _ dto: InfoServiceResponseDTO<[InfoServiceHistoryItemDTO]> ) -> InfoServiceApiResult { @@ -35,14 +35,16 @@ struct InfoServiceMapper: InfoServiceMapperProtocol { let item = $0.first, let tickers = item.tickers else { return .failure(.parsingError) } - - return .success(.init( - date: .init(timeIntervalSince1970: .init(milliseconds: item.date)), - tickers: mapToTickers(tickers) - )) + + return .success( + .init( + date: .init(timeIntervalSince1970: .init(milliseconds: item.date)), + tickers: mapToTickers(tickers) + ) + ) } } - + func mapToNodeStatusInfo( ping: TimeInterval, status: InfoServiceStatus @@ -55,11 +57,11 @@ struct InfoServiceMapper: InfoServiceMapperProtocol { version: status.version ) } - + func mapToRatesRequestDTO(_ coins: [String]) -> InfoServiceRatesRequestDTO { .init(coin: coins.joined(separator: ",")) } - + func mapToHistoryRequestDTO( date: Date, coin: String @@ -71,33 +73,33 @@ struct InfoServiceMapper: InfoServiceMapperProtocol { } } -private extension InfoServiceMapper { - func mapToTickers(_ rawTickers: [String: Decimal]) -> [InfoServiceTicker: Decimal] { +extension InfoServiceMapper { + fileprivate func mapToTickers(_ rawTickers: [String: Decimal]) -> [InfoServiceTicker: Decimal] { var dict = [InfoServiceTicker: Decimal]() - + for raw in rawTickers { guard let ticker = mapToTicker(raw.key) else { continue } dict[ticker] = raw.value } - + return dict } - - func mapToTicker(_ string: String) -> InfoServiceTicker? { + + fileprivate func mapToTicker(_ string: String) -> InfoServiceTicker? { let list: [String] = string.split(separator: "/").map { .init($0) } - + guard list.count == 2, let crypto = list.first, let fiat = list.last else { return nil } - + return currencies.contains(fiat) ? .init(crypto: crypto, fiat: fiat) : nil } - - func mapResponseDTO( + + fileprivate func mapResponseDTO( _ dto: InfoServiceResponseDTO ) -> InfoServiceApiResult { guard dto.success else { return .failure(.unknown) } diff --git a/Adamant/Modules/Login/EurekaPassphraseRow.swift b/Adamant/Modules/Login/EurekaPassphraseRow.swift index be4cfd7b3..30290b945 100644 --- a/Adamant/Modules/Login/EurekaPassphraseRow.swift +++ b/Adamant/Modules/Login/EurekaPassphraseRow.swift @@ -6,14 +6,14 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit import Eureka +import UIKit public class PassphraseCell: Cell, CellType { @IBOutlet weak var passphraseLabel: UILabel! @IBOutlet weak var tipLabel: UILabel! @IBOutlet weak var bottomConstrain: NSLayoutConstraint! - + var tipLabelIsHidden: Bool = false { didSet { if tipLabelIsHidden { @@ -25,7 +25,7 @@ public class PassphraseCell: Cell, CellType { } } } - + var passphrase: String? { get { return passphraseLabel.text @@ -34,7 +34,7 @@ public class PassphraseCell: Cell, CellType { passphraseLabel.text = newValue } } - + var tip: String? { get { return tipLabel.text @@ -43,7 +43,7 @@ public class PassphraseCell: Cell, CellType { tipLabel.text = newValue } } - + public override func update() { passphraseLabel.text = row.value } diff --git a/Adamant/Modules/Login/LoginFactory.swift b/Adamant/Modules/Login/LoginFactory.swift index f949a7edb..52b9bde3a 100644 --- a/Adamant/Modules/Login/LoginFactory.swift +++ b/Adamant/Modules/Login/LoginFactory.swift @@ -6,14 +6,14 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import Swinject import CommonKit +import Swinject +import UIKit @MainActor struct LoginFactory { let assembler: Assembler - + func makeViewController(screenFactory: ScreensFactory) -> LoginViewController { LoginViewController( accountService: assembler.resolve(AccountService.self)!, diff --git a/Adamant/Modules/Login/LoginViewController+Pinpad.swift b/Adamant/Modules/Login/LoginViewController+Pinpad.swift index 7a634aad1..0fc4dfff7 100644 --- a/Adamant/Modules/Login/LoginViewController+Pinpad.swift +++ b/Adamant/Modules/Login/LoginViewController+Pinpad.swift @@ -6,15 +6,15 @@ // Copyright © 2018 Adamant. All rights reserved. // +import CommonKit import Foundation import MyLittlePinpad -import CommonKit extension LoginViewController { /// Shows pinpad in main.async queue func loginWithPinpad() { let button: PinpadBiometryButtonType = accountService.useBiometry ? localAuth.biometryType.pinpadButtonType : .hidden - + DispatchQueue.main.async { [weak self] in let pinpad = PinpadViewController.adamantPinpad(biometryButton: button) pinpad.commentLabel.text = String.adamant.login.loginIntoPrevAccount @@ -34,57 +34,53 @@ extension LoginViewController { self?.present(pinpad, animated: true, completion: nil) } } - + /// Request user biometry authentication func loginWithBiometry() { let biometry = localAuth.biometryType - + guard biometry == .touchID || biometry == .faceID else { return } - - localAuth.authorizeUser(reason: .adamant.login.loginIntoPrevAccount) { result in - Task { @MainActor [weak self] in - switch result { - case .success: - self?.loginIntoSavedAccount() - - case .fallback: - self?.loginWithPinpad() - - case .cancel: - break - - case .failed: - break - } + + Task { @MainActor in + let result = await localAuth.authorizeUser(reason: .adamant.login.loginIntoPrevAccount) + switch result { + case .success: + self.loginIntoSavedAccount() + + case .fallback: + self.loginWithPinpad() + + case .cancel, .failed, .biometryLockout: + break } } } - + @MainActor private func loginIntoSavedAccount() { dialogService.showProgress(withMessage: String.adamant.login.loggingInProgressMessage, userInteractionEnable: false) - + Task { do { let result = try await accountService.loginWithStoredAccount() handleSavedAccountLoginResult(result) } catch { dialogService.showRichError(error: error) - + if let pinpad = presentedViewController as? PinpadViewController { pinpad.clearPin() } } } } - + private func handleSavedAccountLoginResult(_ result: AccountServiceResult) { switch result { case .success(_, let alert): dialogService.dismissProgress() - + let alertVc: UIAlertController? if let alert = alert { alertVc = UIAlertController(title: alert.title, message: alert.message, preferredStyleSafe: .alert, source: nil) @@ -92,20 +88,20 @@ extension LoginViewController { } else { alertVc = nil } - + guard let presenter = presentingViewController else { return } presenter.dismiss(animated: true, completion: nil) - + if let alertVc = alertVc { alertVc.modalPresentationStyle = .overFullScreen presenter.present(alertVc, animated: true, completion: nil) } - + case .failure(let error): dialogService.showRichError(error: error) - + if let pinpad = presentedViewController as? PinpadViewController { pinpad.clearPin() } @@ -120,31 +116,30 @@ extension LoginViewController: PinpadViewControllerDelegate { guard accountService.hasStayInAccount else { return } - + guard accountService.validatePin(pin) else { pinpad.clearPin() pinpad.playWrongPinAnimation() return } - + loginIntoSavedAccount() } } - + nonisolated func pinpadDidTapBiometryButton(_ pinpad: PinpadViewController) { Task { @MainActor in - localAuth.authorizeUser(reason: String.adamant.login.loginIntoPrevAccount, completion: { [weak self] result in - switch result { - case .success: - self?.loginIntoSavedAccount() - - case .fallback, .cancel, .failed: - break - } - }) + let result = await localAuth.authorizeUser(reason: String.adamant.login.loginIntoPrevAccount) + switch result { + case .success: + self.loginIntoSavedAccount() + + case .fallback, .cancel, .failed, .biometryLockout: + break + } } } - + nonisolated func pinpadDidCancel(_ pinpad: PinpadViewController) { Task { @MainActor in pinpad.dismiss(animated: true, completion: nil) diff --git a/Adamant/Modules/Login/LoginViewController+QR.swift b/Adamant/Modules/Login/LoginViewController+QR.swift index 7d08a303b..33c02ba0d 100644 --- a/Adamant/Modules/Login/LoginViewController+QR.swift +++ b/Adamant/Modules/Login/LoginViewController+QR.swift @@ -6,12 +6,12 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit import AVFoundation +import CommonKit +import EFQRCode import Photos @preconcurrency import QRCodeReader -import EFQRCode -import CommonKit +import UIKit extension LoginViewController { func loginWithQrFromCamera() { @@ -21,7 +21,7 @@ extension LoginViewController { reader.delegate = self reader.modalPresentationStyle = .overFullScreen present(reader, animated: true, completion: nil) - + case .notDetermined: AVCaptureDevice.requestAccess(for: .video) { [weak self] (granted: Bool) in if granted { @@ -35,19 +35,19 @@ extension LoginViewController { return } } - + case .restricted: let alert = UIAlertController(title: nil, message: String.adamant.login.cameraNotSupported, preferredStyleSafe: .alert, source: nil) alert.addAction(UIAlertAction(title: String.adamant.alert.ok, style: .cancel, handler: nil)) present(alert, animated: true, completion: nil) - + case .denied: dialogService.presentGoToSettingsAlert(title: nil, message: String.adamant.login.cameraNotAuthorized) @unknown default: break } } - + func loginWithQrFromLibrary() { let presenter: () -> Void = { [weak self] in let picker = UIImagePickerController() @@ -57,7 +57,7 @@ extension LoginViewController { picker.modalPresentationStyle = .overFullScreen self?.present(picker, animated: true, completion: nil) } - + presenter() } } @@ -73,12 +73,12 @@ extension LoginViewController: QRCodeReaderViewControllerDelegate { } return } - + reader.dismiss(animated: true, completion: nil) loginWith(passphrase: result.value) } } - + nonisolated func readerDidCancel(_ reader: QRCodeReaderViewController) { MainActor.assumeIsolatedSafe { reader.dismiss(animated: true, completion: nil) @@ -94,11 +94,11 @@ extension LoginViewController: UINavigationControllerDelegate, UIImagePickerCont dismiss(animated: true) { self.hidingImagePicker = false } - + guard let image = info[.originalImage] as? UIImage, let cgImage = image.cgImage else { return } - + let codes = EFQRCode.recognize(cgImage) if codes.count > 0 { for aCode in codes { @@ -107,7 +107,7 @@ extension LoginViewController: UINavigationControllerDelegate, UIImagePickerCont return } } - + dialogService.showWarning(withMessage: String.adamant.login.wrongQrError) } else { dialogService.showWarning(withMessage: String.adamant.login.noQrError) diff --git a/Adamant/Modules/Login/LoginViewController.swift b/Adamant/Modules/Login/LoginViewController.swift index fb5be3ee3..6665642c5 100644 --- a/Adamant/Modules/Login/LoginViewController.swift +++ b/Adamant/Modules/Login/LoginViewController.swift @@ -6,10 +6,10 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit +import CommonKit import Eureka import MarkdownKit -import CommonKit +import UIKit // MARK: - Localization extension String.adamant { @@ -30,13 +30,19 @@ extension String.adamant { String.localized("LoginScene.Error.NoInternet", comment: "Login: No network error.") } static var cameraNotAuthorized: String { - String.localized("LoginScene.Error.AuthorizeCamera", comment: "Login: Notify user, that he disabled camera in settings, and need to authorize application.") + String.localized( + "LoginScene.Error.AuthorizeCamera", + comment: "Login: Notify user, that he disabled camera in settings, and need to authorize application." + ) } static var cameraNotSupported: String { String.localized("LoginScene.Error.QrNotSupported", comment: "Login: Notify user that device not supported by QR reader") } static var photolibraryNotAuthorized: String { - String.localized("LoginScene.Error.AuthorizePhotolibrary", comment: "Login: User disabled access to photolibrary, he can authorize application in settings") + String.localized( + "LoginScene.Error.AuthorizePhotolibrary", + comment: "Login: User disabled access to photolibrary, he can authorize application in settings" + ) } static var emptyPassphraseAlert: String { String.localized("LoginScene.Error.NoPassphrase", comment: "Login: notify user that he is trying to login without a passphrase") @@ -46,23 +52,23 @@ extension String.adamant { // MARK: - ViewController final class LoginViewController: FormViewController { - + // MARK: Rows & Sections - + enum Sections { case login case newAccount - + var localized: String { switch self { case .login: return .localized("LoginScene.Section.Login", comment: "Login: login with existing passphrase section") - + case .newAccount: return .localized("LoginScene.Section.NewAccount", comment: "Login: Create new account section") } } - + var tag: String { switch self { case .login: return "loginSection" @@ -70,7 +76,7 @@ final class LoginViewController: FormViewController { } } } - + enum Rows { case passphrase case loginButton @@ -82,41 +88,44 @@ final class LoginViewController: FormViewController { case generateNewPassphraseButton case nodes case coinsNodes - + var localized: String { switch self { case .passphrase: return .localized("LoginScene.Row.Passphrase.Placeholder", comment: "Login: Passphrase placeholder") - + case .loginButton: return .localized("LoginScene.Row.Login", comment: "Login: Login button") - + case .loginWithQr: return .localized("LoginScene.Row.Qr", comment: "Login: Login with QR button.") - + case .loginWithPin: return .localized("LoginScene.Row.Pincode", comment: "Login: Login with pincode button") - + case .saveYourPassphraseAlert: - return .localized("LoginScene.Row.SavePassphraseAlert", comment: "Login: security alert, notify user that he must save his new passphrase. Markdown supported, center aligned.") - + return .localized( + "LoginScene.Row.SavePassphraseAlert", + comment: "Login: security alert, notify user that he must save his new passphrase. Markdown supported, center aligned." + ) + case .generateNewPassphraseButton: return .localized("LoginScene.Row.Generate", comment: "Login: generate new passphrase button") - + case .tapToSaveHint: return .localized("LoginScene.Row.TapToSave", comment: "Login: a small hint for a user, that he can tap on passphrase to save it") - + case .newPassphrase: return "" - + case .nodes: return .adamant.nodesList.nodesListButton - + case .coinsNodes: return .adamant.coinsNodesList.title } } - + var tag: String { switch self { case .passphrase: return "pass" @@ -132,28 +141,28 @@ final class LoginViewController: FormViewController { } } } - + // MARK: Dependencies - + let accountService: AccountService let adamantCore: AdamantCore let localAuth: LocalAuthentication let screensFactory: ScreensFactory let apiService: AdamantApiServiceProtocol let dialogService: DialogService - + // MARK: Properties private var hideNewPassphrase: Bool = true private var firstTimeActive: Bool = true internal var hidingImagePicker: Bool = false - + private lazy var versionFooterView = VersionFooterView() - + /// On launch, request user biometry (TouchID/FaceID) if has an account with biometry active var requestBiometryOnFirstTimeActive: Bool = true - + // MARK: Init - + init( accountService: AccountService, adamantCore: AdamantCore, @@ -168,209 +177,236 @@ final class LoginViewController: FormViewController { self.localAuth = localAuth self.screensFactory = screensFactory self.apiService = apiService - + super.init(nibName: nil, bundle: nil) } - + required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: Lifecycle - + override func viewDidLoad() { super.viewDidLoad() navigationOptions = RowNavigationOptions.Disabled tableView.tableFooterView = versionFooterView setVersion() - + // MARK: Header & Footer if let header = UINib(nibName: "LogoFullHeader", bundle: nil).instantiate(withOwner: nil, options: nil).first as? UIView { tableView.tableHeaderView = header - + if let label = header.viewWithTag(888) as? UILabel { label.text = String.adamant.shared.productName label.textColor = UIColor.adamant.primary } } - + // MARK: Login section - form +++ Section(Sections.login.localized) { - $0.tag = Sections.login.tag - - $0.footer = { [weak self] in - var footer = HeaderFooterView(.callback { - let view = ButtonsStripeView.adamantConfigured() - - var stripe: [StripeButtonType] = [.qrCameraReader, .qrPhotoReader] - - if let accountService = self?.accountService, - accountService.hasStayInAccount { - stripe.append(.pinpad) - if accountService.useBiometry, - let button = self?.localAuth.biometryType.stripeButtonType { - stripe.append(button) + form + +++ Section(Sections.login.localized) { + $0.tag = Sections.login.tag + + $0.footer = { [weak self] in + var footer = HeaderFooterView( + .callback { + let view = ButtonsStripeView.adamantConfigured() + + var stripe: [StripeButtonType] = [.qrCameraReader, .qrPhotoReader] + + if let accountService = self?.accountService, + accountService.hasStayInAccount + { + stripe.append(.pinpad) + if accountService.useBiometry, + let button = self?.localAuth.biometryType.stripeButtonType + { + stripe.append(button) + } + } + + view.stripe = stripe + view.delegate = self + + return view } + ) + + footer.height = { ButtonsStripeView.adamantDefaultHeight } + + return footer + }() + } + + // Passphrase row + <<< PasteInterceptingPasswordRow { + $0.tag = Rows.passphrase.tag + $0.placeholder = Rows.passphrase.localized + $0.placeholderColor = UIColor.adamant.secondary + $0.cell._textField.pasteInterceptor = { [weak self] text in + if let text { + self?.loginWith(passphrase: text) } - - view.stripe = stripe - view.delegate = self - - return view - }) - - footer.height = { ButtonsStripeView.adamantDefaultHeight } - - return footer - }() - } - - // Passphrase row - <<< PasswordRow { - $0.tag = Rows.passphrase.tag - $0.placeholder = Rows.passphrase.localized - $0.placeholderColor = UIColor.adamant.secondary - $0.cell.textField.enablePasteButtonAndPasswordToggle() - $0.keyboardReturnType = KeyboardReturnTypeConfiguration(nextKeyboardType: .go, defaultKeyboardType: .go) + } + $0.cell.textField.enablePasteButtonAndPasswordToggle { [weak self] in + // assing text to textfield like this to make loginButton update its enabled/disabled state + let row = self?.form.rowBy(tag: Rows.passphrase.tag) as? PasteInterceptingPasswordRow + row?.value = $0 + row?.cell.textField.text = $0 + self?.loginWith(passphrase: $0) + } + $0.keyboardReturnType = KeyboardReturnTypeConfiguration(nextKeyboardType: .go, defaultKeyboardType: .go) } - - // Login with passphrase row - <<< ButtonRow { - $0.tag = Rows.loginButton.tag - $0.title = Rows.loginButton.localized - $0.disabled = Condition.function([Rows.passphrase.tag], { form -> Bool in - guard let row: PasswordRow = form.rowBy(tag: Rows.passphrase.tag), row.value != nil else { - return true + + // Login with passphrase row + <<< ButtonRow { + $0.tag = Rows.loginButton.tag + $0.title = Rows.loginButton.localized + $0.disabled = Condition.function( + [Rows.passphrase.tag], + { form -> Bool in + guard let row: PasteInterceptingPasswordRow = form.rowBy(tag: Rows.passphrase.tag), row.value != nil else { + return true + } + return false + } + ) + }.onCellSelection { [weak self] (_, _) in + guard let row: PasteInterceptingPasswordRow = self?.form.rowBy(tag: Rows.passphrase.tag), + let passphrase = row.value + else { + return } - return false - }) - }.onCellSelection { [weak self] (_, row) in - guard let row: PasswordRow = self?.form.rowBy(tag: Rows.passphrase.tag), - let passphrase = row.value?.trimmingCharacters(in: .whitespaces) else { - return + + self?.loginWith(passphrase: passphrase) } - - self?.loginWith(passphrase: passphrase) - } - + // MARK: New account section - form +++ Section(Sections.newAccount.localized) { - $0.tag = Sections.newAccount.tag - } - - // Alert - <<< TextAreaRow { - $0.tag = Rows.saveYourPassphraseAlert.tag - $0.textAreaHeight = .dynamic(initialTextViewHeight: 44) - $0.hidden = Condition.function([], { [weak self] _ -> Bool in - return self?.hideNewPassphrase ?? false - }) - }.cellUpdate { (cell, _) in - cell.textView.textAlignment = .center - cell.textView.isSelectable = false - cell.textView.isEditable = false - - let parser = MarkdownParser(font: UIFont.systemFont(ofSize: UIFont.systemFontSize), color: UIColor.adamant.primary) - - let style = NSMutableParagraphStyle() - style.alignment = NSTextAlignment.center - - let mutableText = NSMutableAttributedString(attributedString: parser.parse(Rows.saveYourPassphraseAlert.localized)) - mutableText.addAttribute(NSAttributedString.Key.paragraphStyle, value: style, range: NSRange(location: 0, length: mutableText.length)) - - cell.textView.attributedText = mutableText - } - - // New genegated passphrase - <<< PassphraseRow { - $0.tag = Rows.newPassphrase.tag - $0.cell.tip = Rows.tapToSaveHint.localized - $0.cell.height = {96.0} - $0.hidden = Condition.function([], { [weak self] _ -> Bool in - return self?.hideNewPassphrase ?? true - }) - }.cellUpdate({ (cell, _) in - cell.passphraseLabel.font = UIFont.systemFont(ofSize: 19) - cell.passphraseLabel.textColor = UIColor.adamant.primary - cell.passphraseLabel.textAlignment = .center - - cell.tipLabel.font = UIFont.systemFont(ofSize: 12) - cell.tipLabel.textColor = UIColor.adamant.secondary - cell.tipLabel.textAlignment = .center - }).onCellSelection({ [weak self] (cell, row) in - guard let passphrase = row.value, let dialogService = self?.dialogService else { - return + form + +++ Section(Sections.newAccount.localized) { + $0.tag = Sections.newAccount.tag } - - if let indexPath = row.indexPath, let tableView = self?.tableView { - tableView.deselectRow(at: indexPath, animated: true) + + // Alert + <<< TextAreaRow { + $0.tag = Rows.saveYourPassphraseAlert.tag + $0.textAreaHeight = .dynamic(initialTextViewHeight: 44) + $0.hidden = Condition.function( + [], + { [weak self] _ -> Bool in + return self?.hideNewPassphrase ?? false + } + ) + }.cellUpdate { (cell, _) in + cell.textView.textAlignment = .center + cell.textView.isSelectable = false + cell.textView.isEditable = false + + let parser = MarkdownParser(font: UIFont.systemFont(ofSize: UIFont.systemFontSize), color: UIColor.adamant.primary) + + let style = NSMutableParagraphStyle() + style.alignment = NSTextAlignment.center + + let mutableText = NSMutableAttributedString(attributedString: parser.parse(Rows.saveYourPassphraseAlert.localized)) + mutableText.addAttribute(NSAttributedString.Key.paragraphStyle, value: style, range: NSRange(location: 0, length: mutableText.length)) + + cell.textView.attributedText = mutableText } - - let encodedPassphrase = AdamantUriTools.encode(request: AdamantUri.passphrase(passphrase: passphrase)) - - let didSelectAction: ((ShareType) -> Void)? = { [weak self] type in - guard case .copyToPasteboard = type else { + + // New genegated passphrase + <<< PassphraseRow { + $0.tag = Rows.newPassphrase.tag + $0.cell.tip = Rows.tapToSaveHint.localized + $0.cell.height = { 96.0 } + $0.hidden = Condition.function( + [], + { [weak self] _ -> Bool in + return self?.hideNewPassphrase ?? true + } + ) + }.cellUpdate({ (cell, _) in + cell.passphraseLabel.font = UIFont.systemFont(ofSize: 19) + cell.passphraseLabel.textColor = UIColor.adamant.primary + cell.passphraseLabel.textAlignment = .center + + cell.tipLabel.font = UIFont.systemFont(ofSize: 12) + cell.tipLabel.textColor = UIColor.adamant.secondary + cell.tipLabel.textAlignment = .center + }).onCellSelection({ [weak self] (cell, row) in + guard let passphrase = row.value, let dialogService = self?.dialogService else { return } - Task { @MainActor in - self?.tableView.scrollToBottom(animated: true) + if let indexPath = row.indexPath, let tableView = self?.tableView { + tableView.deselectRow(at: indexPath, animated: true) } + + let encodedPassphrase = AdamantUriTools.encode(request: AdamantUri.passphrase(passphrase: passphrase)) + + let didSelectAction: ((ShareType) -> Void)? = { [weak self] type in + guard case .copyToPasteboard = type else { + return + } + + Task { @MainActor in + self?.tableView.scrollToBottom(animated: true) + } + } + + dialogService.presentShareAlertFor( + string: passphrase, + types: [.copyToPasteboard, .share, .generateQr(encodedContent: encodedPassphrase, sharingTip: nil, withLogo: false)], + excludedActivityTypes: ShareContentType.passphrase.excludedActivityTypes, + animated: true, + from: nil, + completion: nil, + didSelect: didSelectAction + ) + }) + + <<< ButtonRow { + $0.tag = Rows.generateNewPassphraseButton.tag + $0.title = Rows.generateNewPassphraseButton.localized + }.onCellSelection { [weak self] (_, _) in + self?.generateNewPassphrase() } - - dialogService.presentShareAlertFor( - string: passphrase, - types: [.copyToPasteboard, .share, .generateQr(encodedContent: encodedPassphrase, sharingTip: nil, withLogo: false)], - excludedActivityTypes: ShareContentType.passphrase.excludedActivityTypes, - animated: true, - from: nil, - completion: nil, - didSelect: didSelectAction - ) - }) - - <<< ButtonRow { - $0.tag = Rows.generateNewPassphraseButton.tag - $0.title = Rows.generateNewPassphraseButton.localized - }.onCellSelection { [weak self] (_, _) in - self?.generateNewPassphrase() - } - + // MARK: Nodes list settings form +++ Section() - <<< ButtonRow { - $0.title = Rows.nodes.localized - $0.tag = Rows.nodes.tag - }.cellSetup { (cell, _) in - cell.selectionStyle = .gray - }.cellUpdate { (cell, _) in - cell.textLabel?.textColor = UIColor.adamant.primary - }.onCellSelection { [weak self] (_, _) in - guard let self = self else { return } - let vc = screensFactory.makeNodesList() - let nav = UINavigationController(rootViewController: vc) - nav.modalPresentationStyle = .overFullScreen - present(nav, animated: true, completion: nil) - } - - // MARK: Coins nodes list settings - <<< ButtonRow { - $0.title = Rows.coinsNodes.localized - $0.tag = Rows.coinsNodes.tag - }.cellSetup { (cell, _) in - cell.selectionStyle = .gray - }.cellUpdate { (cell, _) in - cell.textLabel?.textColor = UIColor.adamant.primary - }.onCellSelection { [weak self] (_, _) in - guard let self = self else { return } - let vc = screensFactory.makeCoinsNodesList(context: .login) - let nav = UINavigationController(rootViewController: vc) - nav.modalPresentationStyle = .overFullScreen - present(nav, animated: true, completion: nil) - } - + <<< ButtonRow { + $0.title = Rows.nodes.localized + $0.tag = Rows.nodes.tag + }.cellSetup { (cell, _) in + cell.selectionStyle = .gray + }.cellUpdate { (cell, _) in + cell.textLabel?.textColor = UIColor.adamant.primary + }.onCellSelection { [weak self] (_, _) in + guard let self = self else { return } + let vc = screensFactory.makeNodesList() + let nav = UINavigationController(rootViewController: vc) + nav.modalPresentationStyle = .overFullScreen + present(nav, animated: true, completion: nil) + } + + // MARK: Coins nodes list settings + <<< ButtonRow { + $0.title = Rows.coinsNodes.localized + $0.tag = Rows.coinsNodes.tag + }.cellSetup { (cell, _) in + cell.selectionStyle = .gray + }.cellUpdate { (cell, _) in + cell.textLabel?.textColor = UIColor.adamant.primary + }.onCellSelection { [weak self] (_, _) in + guard let self = self else { return } + let vc = screensFactory.makeCoinsNodesList(context: .login) + let nav = UINavigationController(rootViewController: vc) + nav.modalPresentationStyle = .overFullScreen + present(nav, animated: true, completion: nil) + } + // MARK: tableView position tuning - if let row: PasswordRow = form.rowBy(tag: Rows.passphrase.tag) { + if let row: PasteInterceptingPasswordRow = form.rowBy(tag: Rows.passphrase.tag) { NotificationCenter.default.addObserver( forName: UITextField.textDidBeginEditingNotification, object: row.cell.textField, @@ -380,12 +416,12 @@ final class LoginViewController: FormViewController { guard let tableView = self?.tableView, let indexPath = self?.form.rowBy(tag: Rows.loginButton.tag)?.indexPath else { return } - + tableView.scrollToRow(at: indexPath, at: .none, animated: true) } } } - + // MARK: Requesting biometry onActive NotificationCenter.default.addObserver( forName: UIApplication.didBecomeActiveNotification, @@ -397,23 +433,24 @@ final class LoginViewController: FormViewController { vc.firstTimeActive, vc.requestBiometryOnFirstTimeActive, vc.accountService.hasStayInAccount, - vc.accountService.useBiometry else { + vc.accountService.useBiometry + else { return } - + vc.loginWithBiometry() vc.firstTimeActive = false } } - + setColors() } - + override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() versionFooterView.sizeToFit() } - + // MARK: - FormViewController override func textInputShouldReturn(_ textInput: UITextInput, cell: Cell) -> Bool { @@ -426,12 +463,12 @@ final class LoginViewController: FormViewController { } // MARK: - Other - + private func setColors() { view.backgroundColor = UIColor.adamant.secondBackgroundColor tableView.backgroundColor = .clear } - + private func setVersion() { versionFooterView.model = .init( version: AdamantUtilities.applicationVersion, @@ -443,50 +480,51 @@ final class LoginViewController: FormViewController { // MARK: - Login functions extension LoginViewController { func loginWith(passphrase: String) { + let passphrase = passphrase.trimmingCharacters(in: .whitespaces) guard AdamantUtilities.validateAdamantPassphrase(passphrase: passphrase) else { dialogService.showWarning(withMessage: AccountServiceError.invalidPassphrase.localized) return } - + dialogService.showProgress(withMessage: String.adamant.login.loggingInProgressMessage, userInteractionEnable: false) - + Task { let result = await apiService.getAccount(byPassphrase: passphrase) - + switch result { case .success: loginIntoExistingAccount(passphrase: passphrase) - + case .failure(let error): dialogService.showRichError(error: error) } } } - + func generateNewPassphrase() { let passphrase = (try? Mnemonic.generate().joined(separator: " ")) ?? .empty - + hideNewPassphrase = false - + form.rowBy(tag: Rows.saveYourPassphraseAlert.tag)?.evaluateHidden() - + if let row: PassphraseRow = form.rowBy(tag: Rows.newPassphrase.tag) { row.value = passphrase row.updateCell() row.evaluateHidden() } - + if let row = form.rowBy(tag: Rows.generateNewPassphraseButton.tag), let indexPath = row.indexPath { tableView.scrollToRow(at: indexPath, at: .none, animated: true) } } - + @MainActor private func loginIntoExistingAccount(passphrase: String) { Task { do { - let result = try await accountService.loginWith(passphrase: passphrase) - + let result = try await accountService.loginWith(passphrase: passphrase, password: .empty) + if let nav = navigationController { nav.popViewController(animated: true) } else { @@ -494,9 +532,10 @@ extension LoginViewController { } dialogService.dismissProgress() - + if case .success(_, let alert) = result, - let alert = alert { + let alert = alert + { dialogService.showAlert(title: alert.title, message: alert.message, style: UIAlertController.Style.alert, actions: nil, from: nil) } } catch { @@ -513,26 +552,47 @@ extension LoginViewController: ButtonsStripeViewDelegate { switch button { case .pinpad: loginWithPinpad() - + case .touchID, .faceID: loginWithBiometry() - + case .qrCameraReader: loginWithQrFromCamera() - + case .qrPhotoReader: loginWithQrFromLibrary() } } } +// MARK: - Hardware keyboard handling + +extension LoginViewController { + override var canBecomeFirstResponder: Bool { + return true + } + + override func pressesBegan(_ presses: Set, with event: UIPressesEvent?) { + for press in presses { + guard let key = press.key else { continue } + if key.keyCode == UIKeyboardHIDUsage.keyboardReturnOrEnter { + if let passphrase = (form.rowBy(tag: Rows.passphrase.tag) as? PasswordRow)?.value { + return loginWith(passphrase: passphrase) + } + } + } + + super.pressesBegan(presses, with: event) + } +} + // MARK: UITextField + extensions -private extension UITextField { - func enablePasteButtonAndPasswordToggle() { +extension UITextField { + fileprivate func enablePasteButtonAndPasswordToggle(_ pasteButtonHandler: @escaping (String) -> Void) { let passwordToggleButton = makePasswordButton() - let pasteButton = makePasteButton() - + let pasteButton = makePasteButton(pasteButtonHandler) + let containerView = UIView() let buttonStack = UIStackView(arrangedSubviews: [pasteButton, passwordToggleButton]) buttonStack.axis = .horizontal @@ -541,34 +601,40 @@ private extension UITextField { buttonStack.snp.makeConstraints { make in make.directionalEdges.equalToSuperview() } - + containerView.snp.makeConstraints { make in make.height.equalTo(UITextField.buttonContainerHeight) } - + pasteButton.snp.makeConstraints { make in make.width.equalTo(pasteButton.snp.height) } - + passwordToggleButton.snp.makeConstraints { make in make.width.equalTo(passwordToggleButton.snp.height) } - + rightView = containerView rightViewMode = .always } - - func makePasteButton() -> UIButton { + + fileprivate func makePasteButton(_ handler: @escaping (String) -> Void) -> UIButton { let button = UIButton(type: .custom) button.imageEdgeInsets = UITextField.buttonImageEdgeInsets button.setImage(.asset(named: "clipboard"), for: .normal) - button.addTarget(self, action: #selector(pasteFromPasteboard(_:)), for: .touchUpInside) + button.addAction( + UIAction { [weak self] _ in + self?.pasteFromPasteboard(handler) + }, + for: .touchUpInside + ) return button } - - @objc func pasteFromPasteboard(_ sender: UIButton) { + + @objc fileprivate func pasteFromPasteboard(_ handler: @escaping (String) -> Void) { if let pasteboardText = UIPasteboard.general.string { - self.text = pasteboardText + let newText = String(pasteboardText.prefix(150)) + handler(newText) } } } diff --git a/Adamant/Modules/NodesEditor/NodeCell/NodeCell+Model.swift b/Adamant/Modules/NodesEditor/NodeCell/NodeCell+Model.swift index 723ee7278..3d8deea0a 100644 --- a/Adamant/Modules/NodesEditor/NodeCell/NodeCell+Model.swift +++ b/Adamant/Modules/NodesEditor/NodeCell/NodeCell+Model.swift @@ -6,12 +6,12 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit import CommonKit +import UIKit extension NodeCell { typealias NodeUpdateAction = (_ isEnabled: Bool) -> Void - + struct Model: Equatable { let id: UUID let title: String @@ -22,7 +22,7 @@ extension NodeCell { let statusColor: UIColor let isEnabled: Bool let nodeUpdateAction: IDWrapper - + static var `default`: Self { Self( id: .init(), diff --git a/Adamant/Modules/NodesEditor/NodeCell/NodeCell.swift b/Adamant/Modules/NodesEditor/NodeCell/NodeCell.swift index 0aff033a7..8aff5b00c 100644 --- a/Adamant/Modules/NodesEditor/NodeCell/NodeCell.swift +++ b/Adamant/Modules/NodesEditor/NodeCell/NodeCell.swift @@ -6,16 +6,16 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import SnapKit -import Eureka -import CommonKit import Combine +import CommonKit +import Eureka +import SnapKit +import UIKit final class NodeCell: Cell, CellType { private let checkmarkRowView = CheckmarkRowView() private var subscription: AnyCancellable? - + private var model: Model = .default { didSet { guard model != oldValue else { return } @@ -23,17 +23,17 @@ final class NodeCell: Cell, CellType { update() } } - + required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupView() } - + required init?(coder: NSCoder) { super.init(coder: coder) setupView() } - + override func update() { checkmarkRowView.setIsChecked(model.isEnabled, animated: true) checkmarkRowView.title = model.title @@ -43,26 +43,27 @@ final class NodeCell: Cell, CellType { checkmarkRowView.subtitle = model.statusString checkmarkRowView.subtitleColor = model.statusColor } - + func subscribe>(_ publisher: P) { - subscription = publisher + subscription = + publisher .removeDuplicates() .sink { [weak self] in self?.model = $0 } } } -private extension NodeCell { - func setupView() { +extension NodeCell { + fileprivate func setupView() { contentView.addSubview(checkmarkRowView) checkmarkRowView.snp.makeConstraints { $0.directionalEdges.equalToSuperview() } - + checkmarkRowView.checkmarkImage = .asset(named: "status_success") checkmarkRowView.onCheckmarkTap = { [weak self] in self?.onCheckmarkTap() } } - - func onCheckmarkTap() { + + fileprivate func onCheckmarkTap() { model.nodeUpdateAction.value(!checkmarkRowView.isChecked) } } diff --git a/Adamant/Modules/NodesEditor/NodeEditorViewController.swift b/Adamant/Modules/NodesEditor/NodeEditorViewController.swift index c92d640db..b7993d137 100644 --- a/Adamant/Modules/NodesEditor/NodeEditorViewController.swift +++ b/Adamant/Modules/NodesEditor/NodeEditorViewController.swift @@ -6,9 +6,9 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import Eureka import CommonKit +import Eureka +import UIKit // MARK: - Localization extension String.adamant { @@ -41,17 +41,17 @@ protocol NodeEditorDelegate: AnyObject { // MARK: - NodeEditorViewController final class NodeEditorViewController: FormViewController { // MARK: - Rows - + private enum Rows { // Node properties case host, port, scheme - + // Buttons case deleteButton - + // Rows case webSockets - + var localized: String { switch self { case .scheme: return .localized("NodesEditor.SchemeRow", comment: "NodesEditor: Scheme row") @@ -61,14 +61,14 @@ final class NodeEditorViewController: FormViewController { case .deleteButton: return .localized("NodesEditor.DeleteNodeButton", comment: "NodesEditor: Delete node button") } } - + var placeholder: String? { switch self { case .host: return .localized("NodesEditor.HostRow.Placeholder", comment: "NodesEditor: Host row placeholder") case .port, .scheme, .webSockets, .deleteButton: return nil } } - + var tag: String { switch self { case .scheme: return "prtcl" @@ -79,11 +79,11 @@ final class NodeEditorViewController: FormViewController { } } } - + private enum WebSocketsState { case supported case notSupported - + var localized: String { switch self { case .supported: return .localized("NodesEditor.WebSocketsSupported", comment: "NodesEditor: Web sockets are supported") @@ -91,123 +91,124 @@ final class NodeEditorViewController: FormViewController { } } } - + // MARK: - Dependencies var dialogService: DialogService! var apiService: AdamantApiServiceProtocol! var nodesStorage: NodesStorageProtocol! - + // MARK: - Properties var node: Node? - + weak var delegate: NodeEditorDelegate? private var didCallDelegate: Bool = false - + override var customNavigationAccessoryView: (UIView & NavigationAccessory)? { let accessory = NavigationAccessoryView() accessory.tintColor = UIColor.adamant.primary return accessory } - + // MARK: - Lifecycle - + override func viewDidLoad() { super.viewDidLoad() - + if let node = node { self.navigationItem.title = node.mainOrigin.host } else { self.navigationItem.title = String.adamant.nodesEditor.newNodeTitle } - + self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(saveNode)) self.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancel)) - + // MARK: - Node properties form +++ Section() - - // URL - <<< TextRow { - $0.title = Rows.host.localized - $0.tag = Rows.host.tag - $0.placeholder = Rows.host.placeholder - - $0.value = node?.mainOrigin.host - } - - // Port - <<< IntRow { - $0.title = Rows.port.localized - $0.tag = Rows.port.tag - - if let node = node { - $0.value = node.mainOrigin.port - $0.placeholder = String(node.mainOrigin.scheme.defaultPort) - } else { - $0.placeholder = String(NodeOrigin.URLScheme.default.defaultPort) + + // URL + <<< TextRow { + $0.title = Rows.host.localized + $0.tag = Rows.host.tag + $0.placeholder = Rows.host.placeholder + + $0.value = node?.mainOrigin.host } - } - - // Scheme - <<< PickerInlineRow { - $0.title = Rows.scheme.localized - $0.tag = Rows.scheme.tag - $0.value = node?.mainOrigin.scheme ?? NodeOrigin.URLScheme.default - $0.options = [.https, .http] - $0.baseCell.detailTextLabel?.textColor = .adamant.textColor - }.onExpandInlineRow { (cell, _, inlineRow) in - inlineRow.cell.height = { 100 } - }.onChange { [weak self] row in - if let portRow: IntRow = self?.form.rowBy(tag: Rows.port.tag) { - if let scheme = row.value { - portRow.placeholder = String(scheme.defaultPort) + + // Port + <<< IntRow { + $0.title = Rows.port.localized + $0.tag = Rows.port.tag + + if let node = node { + $0.value = node.mainOrigin.port + $0.placeholder = String(node.mainOrigin.scheme.defaultPort) } else { - portRow.placeholder = String(NodeOrigin.URLScheme.default.defaultPort) + $0.placeholder = String(NodeOrigin.URLScheme.default.defaultPort) } - - portRow.updateCell() } - } - + + // Scheme + <<< PickerInlineRow { + $0.title = Rows.scheme.localized + $0.tag = Rows.scheme.tag + $0.value = node?.mainOrigin.scheme ?? NodeOrigin.URLScheme.default + $0.options = [.https, .http] + $0.baseCell.detailTextLabel?.textColor = .adamant.textColor + }.onExpandInlineRow { (cell, _, inlineRow) in + inlineRow.cell.height = { 100 } + }.onChange { [weak self] row in + if let portRow: IntRow = self?.form.rowBy(tag: Rows.port.tag) { + if let scheme = row.value { + portRow.placeholder = String(scheme.defaultPort) + } else { + portRow.placeholder = String(NodeOrigin.URLScheme.default.defaultPort) + } + + portRow.updateCell() + } + } + // MARK: - WebSockets - + if let wsEnabled = node?.wsEnabled { form +++ Section() - <<< LabelRow { - $0.title = Rows.webSockets.localized - $0.tag = Rows.webSockets.tag - $0.baseValue = wsEnabled - ? WebSocketsState.supported.localized - : WebSocketsState.notSupported.localized - } + <<< LabelRow { + $0.title = Rows.webSockets.localized + $0.tag = Rows.webSockets.tag + $0.baseValue = + wsEnabled + ? WebSocketsState.supported.localized + : WebSocketsState.notSupported.localized + } } - + // MARK: - Delete - + if node != nil { form +++ Section() - <<< ButtonRow { - $0.title = Rows.deleteButton.localized - $0.tag = Rows.deleteButton.tag - }.onCellSelection { [weak self] (_, _) in - self?.deleteNode() - } + <<< ButtonRow { + $0.title = Rows.deleteButton.localized + $0.tag = Rows.deleteButton.tag + }.onCellSelection { [weak self] (_, _) in + self?.deleteNode() + } } - + setColors() } - + override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) - + if !didCallDelegate { saveNode() } } - + // MARK: - Other - + private func setColors() { view.backgroundColor = UIColor.adamant.secondBackgroundColor tableView.backgroundColor = .clear @@ -222,26 +223,25 @@ extension NodeEditorViewController { delegate?.nodeEditorViewController(self, didFinishEditingWithResult: .cancel) return } - + let host = rawUrl.trimmingCharacters(in: .whitespaces) let scheme: NodeOrigin.URLScheme - - if - let row = form.rowBy(tag: Rows.scheme.tag), + + if let row = form.rowBy(tag: Rows.scheme.tag), let value = row.baseValue as? NodeOrigin.URLScheme { scheme = value } else { scheme = .default } - + let port: Int? if let row: IntRow = form.rowBy(tag: Rows.port.tag), let p = row.value { port = p } else { port = nil } - + let result: NodeEditorResult if let node = node { nodesStorage.updateNode(id: node.id, group: .adm) { node in @@ -249,49 +249,57 @@ extension NodeEditorViewController { node.mainOrigin.host = host node.mainOrigin.port = port } - + result = .nodeUpdated } else { - result = .new(node: .init( - id: .init(), - isEnabled: true, - wsEnabled: false, - mainOrigin: .init( - scheme: scheme, - host: host, - port: port - ), - altOrigin: nil, - version: nil, - height: nil, - ping: nil, - connectionStatus: nil, - preferMainOrigin: nil, - type: .custom - )) + result = .new( + node: .init( + id: .init(), + isEnabled: true, + wsEnabled: false, + mainOrigin: .init( + scheme: scheme, + host: host, + port: port + ), + altOrigin: nil, + version: nil, + height: nil, + ping: nil, + connectionStatus: nil, + preferMainOrigin: nil, + type: .custom + ) + ) } - + didCallDelegate = true delegate?.nodeEditorViewController(self, didFinishEditingWithResult: result) } - + @objc private func cancel() { didCallDelegate = true delegate?.nodeEditorViewController(self, didFinishEditingWithResult: .cancel) } - + private func deleteNode() { let alert = UIAlertController(title: String.adamant.nodesEditor.deleteNodeAlert, message: nil, preferredStyleSafe: .alert, source: nil) alert.addAction(UIAlertAction(title: String.adamant.alert.cancel, style: .cancel, handler: nil)) - alert.addAction(UIAlertAction(title: String.adamant.alert.delete, style: .destructive, handler: { _ in - self.didCallDelegate = true - - if let node = self.node { - self.delegate?.nodeEditorViewController(self, didFinishEditingWithResult: .delete(node: node)) - } else { - self.delegate?.nodeEditorViewController(self, didFinishEditingWithResult: .cancel) - } - })) + alert.addAction( + UIAlertAction( + title: String.adamant.alert.delete, + style: .destructive, + handler: { _ in + self.didCallDelegate = true + + if let node = self.node { + self.delegate?.nodeEditorViewController(self, didFinishEditingWithResult: .delete(node: node)) + } else { + self.delegate?.nodeEditorViewController(self, didFinishEditingWithResult: .cancel) + } + } + ) + ) alert.modalPresentationStyle = .overFullScreen present(alert, animated: true, completion: nil) } diff --git a/Adamant/Modules/NodesEditor/NodesEditorFactory.swift b/Adamant/Modules/NodesEditor/NodesEditorFactory.swift index e9f590569..c05878e02 100644 --- a/Adamant/Modules/NodesEditor/NodesEditorFactory.swift +++ b/Adamant/Modules/NodesEditor/NodesEditorFactory.swift @@ -6,18 +6,18 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import Swinject import CommonKit +import Swinject +import UIKit @MainActor struct NodesEditorFactory { let assembler: Assembler - + func makeNodesListVC(screensFactory: ScreensFactory) -> UIViewController { NodesListViewController( dialogService: assembler.resolve(DialogService.self)!, - securedStore: assembler.resolve(SecuredStore.self)!, + SecureStore: assembler.resolve(SecureStore.self)!, screensFactory: screensFactory, nodesStorage: assembler.resolve(NodesStorageProtocol.self)!, nodesAdditionalParamsStorage: assembler.resolve(NodesAdditionalParamsStorageProtocol.self)!, @@ -25,7 +25,7 @@ struct NodesEditorFactory { socketService: assembler.resolve(SocketService.self)! ) } - + func makeNodeEditorVC() -> NodeEditorViewController { let c = NodeEditorViewController() c.dialogService = assembler.resolve(DialogService.self) diff --git a/Adamant/Modules/NodesEditor/NodesListViewController.swift b/Adamant/Modules/NodesEditor/NodesListViewController.swift index 591ddf133..6488e66c2 100644 --- a/Adamant/Modules/NodesEditor/NodesListViewController.swift +++ b/Adamant/Modules/NodesEditor/NodesListViewController.swift @@ -6,10 +6,10 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import Eureka -import CommonKit import Combine +import CommonKit +import Eureka +import UIKit // MARK: - Localization extension String.adamant { @@ -20,15 +20,15 @@ extension String.adamant { static var nodesListButton: String { String.localized("NodesList.NodesList", comment: "NodesList: Button label") } - + static var defaultNodesWasLoaded: String { String.localized("NodeList.DefaultNodesLoaded", comment: "NodeList: Inform that default nodes was loaded, if user deleted all nodes") } - + static var resetAlertTitle: String { String.localized("NodesList.ResetNodeListAlert", comment: "NodesList: Reset nodes alert title") } - + static var fastestNodeModeTip: String { String.localized( "NodesList.PreferTheFastestNode.Footer", @@ -41,12 +41,12 @@ extension String.adamant { // MARK: - NodesListViewController final class NodesListViewController: FormViewController { // Rows & Sections - + private enum Sections { case nodes case buttons case preferTheFastestNode - + var tag: String { switch self { case .nodes: return "nds" @@ -55,55 +55,55 @@ final class NodesListViewController: FormViewController { } } } - + private enum Rows { case addNode case save case reset case preferTheFastestNode - + var localized: String { switch self { case .addNode: return .localized("NodesList.AddNewNode", comment: "NodesList: 'Add new node' button lable") - + case .save: return String.adamant.alert.save - + case .reset: return .localized("NodesList.ResetButton", comment: "NodesList: 'Reset' button") - + case .preferTheFastestNode: return .localized("NodesList.PreferTheFastestNode", comment: "NodesList: 'Prefer the fastest node' switch") } } } - + // MARK: Dependencies - + private let dialogService: DialogService - private let securedStore: SecuredStore + private let SecureStore: SecureStore private let screensFactory: ScreensFactory private let nodesStorage: NodesStorageProtocol private let nodesAdditionalParamsStorage: NodesAdditionalParamsStorageProtocol private let apiService: AdamantApiServiceProtocol private let socketService: SocketService - + // Properties - + @ObservableValue private var nodesList = [Node]() @ObservableValue private var currentSocketsNodeId: UUID? @ObservableValue private var chosenRestNodeId: UUID? - + private var nodesHaveBeenDisplayed = false private var timerSubsctiption: AnyCancellable? private var subscriptions = Set() - + // MARK: - Lifecycle - + init( dialogService: DialogService, - securedStore: SecuredStore, + SecureStore: SecureStore, screensFactory: ScreensFactory, nodesStorage: NodesStorageProtocol, nodesAdditionalParamsStorage: NodesAdditionalParamsStorageProtocol, @@ -111,7 +111,7 @@ final class NodesListViewController: FormViewController { socketService: SocketService ) { self.dialogService = dialogService - self.securedStore = securedStore + self.SecureStore = SecureStore self.screensFactory = screensFactory self.nodesStorage = nodesStorage self.nodesAdditionalParamsStorage = nodesAdditionalParamsStorage @@ -119,93 +119,94 @@ final class NodesListViewController: FormViewController { self.socketService = socketService super.init(style: .insetGrouped) } - + required init?(coder aDecoder: NSCoder) { fatalError("Isn't implemented") } - + override func viewDidLoad() { super.viewDidLoad() navigationItem.title = String.adamant.nodesList.title navigationOptions = .Disabled navigationItem.largeTitleDisplayMode = .never - + if splitViewController == nil, navigationController?.viewControllers.count == 1 { let done = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(NodesListViewController.close)) navigationItem.rightBarButtonItem = done } - + // MARK: Nodes - - form +++ Section { - $0.tag = Sections.nodes.tag - } - - // MARK: Prefer the fastest node - - +++ Section { - $0.tag = Sections.preferTheFastestNode.tag - $0.footer = HeaderFooterView(stringLiteral: .adamant.nodesList.fastestNodeModeTip) - } - - <<< SwitchRow { [nodesAdditionalParamsStorage] in - $0.title = Rows.preferTheFastestNode.localized - $0.value = nodesAdditionalParamsStorage.isFastestNodeMode( - group: nodeGroup - ) - }.onChange { [nodesAdditionalParamsStorage] in - nodesAdditionalParamsStorage.setFastestNodeMode( - group: nodeGroup, - value: $0.value ?? true - ) - }.cellUpdate { cell, _ in - cell.switchControl.onTintColor = .adamant.active - } - - // MARK: Buttons - - +++ Section { - $0.tag = Sections.buttons.tag - } - - // Add node - <<< ButtonRow { - $0.title = Rows.addNode.localized - }.cellSetup { (cell, _) in - cell.selectionStyle = .gray - }.onCellSelection { [weak self] (_, _) in - self?.createNewNode() - } - - // Reset - <<< ButtonRow { - $0.title = Rows.reset.localized - }.onCellSelection { [weak self] (_, _) in - self?.resetToDefault() - } - + + form + +++ Section { + $0.tag = Sections.nodes.tag + } + + // MARK: Prefer the fastest node + + +++ Section { + $0.tag = Sections.preferTheFastestNode.tag + $0.footer = HeaderFooterView(stringLiteral: .adamant.nodesList.fastestNodeModeTip) + } + + <<< SwitchRow { [nodesAdditionalParamsStorage] in + $0.title = Rows.preferTheFastestNode.localized + $0.value = nodesAdditionalParamsStorage.isFastestNodeMode( + group: nodeGroup + ) + }.onChange { [nodesAdditionalParamsStorage] in + nodesAdditionalParamsStorage.setFastestNodeMode( + group: nodeGroup, + value: $0.value ?? true + ) + }.cellUpdate { cell, _ in + cell.switchControl.onTintColor = .adamant.active + } + + // MARK: Buttons + + +++ Section { + $0.tag = Sections.buttons.tag + } + + // Add node + <<< ButtonRow { + $0.title = Rows.addNode.localized + }.cellSetup { (cell, _) in + cell.selectionStyle = .gray + }.onCellSelection { [weak self] (_, _) in + self?.createNewNode() + } + + // Reset + <<< ButtonRow { + $0.title = Rows.reset.localized + }.onCellSelection { [weak self] (_, _) in + self?.resetToDefault() + } + setColors() setupObservers() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) apiService.healthCheck() setHealthCheckTimer() } - + // MARK: - Other - + private func setColors() { view.backgroundColor = UIColor.adamant.secondBackgroundColor tableView.backgroundColor = .clear } - + private func setupObservers() { apiService.nodesInfoPublisher .sink { [weak self] in self?.setNewNodesList($0) } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .SocketService.currentNodeUpdate, object: nil) .sink { @MainActor [weak self] _ in @@ -214,14 +215,14 @@ final class NodesListViewController: FormViewController { self?.currentSocketsNodeId = newId } .store(in: &subscriptions) - + currentSocketsNodeId = socketService.currentNode?.id } - + private func setNewNodesList(_ newNodes: NodesListInfo) { nodesList = newNodes.nodes chosenRestNodeId = newNodes.chosenNodeId - + if !nodesHaveBeenDisplayed { UIView.performWithoutAnimation { remakeNodesRows() @@ -229,7 +230,7 @@ final class NodesListViewController: FormViewController { } else { remakeNodesRows() } - + nodesHaveBeenDisplayed = true } } @@ -239,37 +240,37 @@ extension NodesListViewController { func createNewNode() { presentEditor(forNode: nil) } - + func addNode(node: Node) { getNodesSection()?.append(createRowFor(nodeId: node.id, tag: generateRandomTag())) nodesStorage.addNode(node, group: nodeGroup) } - + func removeNode(nodeId: UUID) { guard let index = getNodeIndex(nodeId: nodeId) else { return } - + getNodesSection()?.remove(at: index) nodesStorage.removeNode(id: nodeId, group: .adm) } - + func getNodeIndex(nodeId: UUID) -> Int? { displayedNodesIds.firstIndex { $0 == nodeId } } - + func getNodesSection() -> Section? { form.sectionBy(tag: Sections.nodes.tag) } - + var displayedNodesIds: [UUID] { getNodesSection()?.allRows.compactMap { ($0.baseValue as? NodeCell.Model)?.id } ?? [] } - + func editNode(_ node: Node) { presentEditor(forNode: node) } - + @objc func close() { if self.navigationController?.viewControllers.count == 1 { self.dismiss(animated: true, completion: nil) @@ -277,32 +278,34 @@ extension NodesListViewController { self.navigationController?.popViewController(animated: true) } } - + func resetToDefault(silent: Bool = false) { if silent { nodesStorage.resetNodes([nodeGroup]) return } - + let alert = UIAlertController(title: String.adamant.nodesList.resetAlertTitle, message: nil, preferredStyleSafe: .alert, source: nil) alert.addAction(UIAlertAction(title: String.adamant.alert.cancel, style: .cancel, handler: nil)) - alert.addAction(UIAlertAction( - title: Rows.reset.localized, - style: .destructive, - handler: { [weak self] _ in self?.nodesStorage.resetNodes([nodeGroup]) } - )) + alert.addAction( + UIAlertAction( + title: Rows.reset.localized, + style: .destructive, + handler: { [weak self] _ in self?.nodesStorage.resetNodes([nodeGroup]) } + ) + ) alert.modalPresentationStyle = .overFullScreen present(alert, animated: true, completion: nil) } - + func remakeNodesRows() { guard let nodesSection = getNodesSection(), displayedNodesIds != nodesList.map({ $0.id }) else { return } - + nodesSection.removeAll() - + for node in nodesList { let row = createRowFor(nodeId: node.id, tag: generateRandomTag()) nodesSection.append(row) @@ -321,7 +324,7 @@ extension NodesListViewController: NodeEditorDelegate { case .nodeUpdated, .cancel: break } - + if UIScreen.main.traitCollection.userInterfaceIdiom == .pad { navigationController?.popToViewController(self, animated: true) } else { @@ -335,7 +338,7 @@ extension NodesListViewController: NodeEditorDelegate { extension NodesListViewController { func loadDefaultNodes(showAlert: Bool) { nodesStorage.resetNodes([nodeGroup]) - + if showAlert { dialogService.showSuccess(withMessage: String.adamant.nodesList.defaultNodesWasLoaded) } @@ -348,17 +351,17 @@ extension NodesListViewController { let row = NodeRow { $0.cell.subscribe(makeNodeCellPublisher(nodeId: nodeId)) $0.tag = tag - + let deleteAction = SwipeAction( style: .destructive, title: "Delete" ) { [weak self] _, row, completionHandler in defer { completionHandler?(true) } - + guard let model = row.baseValue as? NodeCell.Model else { return } self?.removeNode(nodeId: model.id) } - + $0.trailingSwipe.actions = [deleteAction] $0.trailingSwipe.performsFirstActionWithFullSwipe = true }.cellUpdate { (cell, _) in @@ -367,21 +370,21 @@ extension NodesListViewController { } }.onCellSelection { [weak self] (_, row) in defer { row.deselect(animated: true) } - + guard let self = self, let node = self.nodesList.first(where: { $0.id == row.value?.id }) else { return } - + self.editNode(node) } - + return row } - + private func presentEditor(forNode node: Node?) { let editor = screensFactory.makeNodeEditor() - + editor.delegate = self editor.node = node if UIScreen.main.traitCollection.userInterfaceIdiom == .pad { @@ -392,26 +395,27 @@ extension NodesListViewController { present(navigator, animated: true, completion: nil) } } - + private func generateRandomTag() -> String { let capacity = 6 var nums: [UInt32] = [] nums.reserveCapacity(capacity) - + for _ in 0...capacity { nums.append(arc4random_uniform(10)) } - + return nums.compactMap { String($0) }.joined() } - + private func setHealthCheckTimer() { - timerSubsctiption = Timer + timerSubsctiption = + Timer .publish(every: nodeGroup.onScreenUpdateInterval, on: .main, in: .default) .autoconnect() .sink { [apiService] _ in apiService.healthCheck() } } - + private func makeNodeCellModel(node: Node) -> NodeCell.Model { .init( id: node.id, @@ -433,19 +437,19 @@ extension NodesListViewController { } ) } - + private func makeNodeCellPublisher(nodeId: UUID) -> some Observable { $nodesList.combineLatest( $currentSocketsNodeId, $chosenRestNodeId ).compactMap { [weak self] tuple in let nodes = tuple.0 - + guard let self = self, let node = nodes.first(where: { $0.id == nodeId }) else { return nil } - + return self.makeNodeCellModel(node: node) } } diff --git a/Adamant/Modules/Onboard/EulaViewController.swift b/Adamant/Modules/Onboard/EulaViewController.swift index a67c939ce..d0d6b95a3 100644 --- a/Adamant/Modules/Onboard/EulaViewController.swift +++ b/Adamant/Modules/Onboard/EulaViewController.swift @@ -9,11 +9,11 @@ import UIKit final class EulaViewController: UIViewController { - + // MARK: Outlets @IBOutlet weak var eulaTextView: UITextView! @IBOutlet var buttons: [UIButton]! - + var onAccept: (() -> Void)? var onDecline: (() -> Void)? @@ -23,9 +23,9 @@ final class EulaViewController: UIViewController { setColors() } - + // MARK: - Other - + private func setColors() { buttons.forEach { btn in btn.setTitleColor(UIColor.adamant.textColor, for: .normal) @@ -33,16 +33,16 @@ final class EulaViewController: UIViewController { eulaTextView.textColor = UIColor.adamant.textColor view.backgroundColor = UIColor.adamant.welcomeBackgroundColor } - + // MARK: - Actions - + @IBAction func handleAccept() { DispatchQueue.main.async { [weak self] in self?.onAccept?() self?.dismiss(animated: true, completion: nil) } } - + @IBAction func handleDecline() { DispatchQueue.main.async { [weak self] in self?.onDecline?() diff --git a/Adamant/Modules/Onboard/OnboardFactory.swift b/Adamant/Modules/Onboard/OnboardFactory.swift index 938c9136d..6a934c29d 100644 --- a/Adamant/Modules/Onboard/OnboardFactory.swift +++ b/Adamant/Modules/Onboard/OnboardFactory.swift @@ -6,15 +6,15 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UIKit import Swinject +import UIKit @MainActor struct OnboardFactory { func makeOnboardVC() -> UIViewController { OnboardViewController(nibName: "OnboardViewController", bundle: nil) } - + func makeEulaVC() -> UIViewController { EulaViewController(nibName: "EulaViewController", bundle: nil) } diff --git a/Adamant/Modules/Onboard/OnboardOverlay.swift b/Adamant/Modules/Onboard/OnboardOverlay.swift index c983971f1..d061c1e98 100644 --- a/Adamant/Modules/Onboard/OnboardOverlay.swift +++ b/Adamant/Modules/Onboard/OnboardOverlay.swift @@ -6,60 +6,63 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UIKit import CommonKit +import UIKit final class OnboardOverlay: SwiftyOnboardOverlay { - + lazy var agreeSwitch: UISwitch = { let view = UISwitch() view.isOn = false view.onTintColor = UIColor.adamant.switchColor return view }() - + lazy var agreeLabel: UILabel = { let view = UILabel() view.text = " \(String.adamant.Onboard.agreeLabel)" - view.font = UIFont.adamantPrimary(ofSize: 18)//UIFont(name: "Exo2-Regular", size: 18) + view.font = UIFont.adamantPrimary(ofSize: 18) //UIFont(name: "Exo2-Regular", size: 18) view.textColor = UIColor.adamant.textColor return view }() - + lazy var eulaButton: UIButton = { let button = UIButton(type: .system) button.contentHorizontalAlignment = .center - - let attrs = NSAttributedString(string: String.adamant.Onboard.eulaTitle, - attributes: - [NSAttributedString.Key.foregroundColor: UIColor.adamant.textColor, - NSAttributedString.Key.font: UIFont(name: "Exo2-Regular", size: 18) ?? UIFont.adamantPrimary(ofSize: 18), - NSAttributedString.Key.underlineColor: UIColor.adamant.textColor, - NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue]) + + let attrs = NSAttributedString( + string: String.adamant.Onboard.eulaTitle, + attributes: [ + NSAttributedString.Key.foregroundColor: UIColor.adamant.textColor, + NSAttributedString.Key.font: UIFont(name: "Exo2-Regular", size: 18) ?? UIFont.adamantPrimary(ofSize: 18), + NSAttributedString.Key.underlineColor: UIColor.adamant.textColor, + NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue + ] + ) button.setAttributedTitle(attrs, for: .normal) return button }() - + func configure() { let margin = self.layoutMarginsGuide - + let stack = UIStackView(arrangedSubviews: [agreeSwitch, agreeLabel, eulaButton]) stack.alignment = .fill stack.distribution = .fill stack.spacing = 4 - + self.addSubview(stack) stack.translatesAutoresizingMaskIntoConstraints = false stack.heightAnchor.constraint(equalTo: agreeSwitch.heightAnchor).isActive = true stack.bottomAnchor.constraint(equalTo: continueButton.topAnchor, constant: -20).isActive = true stack.centerXAnchor.constraint(equalTo: margin.centerXAnchor).isActive = true } - + } -private extension String.adamant { - enum Onboard { +extension String.adamant { + fileprivate enum Onboard { static var agreeLabel: String { String.localized("WelcomeScene.Description.Accept", comment: "Welcome: Description accept") } diff --git a/Adamant/Modules/Onboard/OnboardPage.swift b/Adamant/Modules/Onboard/OnboardPage.swift index 6be25d660..80c1bd885 100644 --- a/Adamant/Modules/Onboard/OnboardPage.swift +++ b/Adamant/Modules/Onboard/OnboardPage.swift @@ -6,55 +6,55 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import MarkdownKit import CommonKit -import SnapKit +import MarkdownKit import SafariServices +import SnapKit +import UIKit final class OnboardPage: SwiftyOnboardPage { - + private lazy var mainImageView = UIImageView(image: image) - + private lazy var textView: UITextView = { let textView = UITextView() textView.textColor = UIColor.adamant.active textView.backgroundColor = .clear textView.isEditable = false textView.delegate = self - + let attributedString = NSMutableAttributedString( attributedString: MarkdownParser().parse(self.text) ) - + attributedString.apply(font: UIFont.adamantPrimary(ofSize: 18), alignment: .center) textView.attributedText = attributedString - textView.linkTextAttributes = [NSAttributedString.Key.foregroundColor : UIColor.adamant.active] - + textView.linkTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.adamant.active] + return textView }() - + private let image: UIImage? private let text: String - + var tapURLCompletion: ((URL) -> Void)? - + init(image: UIImage?, text: String) { self.image = image self.text = text super.init(frame: .zero) - + setupView() } - + required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + private func setupView() { addSubview(mainImageView) addSubview(textView) - + let space = UIScreen.main.bounds.height / 1.7 mainImageView.contentMode = .scaleAspectFit mainImageView.snp.makeConstraints { make in @@ -62,7 +62,7 @@ final class OnboardPage: SwiftyOnboardPage { make.top.equalTo(safeAreaLayoutGuide).offset(50) make.bottom.equalTo(safeAreaLayoutGuide).offset(-space) } - + textView.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview().inset(20) make.bottom.equalToSuperview().offset(-150) diff --git a/Adamant/Modules/Onboard/OnboardViewController.swift b/Adamant/Modules/Onboard/OnboardViewController.swift index 0c0abaff7..4ae0a7ec9 100644 --- a/Adamant/Modules/Onboard/OnboardViewController.swift +++ b/Adamant/Modules/Onboard/OnboardViewController.swift @@ -6,22 +6,22 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import SafariServices import CommonKit +import SafariServices +import UIKit private class OnboardingPageItem { var image: UIImage var text: String - + init(image: UIImage, text: String) { self.image = image self.text = text } } -fileprivate extension String.adamant { - enum Onboard { +extension String.adamant { + fileprivate enum Onboard { static var beginButton: String { String.localized("WelcomeScene.Description.BeginButton", comment: "Welcome: Last slide Begin button") } @@ -35,71 +35,81 @@ fileprivate extension String.adamant { } final class OnboardViewController: UIViewController { - + // MARK: Constants private static let titleFont = UIFont.adamantPrimary(ofSize: 18) private static let buttonsFont = UIFont.adamantPrimary(ofSize: 16, weight: .bold) private static let themeColor = UIColor.adamant.primary - + // MARK: Outlets @IBOutlet weak var onboarding: SwiftyOnboard! weak var agreeSwitch: UISwitch? - + // MARK: Properties fileprivate let items = [ - OnboardingPageItem(image: .asset(named: "SlideImage1") ?? .init(), - text: .localized("WelcomeScene.Description.Slide1", comment: "Welcome: Slide 1 Description")), + OnboardingPageItem( + image: .asset(named: "SlideImage1") ?? .init(), + text: .localized("WelcomeScene.Description.Slide1", comment: "Welcome: Slide 1 Description") + ), - OnboardingPageItem(image: .asset(named: "SlideImage2") ?? .init(), - text: .localized("WelcomeScene.Description.Slide2", comment: "Welcome: Slide 2 Description")), + OnboardingPageItem( + image: .asset(named: "SlideImage2") ?? .init(), + text: .localized("WelcomeScene.Description.Slide2", comment: "Welcome: Slide 2 Description") + ), - OnboardingPageItem(image: .asset(named: "SlideImage3") ?? .init(), - text: .localized("WelcomeScene.Description.Slide3", comment: "Welcome: Slide 3 Description")), + OnboardingPageItem( + image: .asset(named: "SlideImage3") ?? .init(), + text: .localized("WelcomeScene.Description.Slide3", comment: "Welcome: Slide 3 Description") + ), - OnboardingPageItem(image: .asset(named: "SlideImage4") ?? .init(), - text: .localized("WelcomeScene.Description.Slide4", comment: "Welcome: Slide 4 Description")), + OnboardingPageItem( + image: .asset(named: "SlideImage4") ?? .init(), + text: .localized("WelcomeScene.Description.Slide4", comment: "Welcome: Slide 4 Description") + ), - OnboardingPageItem(image: .asset(named: "SlideImage5") ?? .init(), - text: .localized("WelcomeScene.Description.Slide5", comment: "Welcome: Slide 5 Description")) - ] + OnboardingPageItem( + image: .asset(named: "SlideImage5") ?? .init(), + text: .localized("WelcomeScene.Description.Slide5", comment: "Welcome: Slide 5 Description") + ) + ] override func viewDidLoad() { super.viewDidLoad() - + onboarding.delegate = self onboarding.dataSource = self setColors() } - + // MARK: - Other - + private func setColors() { agreeSwitch?.onTintColor = UIColor.adamant.switchColor onboarding.backgroundColor = UIColor.adamant.welcomeBackgroundColor view.backgroundColor = UIColor.adamant.welcomeBackgroundColor } - + // MARK: - Actions - + @objc func handleSkip() { guard self.agreeSwitch?.isOn == true else { handleEula(true) return } - + UserDefaults.standard.set(true, forKey: StoreKey.application.eulaAccepted) - + DispatchQueue.main.async { [weak self] in self?.dismiss(animated: true, completion: nil) } } - + @objc func handleContinue() { DispatchQueue.main.async { [weak self] in guard let onboarding = self?.onboarding else { return } - + if let count = self?.items.count, onboarding.currentPage == count - 1 { self?.handleSkip() } else { @@ -107,7 +117,7 @@ final class OnboardViewController: UIViewController { } } } - + @objc func handleEula(_ skip: Bool = false) { DispatchQueue.main.async { [weak self] in let eula = EulaViewController(nibName: "EulaViewController", bundle: nil) @@ -130,47 +140,47 @@ final class OnboardViewController: UIViewController { // MARK: SwiftyOnboard Delegate & DataSource extension OnboardViewController: SwiftyOnboardDelegate, SwiftyOnboardDataSource { - + func swiftyOnboardNumberOfPages(_ swiftyOnboard: SwiftyOnboard) -> Int { return items.count } - + func swiftyOnboardPageForIndex(_ swiftyOnboard: SwiftyOnboard, index: Int) -> SwiftyOnboardPage? { let item = items[index] - + let view = OnboardPage(image: item.image, text: item.text) view.tapURLCompletion = { [weak self] url in self?.openURL(url) } return view } - + func swiftyOnboardViewForOverlay(_ swiftyOnboard: SwiftyOnboard) -> SwiftyOnboardOverlay? { let overlay = OnboardOverlay(frame: .zero) overlay.configure() - + //Setup targets for the buttons on the overlay view: overlay.skipButton.addTarget(self, action: #selector(handleSkip), for: .touchUpInside) overlay.continueButton.addTarget(self, action: #selector(handleContinue), for: .touchUpInside) - + agreeSwitch = overlay.agreeSwitch - + //Setup for the overlay buttons: overlay.continueButton.titleLabel?.font = OnboardViewController.buttonsFont overlay.continueButton.setTitle(String.adamant.Onboard.continueButton, for: .normal) - + overlay.skipButton.titleLabel?.font = OnboardViewController.buttonsFont overlay.skipButton.setTitle(String.adamant.Onboard.skipButton, for: .normal) - + overlay.eulaButton.addTarget(self, action: #selector(handleEula), for: .touchUpInside) - + return overlay } - + func swiftyOnboardOverlayForPosition(_ swiftyOnboard: SwiftyOnboard, overlay: SwiftyOnboardOverlay, for position: Double) { let currentPage = Int(round(position)) overlay.pageControl.currentPage = currentPage - + if currentPage == items.count - 1 { overlay.skipButton.isHidden = true overlay.continueButton.setTitle(String.adamant.Onboard.beginButton, for: .normal) @@ -181,8 +191,8 @@ extension OnboardViewController: SwiftyOnboardDelegate, SwiftyOnboardDataSource } } -private extension OnboardViewController { - func openURL(_ url: URL) { +extension OnboardViewController { + fileprivate func openURL(_ url: URL) { let safari = SFSafariViewController(url: url) safari.preferredControlTintColor = UIColor.adamant.primary safari.modalPresentationStyle = .overFullScreen diff --git a/Adamant/Modules/PartnerQR/PartnerQRFactory.swift b/Adamant/Modules/PartnerQR/PartnerQRFactory.swift index e0eb35022..cfa0f206b 100644 --- a/Adamant/Modules/PartnerQR/PartnerQRFactory.swift +++ b/Adamant/Modules/PartnerQR/PartnerQRFactory.swift @@ -6,30 +6,30 @@ // Copyright © 2023 Adamant. All rights reserved. // -import Swinject -import SwiftUI import CommonKit +import SwiftUI +import Swinject @MainActor struct PartnerQRFactory { private let parent: Assembler private let assemblies = [PartnerQRAssembly()] - + init(parent: Assembler) { self.parent = parent } - + @MainActor - func makeViewController(partner: CoreDataAccount) -> UIViewController { + func makeViewController(partner: CoreDataAccount, screenFactory: ScreensFactory) -> UIViewController { let assembler = Assembler(assemblies, parent: parent) - + let viewModel = { let viewModel = assembler.resolver.resolve(PartnerQRViewModel.self)! viewModel.setup(partner: partner) return viewModel } - - return UIHostingController(rootView: PartnerQRView(viewModel: viewModel)) + + return UIHostingController(rootView: PartnerQRView(viewModel: viewModel, screenFactory: screenFactory)) } } @@ -37,16 +37,17 @@ private struct PartnerQRAssembly: MainThreadAssembly { func assembleOnMainThread(container: Container) { container.register(PartnerQRService.self) { r in AdamantPartnerQRService( - securedStore: r.resolve(SecuredStore.self)! + SecureStore: r.resolve(SecureStore.self)! ) }.inObjectScope(.container) - + container.register(PartnerQRViewModel.self) { PartnerQRViewModel( dialogService: $0.resolve(DialogService.self)!, addressBookService: $0.resolve(AddressBookService.self)!, avatarService: $0.resolve(AvatarService.self)!, - partnerQRService: $0.resolve(PartnerQRService.self)! + partnerQRService: $0.resolve(PartnerQRService.self)!, + accountService: $0.resolve(AccountService.self)! ) }.inObjectScope(.transient) } diff --git a/Adamant/Modules/PartnerQR/PartnerQRView.swift b/Adamant/Modules/PartnerQR/PartnerQRView.swift index f872bf394..6a1ca64ce 100644 --- a/Adamant/Modules/PartnerQR/PartnerQRView.swift +++ b/Adamant/Modules/PartnerQR/PartnerQRView.swift @@ -9,8 +9,9 @@ import SwiftUI struct PartnerQRView: View { + let screenFactory: ScreensFactory @ObservedObject var viewModel: PartnerQRViewModel - + var body: some View { GeometryReader { geometry in Form { @@ -25,15 +26,21 @@ struct PartnerQRView: View { } } } + .fullScreenCover(isPresented: $viewModel.presentBuyAndSell) { + screenFactory.makeBuyAndSellView(action: { + viewModel.presentBuyAndSell = false + }) + } } - - init(viewModel: @escaping () -> PartnerQRViewModel) { + + init(viewModel: @escaping () -> PartnerQRViewModel, screenFactory: ScreensFactory) { _viewModel = .init(wrappedValue: viewModel()) + self.screenFactory = screenFactory } } -private extension PartnerQRView { - func toolbar(maxWidth: CGFloat) -> some View { +extension PartnerQRView { + fileprivate func toolbar(maxWidth: CGFloat) -> some View { Button(action: viewModel.renameContact) { HStack { if let uiImage = viewModel.partnerImage { @@ -49,8 +56,8 @@ private extension PartnerQRView { .frame(maxWidth: maxWidth - toolbarSpace, alignment: .center) } } - - func infoSection() -> some View { + + fileprivate func infoSection() -> some View { Section { if let uiImage = viewModel.image { HStack { @@ -62,21 +69,24 @@ private extension PartnerQRView { Spacer() } } - + HStack { Spacer() - Button(action: { - viewModel.copyToPasteboard() - }, label: { - Text(viewModel.title) - .padding() - }) + Button( + action: { + viewModel.copyToPasteboard() + }, + label: { + Text(viewModel.title) + .padding() + } + ) Spacer() } } } - - func toggleSection() -> some View { + + fileprivate func toggleSection() -> some View { Section { Toggle(String.adamant.partnerQR.includePartnerName, isOn: $viewModel.includeContactsName) .disabled(!viewModel.includeContactsNameEnabled) @@ -84,7 +94,7 @@ private extension PartnerQRView { .onChange(of: viewModel.includeContactsName) { _ in viewModel.didToggle() } - + Toggle(String.adamant.partnerQR.includePartnerURL, isOn: $viewModel.includeWebAppLink) .tint(.init(uiColor: .adamant.active)) .onChange(of: viewModel.includeWebAppLink) { _ in @@ -92,17 +102,17 @@ private extension PartnerQRView { } } } - - func buttonSection() -> some View { + + fileprivate func buttonSection() -> some View { Section { Button(viewModel.renameTitle) { viewModel.renameContact() } - + Button(String.adamant.alert.saveToPhotolibrary) { viewModel.saveToPhotos() } - + Button(String.adamant.alert.share) { viewModel.share() } diff --git a/Adamant/Modules/PartnerQR/PartnerQRViewModel.swift b/Adamant/Modules/PartnerQR/PartnerQRViewModel.swift index 66588476a..740c01d81 100644 --- a/Adamant/Modules/PartnerQR/PartnerQRViewModel.swift +++ b/Adamant/Modules/PartnerQR/PartnerQRViewModel.swift @@ -6,10 +6,10 @@ // Copyright © 2023 Adamant. All rights reserved. // -import SwiftUI import Combine import CommonKit import Photos +import SwiftUI @MainActor final class PartnerQRViewModel: NSObject, ObservableObject { @@ -20,65 +20,76 @@ final class PartnerQRViewModel: NSObject, ObservableObject { @Published var renameTitle: String = "" @Published var includeWebAppLink = false @Published var includeContactsName = false - + @Published var presentBuyAndSell = false + private var partner: CoreDataAccount? private let dialogService: DialogService private let addressBookService: AddressBookService private let avatarService: AvatarService private let partnerQRService: PartnerQRService + private let accountService: AccountService private var subscriptions = Set() - + let partnerImageSize: CGFloat = 25 - + var title: String { partner?.address ?? "" } - + init( dialogService: DialogService, addressBookService: AddressBookService, avatarService: AvatarService, - partnerQRService: PartnerQRService + partnerQRService: PartnerQRService, + accountService: AccountService ) { + self.accountService = accountService self.dialogService = dialogService self.addressBookService = addressBookService self.avatarService = avatarService self.partnerQRService = partnerQRService } - + func setup(partner: CoreDataAccount) { self.partner = partner updatePartnerInfo() generateQR() } - + func renameContact() { - guard let alert = self.makeRenameAlert(for: self.title) else { return } + let alert = dialogService.makeRenameAlert( + titleFormat: String(format: .adamant.chat.actionsBody, self.title), + initialText: self.addressBookService.getName(for: self.title) ?? self.partnerName, + isEnoughMoney: accountService.account?.isEnoughMoneyForTransaction ?? false, + url: accountService.account?.address, + showVC: showBuyAndSell, + onRename: handleRename(newName:) + ) self.dialogService.present(alert, animated: true) { self.dialogService.selectAllTextFields(in: alert) } } - + func saveToPhotos() { guard let qrCode = image else { return } - + switch PHPhotoLibrary.authorizationStatus() { case .authorized, .limited: UIImageWriteToSavedPhotosAlbum( qrCode, self, - #selector(image(_: didFinishSavingWithError: contextInfo:)), + #selector(image(_:didFinishSavingWithError:contextInfo:)), nil ) - + case .notDetermined: UIImageWriteToSavedPhotosAlbum( qrCode, self, - #selector(image(_: didFinishSavingWithError: contextInfo:)), + #selector(image(_:didFinishSavingWithError:contextInfo:)), nil ) - + case .restricted, .denied: dialogService.presentGoToSettingsAlert( title: nil, @@ -96,10 +107,10 @@ final class PartnerQRViewModel: NSObject, ObservableObject { activityItems: [qrCode], applicationActivities: nil ) - + vc.completionWithItemsHandler = { [weak self] (_: UIActivity.ActivityType?, completed: Bool, _, error: Error?) in guard completed else { return } - + if let error = error { self?.dialogService.showWarning(withMessage: error.localizedDescription) } else { @@ -109,32 +120,32 @@ final class PartnerQRViewModel: NSObject, ObservableObject { vc.modalPresentationStyle = .overFullScreen dialogService.present(vc, animated: true, completion: nil) } - + func didToggle() { partnerQRService.setIncludeURLEnabled(includeWebAppLink) partnerQRService.setIncludeNameEnabled(includeContactsName) generateQR() } - + func copyToPasteboard() { UIPasteboard.general.string = title dialogService.showToastMessage(.adamant.alert.copiedToPasteboardNotification) } } -private extension PartnerQRViewModel { - func updatePartnerInfo() { +extension PartnerQRViewModel { + fileprivate func updatePartnerInfo() { guard let publicKey = partner?.publicKey, - let address = partner?.address + let address = partner?.address else { includeContactsNameEnabled = false includeContactsName = false includeWebAppLink = false return } - + let name = addressBookService.getName(for: partner) - + if let name = name { partnerName = name includeContactsNameEnabled = true @@ -146,11 +157,11 @@ private extension PartnerQRViewModel { includeContactsName = false renameTitle = .adamant.alert.renameContactInitial } - + includeWebAppLink = partnerQRService.isIncludeURLEnabled() - + guard let avatarName = partner?.avatar, - let avatar = UIImage.asset(named: avatarName) + let avatar = UIImage.asset(named: avatarName) else { partnerImage = avatarService.avatar( for: publicKey, @@ -158,41 +169,46 @@ private extension PartnerQRViewModel { ) return } - + partnerImage = avatar } - - func generateQR() { + + fileprivate func generateQR() { guard let address = partner?.address else { return } - + var params: [AdamantAddressParam] = [] - + let name = addressBookService.getName(for: partner) - + if includeContactsName, - let name = name { + let name = name + { params.append(.label(name)) } - + var data: String = address - + if includeWebAppLink { - data = AdamantUriTools.encode(request: AdamantUri.address( - address: address, - params: params - )) + data = AdamantUriTools.encode( + request: AdamantUri.address( + address: address, + params: params + ) + ) } else { - data = AdamantUriTools.encode(request: AdamantUri.addressLegacy( - address: address, - params: params - )) + data = AdamantUriTools.encode( + request: AdamantUri.addressLegacy( + address: address, + params: params + ) + ) } - + let qr = AdamantQRTools.generateQrFrom( string: data, withLogo: true ) - + switch qr { case .success(let uIImage): image = uIImage @@ -200,7 +216,7 @@ private extension PartnerQRViewModel { dialogService.showError(withMessage: "", supportEmail: false, error: error) } } - + @objc private func image( _ image: UIImage, didFinishSavingWithError error: NSError?, @@ -210,47 +226,22 @@ private extension PartnerQRViewModel { dialogService.presentGoToSettingsAlert(title: String.adamant.shared.photolibraryNotAuthorized, message: nil) return } - + dialogService.showSuccess(withMessage: String.adamant.alert.done) } - func makeRenameAlert(for address: String) -> UIAlertController? { - let alert = UIAlertController( - title: .init(format: .adamant.chat.actionsBody, address), - message: nil, - preferredStyleSafe: .alert, - source: nil - ) - - alert.addTextField { [weak self] textField in - textField.placeholder = .adamant.chat.name - textField.autocapitalizationType = .words - textField.text = self?.addressBookService.getName(for: address) ?? self?.partnerName - } - - let renameAction = UIAlertAction( - title: .adamant.chat.rename, - style: .default - ) { [weak self] _ in - guard - let textField = alert.textFields?.first, - let newName = textField.text - else { return } - - Task { - self?.partnerName = newName - self?.renameTitle = .adamant.alert.renameContact - await self?.addressBookService.set(name: newName, for: address) - } + fileprivate func makeCancelAction() -> UIAlertAction { + .init(title: .adamant.alert.cancel, style: .cancel, handler: nil) + } + fileprivate func handleRename(newName: String) { + Task { [weak self] in + guard let self else { return } + self.partnerName = newName + self.renameTitle = .adamant.alert.renameContact + await self.addressBookService.set(name: newName, for: self.title) } - - alert.addAction(renameAction) - alert.addAction(makeCancelAction()) - alert.modalPresentationStyle = .overFullScreen - return alert } - - func makeCancelAction() -> UIAlertAction { - .init(title: .adamant.alert.cancel, style: .cancel, handler: nil) + fileprivate func showBuyAndSell() { + presentBuyAndSell = true } } diff --git a/Adamant/Modules/ScreensFactory/AdamantScreensFactory.swift b/Adamant/Modules/ScreensFactory/AdamantScreensFactory.swift index 0bb351920..16fcd16e9 100644 --- a/Adamant/Modules/ScreensFactory/AdamantScreensFactory.swift +++ b/Adamant/Modules/ScreensFactory/AdamantScreensFactory.swift @@ -6,9 +6,9 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit -import Swinject import SwiftUI +import Swinject +import UIKit @MainActor struct AdamantScreensFactory: ScreensFactory { @@ -25,7 +25,7 @@ struct AdamantScreensFactory: ScreensFactory { private let onboardFactory: OnboardFactory private let shareQRFactory: ShareQRFactory private let accountFactory: AccountFactory - private let vibrationSelectionFactory: VibrationSelectionFactory + private let settingSelectionFactory: SettingSelectionFactory private let partnerQRFactory: PartnerQRFactory private let coinsNodesListFactory: CoinsNodesListFactory private let chatSelectTextFactory: ChatSelectTextViewFactory @@ -33,7 +33,7 @@ struct AdamantScreensFactory: ScreensFactory { private let notificationSoundsFactory: NotificationSoundsFactory private let storageUsageFactory: StorageUsageFactory private let pkGeneratorFactory: PKGeneratorFactory - + init(assembler: Assembler) { admWalletFactory = .init(assembler: assembler) chatListFactory = .init(assembler: assembler) @@ -46,7 +46,7 @@ struct AdamantScreensFactory: ScreensFactory { onboardFactory = .init() shareQRFactory = .init(assembler: assembler) accountFactory = .init(assembler: assembler) - vibrationSelectionFactory = .init(parent: assembler) + settingSelectionFactory = .init(parent: assembler) partnerQRFactory = .init(parent: assembler) coinsNodesListFactory = .init(parent: assembler) chatSelectTextFactory = .init() @@ -54,7 +54,7 @@ struct AdamantScreensFactory: ScreensFactory { notificationSoundsFactory = .init(parent: assembler) storageUsageFactory = .init(parent: assembler) pkGeneratorFactory = .init(parent: assembler) - + walletFactoryCompose = AdamantWalletFactoryCompose( klyWalletFactory: .init(assembler: assembler), dogeWalletFactory: .init(assembler: assembler), @@ -65,143 +65,146 @@ struct AdamantScreensFactory: ScreensFactory { admWalletFactory: admWalletFactory ) } - + func makeWalletVC(service: WalletService) -> WalletViewController { walletFactoryCompose.makeWalletVC(service: service, screensFactory: self) } - + func makeTransferListVC(service: WalletService) -> UIViewController { walletFactoryCompose.makeTransferListVC(service: service, screenFactory: self) } - + func makeTransferVC(service: WalletService) -> TransferViewControllerBase { walletFactoryCompose.makeTransferVC(service: service, screenFactory: self) } - + func makeDetailsVC(service: WalletService) -> TransactionDetailsViewControllerBase { walletFactoryCompose.makeDetailsVC(service: service) } - + func makeDetailsVC(service: WalletService, transaction: RichMessageTransaction) -> UIViewController? { walletFactoryCompose.makeDetailsVC(service: service, transaction: transaction) } - + func makeAdmTransactionDetails(transaction: TransferTransaction) -> UIViewController { admWalletFactory.makeDetailsVC(transaction: transaction, screensFactory: self) } - + func makeAdmTransactionDetails() -> AdmTransactionDetailsViewController { admWalletFactory.makeDetailsVC(screensFactory: self) } - + func makeBuyAndSell() -> UIViewController { - admWalletFactory.makeBuyAndSellVC() + admWalletFactory.makeBuyAndSellVC(screenFactory: self) } - + func makeBuyAndSellView(action: @escaping () -> Void) -> AnyView { + admWalletFactory.makeBuyAndSellView(screenFactory: self, action: action) + } + func makeChatList() -> UIViewController { chatListFactory.makeChatListVC(screensFactory: self) } - + func makeChat() -> ChatViewController { chatFactory.makeViewController(screensFactory: self) } - + func makeNewChat() -> NewChatViewController { chatListFactory.makeNewChatVC(screensFactory: self) } - + func makeDelegatesList() -> UIViewController { delegatesFactory.makeDelegatesListVC(screensFactory: self) } - + func makeDelegateDetails() -> DelegateDetailsViewController { delegatesFactory.makeDelegateDetails() } - + func makeNodesList() -> UIViewController { nodesEditorFactory.makeNodesListVC(screensFactory: self) } - + func makeNodeEditor() -> NodeEditorViewController { nodesEditorFactory.makeNodeEditorVC() } - + func makeEula() -> UIViewController { onboardFactory.makeEulaVC() } - + func makeOnboard() -> UIViewController { onboardFactory.makeOnboardVC() } - + func makeShareQr() -> ShareQrViewController { shareQRFactory.makeViewController() } - + func makeAccount() -> UIViewController { accountFactory.makeViewController(screensFactory: self) } - + func makeComplexTransfer() -> UIViewController { chatListFactory.makeComplexTransferVC(screensFactory: self) } - + func makeSearchResults() -> SearchResultsViewController { chatListFactory.makeSearchResultsViewController(screensFactory: self) } - + func makeSecurity() -> UIViewController { settingsFactory.makeSecurityVC(screensFactory: self) } - + func makeQRGenerator() -> UIViewController { settingsFactory.makeQRGeneratorVC() } - + func makePKGenerator() -> UIViewController { pkGeneratorFactory.makeViewController() } - + func makeAbout() -> UIViewController { settingsFactory.makeAboutVC(screensFactory: self) } - + func makeNotifications() -> UIViewController { - notificationsFactory.makeViewController() + notificationsFactory.makeViewController(screensFactory: self) } - + func makeNotificationSounds(target: NotificationTarget) -> NotificationSoundsView { notificationSoundsFactory.makeView(target: target) } - + func makeVisibleWallets() -> UIViewController { settingsFactory.makeVisibleWalletsVC() } - + func makeContribute() -> UIViewController { contributeFactory.makeViewController() } - + func makeStorageUsage() -> UIViewController { storageUsageFactory.makeViewController() } - + func makeLogin() -> LoginViewController { loginFactory.makeViewController(screenFactory: self) } - - func makeVibrationSelection() -> UIViewController { - vibrationSelectionFactory.makeViewController() + + func makeVibrationSelection(onSettingsSelect: @escaping (SettingsView.SettingsType) -> Void) -> UIViewController { + settingSelectionFactory.makeViewController(onSettingsSelect: onSettingsSelect) } - + func makePartnerQR(partner: CoreDataAccount) -> UIViewController { - partnerQRFactory.makeViewController(partner: partner) + partnerQRFactory.makeViewController(partner: partner, screenFactory: self) } - + func makeCoinsNodesList(context: CoinsNodesListContext) -> UIViewController { coinsNodesListFactory.makeViewController(context: context) } - + func makeChatSelectTextView(text: String) -> UIViewController { chatSelectTextFactory.makeViewController(text: text) } diff --git a/Adamant/Modules/ScreensFactory/ScreensFactory.swift b/Adamant/Modules/ScreensFactory/ScreensFactory.swift index 5b6f27f62..5b494c173 100644 --- a/Adamant/Modules/ScreensFactory/ScreensFactory.swift +++ b/Adamant/Modules/ScreensFactory/ScreensFactory.swift @@ -6,49 +6,48 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit import SwiftUI @MainActor protocol ScreensFactory { // MARK: Wallets - + func makeWalletVC(service: WalletService) -> WalletViewController func makeTransferListVC(service: WalletService) -> UIViewController func makeTransferVC(service: WalletService) -> TransferViewControllerBase func makeDetailsVC(service: WalletService) -> TransactionDetailsViewControllerBase - + func makeDetailsVC( service: WalletService, transaction: RichMessageTransaction ) -> UIViewController? - + func makeAdmTransactionDetails(transaction: TransferTransaction) -> UIViewController func makeAdmTransactionDetails() -> AdmTransactionDetailsViewController func makeBuyAndSell() -> UIViewController - + // MARK: Chats - + func makeChat() -> ChatViewController func makeChatList() -> UIViewController func makeNewChat() -> NewChatViewController func makeComplexTransfer() -> UIViewController func makeSearchResults() -> SearchResultsViewController func makeChatSelectTextView(text: String) -> UIViewController - + // MARK: Delegates - + func makeDelegatesList() -> UIViewController func makeDelegateDetails() -> DelegateDetailsViewController - + // MARK: Nodes - + func makeNodesList() -> UIViewController func makeNodeEditor() -> NodeEditorViewController func makeCoinsNodesList(context: CoinsNodesListContext) -> UIViewController - + // MARK: Other - + func makeEula() -> UIViewController func makeOnboard() -> UIViewController func makeShareQr() -> ShareQrViewController @@ -62,7 +61,8 @@ protocol ScreensFactory { func makeContribute() -> UIViewController func makeStorageUsage() -> UIViewController func makeLogin() -> LoginViewController - func makeVibrationSelection() -> UIViewController + func makeVibrationSelection(onSettingsSelect: @escaping (SettingsView.SettingsType) -> Void) -> UIViewController func makePartnerQR(partner: CoreDataAccount) -> UIViewController func makeNotificationSounds(target: NotificationTarget) -> NotificationSoundsView + func makeBuyAndSellView(action: @escaping () -> Void) -> AnyView } diff --git a/Adamant/Modules/Settings/AboutViewController.swift b/Adamant/Modules/Settings/AboutViewController.swift index d8da353e1..c8464649b 100644 --- a/Adamant/Modules/Settings/AboutViewController.swift +++ b/Adamant/Modules/Settings/AboutViewController.swift @@ -6,11 +6,11 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation +import CommonKit import Eureka -import SafariServices +import Foundation import MessageUI -import CommonKit +import SafariServices // MARK: - Localization extension String.adamant { @@ -18,7 +18,7 @@ extension String.adamant { static var title: String { String.localized("About.Title", comment: "About page: scene title") } - + static func commit(_ commit: String) -> String { String.localizedStringWithFormat( String.localized( @@ -34,18 +34,18 @@ extension String.adamant { // MARK: - AboutViewController final class AboutViewController: FormViewController { // MARK: Section & Rows - + enum Sections { case about case contactUs - + var tag: String { switch self { case .about: return "about" case .contactUs: return "contact" } } - + var localized: String { switch self { case .about: return .localized("About.Section.About", comment: "About scene: 'Read about' section title.") @@ -53,11 +53,11 @@ final class AboutViewController: FormViewController { } } } - + enum Rows { case website, whitepaper, blog, github, welcomeScreens case adm, email, twitter, vibration - + var tag: String { switch self { case .website: return "www" @@ -71,7 +71,7 @@ final class AboutViewController: FormViewController { case .vibration: return "vibration" } } - + var localized: String { switch self { case .website: return .localized("About.Row.Website", comment: "About scene: Website row") @@ -82,10 +82,10 @@ final class AboutViewController: FormViewController { case .email: return .localized("About.Row.WriteUs", comment: "About scene: Write us row") case .blog: return .localized("About.Row.Blog", comment: "About scene: Our blog row") case .twitter: return .localized("About.Row.Twitter", comment: "About scene: Twitter row") - case .vibration: return "Vibrations" + case .vibration: return "Developer" } } - + var localizedUrl: String { switch self { case .website: return .localized("About.Row.Website.Url", comment: "About scene: Website localized url") @@ -93,12 +93,12 @@ final class AboutViewController: FormViewController { case .github: return .localized("About.Row.GitHub.Url", comment: "About scene: Project's GitHub page localized url") case .blog: return .localized("About.Row.Blog.Url", comment: "About scene: Our blog localized url") case .twitter: return .localized("About.Row.Twitter.Url", comment: "About scene: Twitter localized url") - + // No urls case .adm, .email, .welcomeScreens, .vibration: return "" } } - + var image: UIImage? { switch self { case .whitepaper: return .asset(named: "row_whitepapper") @@ -109,29 +109,29 @@ final class AboutViewController: FormViewController { case .website: return .asset(named: "row_website") case .welcomeScreens: return .asset(named: "row_logo") case .twitter: return .asset(named: "row_twitter") - case .vibration: return .asset(named: "row_vibration") + case .vibration: return .asset(named: "row_crashlytics") } } } - + // MARK: Dependencies - + private let accountService: AccountService private let accountsProvider: AccountsProvider private let dialogService: DialogService private let screensFactory: ScreensFactory private let vibroService: VibroService - + // MARK: Properties - + private var storedIOSSupportMessage: String? private var numerOfTap = 0 private let maxNumerOfTap = 10 - + private lazy var versionFooterView = VersionFooterView() - + // MARK: Init - + init( accountService: AccountService, accountsProvider: AccountsProvider, @@ -144,159 +144,169 @@ final class AboutViewController: FormViewController { self.dialogService = dialogService self.screensFactory = screensFactory self.vibroService = vibroService - + super.init(style: .insetGrouped) } - + required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: Lifecycle - + override func viewDidLoad() { super.viewDidLoad() - + navigationItem.largeTitleDisplayMode = .always navigationItem.title = String.adamant.about.title tableView.tableFooterView = versionFooterView setVersion() - + // MARK: Header & Footer if let header = UINib(nibName: "LogoFullHeader", bundle: nil).instantiate(withOwner: nil, options: nil).first as? UIView { - + let tapGestureRecognizer = UITapGestureRecognizer( target: self, action: #selector(tapAction) ) header.addGestureRecognizer(tapGestureRecognizer) - + tableView.tableHeaderView = header - + if let label = header.viewWithTag(888) as? UILabel { label.text = String.adamant.shared.productName } } - + // MARK: About - form +++ Section(Sections.about.localized) { - $0.tag = Sections.about.tag - } - - // Website - <<< buildUrlRow(title: Rows.website.localized, - value: "adamant.im", - tag: Rows.website.tag, - url: Rows.website.localizedUrl, - image: Rows.website.image) - - // Whitepaper - <<< buildUrlRow(title: Rows.whitepaper.localized, - value: nil, - tag: Rows.whitepaper.tag, - url: Rows.whitepaper.localizedUrl, - image: Rows.whitepaper.image) - - // Blog - <<< buildUrlRow(title: Rows.blog.localized, - value: nil, - tag: Rows.blog.tag, - url: Rows.blog.localizedUrl, - image: Rows.blog.image) - - // Twitter - <<< buildUrlRow( - title: Rows.twitter.localized, - value: nil, - tag: Rows.twitter.tag, - url: Rows.twitter.localizedUrl, - image: Rows.twitter.image) - - // Github - <<< buildUrlRow(title: Rows.github.localized, - value: nil, - tag: Rows.github.tag, - url: Rows.github.localizedUrl, - image: Rows.github.image) - - // Welcome screens - <<< LabelRow { - $0.title = Rows.welcomeScreens.localized - $0.tag = Rows.welcomeScreens.tag - $0.cell.imageView?.image = Rows.welcomeScreens.image - $0.cell.imageView?.tintColor = UIColor.adamant.tableRowIcons - $0.cell.selectionStyle = .gray - }.cellUpdate { (cell, _) in - cell.accessoryType = .disclosureIndicator - }.onCellSelection { [weak self] (_, _) in - guard let self = self else { return } - let vc = self.screensFactory.makeOnboard() - vc.modalPresentationStyle = .overFullScreen - self.present(vc, animated: true, completion: nil) - } - - // MARK: Contact - +++ Section(Sections.contactUs.localized) { - $0.tag = Sections.contactUs.tag - } - - // Adamant - <<< LabelRow { - $0.title = Rows.adm.localized - $0.tag = Rows.adm.tag - $0.cell.imageView?.image = Rows.adm.image - $0.cell.imageView?.tintColor = UIColor.adamant.tableRowIcons - $0.cell.selectionStyle = .gray - }.cellUpdate { (cell, _) in - cell.accessoryType = .disclosureIndicator - }.onCellSelection { [weak self] (_, _) in - self?.contactUsAction() - } - - // E-mail - <<< LabelRow { - $0.title = Rows.email.localized - $0.value = AdamantResources.supportEmail - $0.tag = Rows.email.tag - $0.cell.imageView?.image = Rows.email.image - $0.cell.imageView?.tintColor = UIColor.adamant.tableRowIcons - $0.cell.selectionStyle = .gray - }.cellUpdate { (cell, _) in - cell.accessoryType = .disclosureIndicator - }.onCellSelection { [weak self] (_, row) in - self?.openEmailScreen( - recipient: AdamantResources.supportEmail, - subject: "ADAMANT Support", - body: "\n\n\n" + AdamantUtilities.deviceInfo, - delegate: self + form + +++ Section(Sections.about.localized) { + $0.tag = Sections.about.tag + } + + // Website + <<< buildUrlRow( + title: Rows.website.localized, + value: "adamant.im", + tag: Rows.website.tag, + url: Rows.website.localizedUrl, + image: Rows.website.image ) - - row.deselect() - } - + + // Whitepaper + <<< buildUrlRow( + title: Rows.whitepaper.localized, + value: nil, + tag: Rows.whitepaper.tag, + url: Rows.whitepaper.localizedUrl, + image: Rows.whitepaper.image + ) + + // Blog + <<< buildUrlRow( + title: Rows.blog.localized, + value: nil, + tag: Rows.blog.tag, + url: Rows.blog.localizedUrl, + image: Rows.blog.image + ) + + // Twitter + <<< buildUrlRow( + title: Rows.twitter.localized, + value: nil, + tag: Rows.twitter.tag, + url: Rows.twitter.localizedUrl, + image: Rows.twitter.image + ) + + // Github + <<< buildUrlRow( + title: Rows.github.localized, + value: nil, + tag: Rows.github.tag, + url: Rows.github.localizedUrl, + image: Rows.github.image + ) + + // Welcome screens + <<< LabelRow { + $0.title = Rows.welcomeScreens.localized + $0.tag = Rows.welcomeScreens.tag + $0.cell.imageView?.image = Rows.welcomeScreens.image + $0.cell.imageView?.tintColor = UIColor.adamant.tableRowIcons + $0.cell.selectionStyle = .gray + }.cellUpdate { (cell, _) in + cell.accessoryType = .disclosureIndicator + }.onCellSelection { [weak self] (_, _) in + guard let self = self else { return } + let vc = self.screensFactory.makeOnboard() + vc.modalPresentationStyle = .overFullScreen + self.present(vc, animated: true, completion: nil) + } + + // MARK: Contact + +++ Section(Sections.contactUs.localized) { + $0.tag = Sections.contactUs.tag + } + + // Adamant + <<< LabelRow { + $0.title = Rows.adm.localized + $0.tag = Rows.adm.tag + $0.cell.imageView?.image = Rows.adm.image + $0.cell.imageView?.tintColor = UIColor.adamant.tableRowIcons + $0.cell.selectionStyle = .gray + }.cellUpdate { (cell, _) in + cell.accessoryType = .disclosureIndicator + }.onCellSelection { [weak self] (_, _) in + self?.contactUsAction() + } + + // E-mail + <<< LabelRow { + $0.title = Rows.email.localized + $0.value = AdamantResources.supportEmail + $0.tag = Rows.email.tag + $0.cell.imageView?.image = Rows.email.image + $0.cell.imageView?.tintColor = UIColor.adamant.tableRowIcons + $0.cell.selectionStyle = .gray + }.cellUpdate { (cell, _) in + cell.accessoryType = .disclosureIndicator + }.onCellSelection { [weak self] (_, row) in + self?.openEmailScreen( + recipient: AdamantResources.supportEmail, + subject: "ADAMANT Support", + body: "\n\n\n" + AdamantUtilities.deviceInfo, + delegate: self + ) + + row.deselect() + } + setColors() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - + if let indexPath = tableView.indexPathForSelectedRow { tableView.deselectRow(at: indexPath, animated: animated) } } - + override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() versionFooterView.sizeToFit() } - + // MARK: - Other - + private func setColors() { view.backgroundColor = UIColor.adamant.secondBackgroundColor tableView.backgroundColor = .clear } - + @MainActor private func contactUsAction() { Task { @@ -367,15 +377,15 @@ extension AboutViewController { guard let url = URL(string: urlRaw) else { fatalError("Failed to build page url: \(urlRaw)") } - + let safari = SFSafariViewController(url: url) safari.preferredControlTintColor = UIColor.adamant.primary safari.modalPresentationStyle = .overFullScreen self?.present(safari, animated: true, completion: nil) - + row.deselect() } - + return row } } @@ -389,23 +399,23 @@ extension AboutViewController: MFMailComposeViewControllerDelegate { } } -private extension AboutViewController { - @objc func tapAction() { +extension AboutViewController { + @objc fileprivate func tapAction() { numerOfTap += 1 - + guard numerOfTap == maxNumerOfTap else { return } - + vibroService.applyVibration(.success) addVibrationRow() } - - func addVibrationRow() { + + fileprivate func addVibrationRow() { guard let appSection = form.sectionBy(tag: Sections.contactUs.tag), - form.rowBy(tag: Rows.vibration.tag) == nil + form.rowBy(tag: Rows.vibration.tag) == nil else { return } - + let vibrationRow = LabelRow { $0.title = Rows.vibration.localized $0.tag = Rows.vibration.tag @@ -414,26 +424,27 @@ private extension AboutViewController { }.cellUpdate { (cell, _) in cell.accessoryType = .disclosureIndicator }.onCellSelection { [weak self] (_, _) in - guard let vc = self?.screensFactory.makeVibrationSelection() + guard + let vc = self?.screensFactory.makeVibrationSelection( + onSettingsSelect: { [weak self] settingType in + switch settingType { + case .adamantWallets: + let vc = TokensAndCoinsViewController(dialogService: self?.dialogService) + self?.showViewController(vc) + } + } + ) else { return } - - if let split = self?.splitViewController { - let details = UINavigationController(rootViewController:vc) - split.showDetailViewController(details, sender: self) - } else if let nav = self?.navigationController { - nav.pushViewController(vc, animated: true) - } else { - vc.modalPresentationStyle = .overFullScreen - self?.present(vc, animated: true, completion: nil) - } + + self?.showViewController(vc) } - + appSection.append(vibrationRow) } - - func setVersion() { + + fileprivate func setVersion() { versionFooterView.model = .init( version: AdamantUtilities.applicationVersion, commit: AdamantUtilities.Git.commitHash.map { @@ -441,4 +452,16 @@ private extension AboutViewController { } ) } + + private func showViewController(_ vc: UIViewController) { + if let split = self.splitViewController { + let details = UINavigationController(rootViewController: vc) + split.showDetailViewController(details, sender: self) + } else if let nav = self.navigationController { + nav.pushViewController(vc, animated: true) + } else { + vc.modalPresentationStyle = .overFullScreen + self.present(vc, animated: true, completion: nil) + } + } } diff --git a/Adamant/Modules/Settings/Contribute/ContributeFactory.swift b/Adamant/Modules/Settings/Contribute/ContributeFactory.swift index 22f985749..2de85aed6 100644 --- a/Adamant/Modules/Settings/Contribute/ContributeFactory.swift +++ b/Adamant/Modules/Settings/Contribute/ContributeFactory.swift @@ -6,18 +6,18 @@ // Copyright © 2023 Adamant. All rights reserved. // -import Swinject import SwiftUI +import Swinject @MainActor struct ContributeFactory { private let parent: Assembler private let assemblies = [ContributeAssembly()] - + init(parent: Assembler) { self.parent = parent } - + func makeViewController() -> UIViewController { let assembler = Assembler(assemblies, parent: parent) let viewModel = { assembler.resolver.resolve(ContributeViewModel.self)! } diff --git a/Adamant/Modules/Settings/Contribute/ContributeState.swift b/Adamant/Modules/Settings/Contribute/ContributeState.swift index 31398af8d..c65a35546 100644 --- a/Adamant/Modules/Settings/Contribute/ContributeState.swift +++ b/Adamant/Modules/Settings/Contribute/ContributeState.swift @@ -6,21 +6,21 @@ // Copyright © 2023 Adamant. All rights reserved. // -import SwiftUI import CommonKit +import SwiftUI struct ContributeState { var isCrashlyticsOn: Bool var isCrashButtonOn: Bool var safariURL: IDWrapper? - + let name: String let crashliticsRowImage: UIImage let crashliticsRowName: String let crashliticsRowDescription: String let crashButtonTitle: String let linkRows: [LinkRow] - + static var initial: ContributeState { Self( isCrashlyticsOn: false, diff --git a/Adamant/Modules/Settings/Contribute/ContributeView.swift b/Adamant/Modules/Settings/Contribute/ContributeView.swift index 99f4719d8..31f382f2e 100644 --- a/Adamant/Modules/Settings/Contribute/ContributeView.swift +++ b/Adamant/Modules/Settings/Contribute/ContributeView.swift @@ -6,12 +6,12 @@ // Copyright © 2023 Adamant. All rights reserved. // -import SwiftUI import CommonKit +import SwiftUI struct ContributeView: View { @StateObject private var viewModel: ContributeViewModel - + var body: some View { List { Section( @@ -25,7 +25,7 @@ struct ContributeView: View { }, footer: { Text(viewModel.state.crashliticsRowDescription) } ) - + ForEach(viewModel.state.linkRows) { makeLinkSection(row: $0) } @@ -38,14 +38,14 @@ struct ContributeView: View { SafariWebView(url: $0.value).ignoresSafeArea() } } - + init(viewModel: @escaping () -> ContributeViewModel) { _viewModel = .init(wrappedValue: viewModel()) } } -private extension ContributeView { - var crashliticsContent: some View { +extension ContributeView { + fileprivate var crashliticsContent: some View { Toggle(isOn: $viewModel.state.isCrashlyticsOn) { HStack { Image(uiImage: viewModel.state.crashliticsRowImage) @@ -57,12 +57,12 @@ private extension ContributeView { } .tint(.init(uiColor: .adamant.active)) } - - var crashButton: some View { + + fileprivate var crashButton: some View { Button(viewModel.state.crashButtonTitle) { viewModel.simulateCrash() } } - - func makeLinkSection(row: ContributeState.LinkRow) -> some View { + + fileprivate func makeLinkSection(row: ContributeState.LinkRow) -> some View { Section( content: { NavigationButton(action: { viewModel.openLink(row: row) }) { diff --git a/Adamant/Modules/Settings/Contribute/ContributeViewModel.swift b/Adamant/Modules/Settings/Contribute/ContributeViewModel.swift index 72b86c082..2b598045f 100644 --- a/Adamant/Modules/Settings/Contribute/ContributeViewModel.swift +++ b/Adamant/Modules/Settings/Contribute/ContributeViewModel.swift @@ -6,41 +6,41 @@ // Copyright © 2023 Adamant. All rights reserved. // -import SwiftUI import Combine import CommonKit +import SwiftUI @MainActor final class ContributeViewModel: ObservableObject { private let crashliticsService: CrashlyticsService private var subscriptions = Set() - + @Published var state: ContributeState = .initial - + init(crashliticsService: CrashlyticsService) { self.crashliticsService = crashliticsService setup() } - + func enableCrashButton() { withAnimation { state.isCrashButtonOn = true } } - + func openLink(row: ContributeState.LinkRow) { state.safariURL = row.link.map { .init(id: $0.absoluteString, value: $0) } } - + func simulateCrash() { fatalError("Test crash") } } -private extension ContributeViewModel { - func setup() { +extension ContributeViewModel { + fileprivate func setup() { state.isCrashlyticsOn = crashliticsService.isCrashlyticsEnabled() - + $state.map(\.isCrashlyticsOn) .removeDuplicates() .sink { [weak crashliticsService] in crashliticsService?.setCrashlyticsEnabled($0) } diff --git a/Adamant/Modules/Settings/NodesAndTokensInfo/BlockchainTokensViewController.swift b/Adamant/Modules/Settings/NodesAndTokensInfo/BlockchainTokensViewController.swift new file mode 100644 index 000000000..55f21777d --- /dev/null +++ b/Adamant/Modules/Settings/NodesAndTokensInfo/BlockchainTokensViewController.swift @@ -0,0 +1,61 @@ +// +// BlockchainTokensViewController.swift +// Adamant +// +// Created by Sergei Veretennikov on 28.03.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import AdamantWalletsKit +import UIKit + +final class BlockchainsTokensViewController: UIViewController { + private lazy var tableView: UITableView = .init(frame: view.bounds) + private var coinsData: [CoinInfoDTO] + private let dialogService: DialogService? + + init(coinsData: [CoinInfoDTO], title: String, dialogService: DialogService?) { + self.coinsData = coinsData.sorted(by: { $0.symbol < $1.symbol }) + self.dialogService = dialogService + super.init(nibName: nil, bundle: nil) + self.title = "Tokens in \(title)" + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + setupViews() + } + + private func setupViews() { + view.addSubview(tableView) + view.backgroundColor = .white + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") + tableView.dataSource = self + tableView.delegate = self + tableView.reloadData() + } +} + +extension BlockchainsTokensViewController: UITableViewDataSource, UITableViewDelegate { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + coinsData.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) + var config = cell.defaultContentConfiguration() + config.text = coinsData[indexPath.row].symbol.uppercased() + cell.contentConfiguration = config + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + let model = coinsData[indexPath.row] + navigationController?.pushViewController(CoinInfoDTOViewController(coinInfo: model, dialogService: dialogService), animated: true) + } +} diff --git a/Adamant/Modules/Settings/NodesAndTokensInfo/CoinInfoDTOViewController.swift b/Adamant/Modules/Settings/NodesAndTokensInfo/CoinInfoDTOViewController.swift new file mode 100644 index 000000000..330d47914 --- /dev/null +++ b/Adamant/Modules/Settings/NodesAndTokensInfo/CoinInfoDTOViewController.swift @@ -0,0 +1,131 @@ +// +// CoinInfoDTOViewController.swift +// Adamant +// +// Created by Sergei Veretennikov on 28.03.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import AdamantWalletsKit +import CommonKit +import UIKit + +private protocol AnyPrimitive: Sendable {} + +extension Int: AnyPrimitive {} +extension String: AnyPrimitive {} +extension Double: AnyPrimitive {} +extension Bool: AnyPrimitive {} + +final class CoinInfoDTOViewController: UIViewController { + private var coinInfoSerialized: [String: String] = [:] + private var keys: [String] = [] + private lazy var tableView: UITableView = .init(frame: view.bounds) + private let dialogService: DialogService? + + init(coinInfo: CoinInfoDTO, dialogService: DialogService?) { + self.dialogService = dialogService + super.init(nibName: nil, bundle: nil) + setDataSource(for: coinInfo) + title = "Description for \(coinInfo.symbol.uppercased())" + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + setupViews() + } + + private func setupViews() { + view.addSubview(tableView) + view.backgroundColor = .white + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") + tableView.dataSource = self + tableView.delegate = self + tableView.reloadData() + } + + private func setDataSource(for coinInfo: CoinInfoDTO) { + guard let data = try? JSONEncoder().encode(coinInfo) else { return } + + func flatten(value: Any, prefix: String) { + let mirror = Mirror(reflecting: value) + + if let primitiveValue = value as? AnyPrimitive { + coinInfoSerialized[prefix] = "\(primitiveValue)" + return + } + + if let stringConvertible = value as? CustomStringConvertible { + coinInfoSerialized[prefix] = stringConvertible.description + return + } + + switch mirror.displayStyle { + case .optional: + if let firstChild = mirror.children.first { + flatten(value: firstChild.value, prefix: prefix) + } else { + coinInfoSerialized[prefix] = "nil" + } + + case .struct, .class: + for child in mirror.children { + guard let propertyName = child.label else { continue } + flatten(value: child.value, prefix: "\(prefix).\(propertyName)") + } + + case .enum: + coinInfoSerialized[prefix] = "\(value)" + + case .collection: + var index = 0 + for item in mirror.children { + flatten(value: item.value, prefix: "\(prefix)[\(index)]") + index += 1 + } + + case .dictionary: + for (key, val) in mirror.children { + guard let key else { continue } + flatten(value: val, prefix: "\(prefix).\(key)") + } + + default: + let stringValue = "\(value)" + coinInfoSerialized[prefix] = stringValue.isEmpty ? "empty" : stringValue + } + } + flatten(value: coinInfo, prefix: coinInfo.symbol.uppercased()) + keys = coinInfoSerialized.map { $0.key }.sorted { $0 < $1 } + } +} + +extension CoinInfoDTOViewController: UITableViewDataSource, UITableViewDelegate { + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + keys.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) + var config = cell.defaultContentConfiguration() + let key = keys[indexPath.row] + config.text = key + config.secondaryText = coinInfoSerialized[key] + cell.contentConfiguration = config + return cell + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + if let cell = tableView.cellForRow(at: indexPath), + let value = (cell.contentConfiguration as? UIListContentConfiguration)?.secondaryText + { + UIPasteboard.general.string = value + dialogService?.showToastMessage("Value copied") + } + } +} diff --git a/Adamant/Modules/Settings/NodesAndTokensInfo/TokensAndCoinsViewController.swift b/Adamant/Modules/Settings/NodesAndTokensInfo/TokensAndCoinsViewController.swift new file mode 100644 index 000000000..f4ea27982 --- /dev/null +++ b/Adamant/Modules/Settings/NodesAndTokensInfo/TokensAndCoinsViewController.swift @@ -0,0 +1,117 @@ +// +// TokensAndCoinsViewController.swift +// Adamant +// +// Created by Sergei Veretennikov on 28.03.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import AdamantWalletsKit +import CommonKit +import SnapKit +import UIKit + +final class TokensAndCoinsViewController: UIViewController { + private lazy var tableView: UITableView = .init(frame: view.bounds) + + private var chainsData: [AnyBlockchain] = [] + private var coinsData: [CoinInfoDTO] = [] + private let dialogService: DialogService? + + init(dialogService: DialogService?) { + self.dialogService = dialogService + super.init(nibName: nil, bundle: nil) + title = "Coins and Tokens storage" + setupDataSource() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + setupViews() + } + + private func setupDataSource() { + guard let coinsData = CoinInfoProvider.storage?.getCoinsAndChains() else { return } + self.chainsData = coinsData.chains.map { $0.key }.sorted(by: { $0.rawValue < $1.rawValue }) + self.coinsData = coinsData.coins.sorted(by: { $0.key < $1.key }).map({ $0.value }) + } + + private func setupViews() { + view.addSubview(tableView) + view.backgroundColor = .white + tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") + tableView.dataSource = self + tableView.delegate = self + tableView.reloadData() + } +} + +extension TokensAndCoinsViewController: UITableViewDataSource, UITableViewDelegate { + func numberOfSections(in tableView: UITableView) -> Int { + 2 + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if section == 0 { + chainsData.count + } else { + coinsData.count + } + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if indexPath.section == 0 { + cellForChain(at: indexPath, in: tableView) + } else { + cellForCoin(at: indexPath, in: tableView) + } + } + + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + if section == 0 { + "Blockchains" + } else { + "Coins" + } + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + let viewController = viewControllerFor(indexPath: indexPath) + navigationController?.pushViewController(viewController, animated: true) + } + + private func viewControllerFor(indexPath: IndexPath) -> UIViewController { + if indexPath.section == 0 { + let chain = chainsData[indexPath.row] + if let coinsForChain = CoinInfoProvider.storage?[chain] { + return BlockchainsTokensViewController(coinsData: coinsForChain.map { $0.value }, title: chain.rawValue, dialogService: dialogService) + } else { + return UIViewController() + } + } else { + let model = coinsData[indexPath.row] + return CoinInfoDTOViewController(coinInfo: model, dialogService: dialogService) + } + } + + private func cellForChain(at indexPath: IndexPath, in tableView: UITableView) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) + var config = cell.defaultContentConfiguration() + config.text = chainsData[indexPath.row].rawValue + cell.contentConfiguration = config + return cell + } + + private func cellForCoin(at indexPath: IndexPath, in tableView: UITableView) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) + var config = cell.defaultContentConfiguration() + config.text = coinsData[indexPath.row].symbol.uppercased() + cell.contentConfiguration = config + return cell + } +} diff --git a/Adamant/Modules/Settings/Notifications/NotificationSounds/NotificationSoundsFactory.swift b/Adamant/Modules/Settings/Notifications/NotificationSounds/NotificationSoundsFactory.swift index 622e16572..1216f37e4 100644 --- a/Adamant/Modules/Settings/Notifications/NotificationSounds/NotificationSoundsFactory.swift +++ b/Adamant/Modules/Settings/Notifications/NotificationSounds/NotificationSoundsFactory.swift @@ -6,27 +6,27 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Swinject import SwiftUI +import Swinject @MainActor struct NotificationSoundsFactory { private let parent: Assembler private let assemblies = [NotificationSoundAssembly()] - + init(parent: Assembler) { self.parent = parent } - + @MainActor func makeView(target: NotificationTarget) -> NotificationSoundsView { let assembler = Assembler(assemblies, parent: parent) let viewModel = { assembler.resolver.resolve(NotificationSoundsViewModel.self, argument: target)! } - + let view = NotificationSoundsView(viewModel: viewModel) - + return view } } diff --git a/Adamant/Modules/Settings/Notifications/NotificationSounds/NotificationSoundsView.swift b/Adamant/Modules/Settings/Notifications/NotificationSounds/NotificationSoundsView.swift index b3523c161..eec84c718 100644 --- a/Adamant/Modules/Settings/Notifications/NotificationSounds/NotificationSoundsView.swift +++ b/Adamant/Modules/Settings/Notifications/NotificationSounds/NotificationSoundsView.swift @@ -6,18 +6,18 @@ // Copyright © 2024 Adamant. All rights reserved. // -import SwiftUI import CommonKit +import SwiftUI struct NotificationSoundsView: View { @StateObject var viewModel: NotificationSoundsViewModel - + @Environment(\.dismiss) var dismiss - + init(viewModel: @escaping () -> NotificationSoundsViewModel) { _viewModel = .init(wrappedValue: viewModel()) } - + var body: some View { GeometryReader { _ in Form { @@ -30,15 +30,18 @@ struct NotificationSoundsView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .topBarLeading) { - Button(action: { - dismiss() - }, label: { - HStack { - Text(cancelTitle) + Button( + action: { + dismiss() + }, + label: { + HStack { + Text(cancelTitle) + } } - }) + ) } - + ToolbarItem(placement: .principal) { Text(toolbarTitle) .font(.headline) @@ -46,15 +49,18 @@ struct NotificationSoundsView: View { .lineLimit(1) .frame(maxWidth: .infinity, alignment: .center) } - + ToolbarItem(placement: .topBarTrailing) { - Button(action: { - viewModel.save() - }, label: { - HStack { - Text(saveTitle) + Button( + action: { + viewModel.save() + }, + label: { + HStack { + Text(saveTitle) + } } - }) + ) } } .onReceive(viewModel.dismissAction) { @@ -64,38 +70,44 @@ struct NotificationSoundsView: View { } } -private extension NotificationSoundsView { - func toolbar() -> some View { +extension NotificationSoundsView { + fileprivate func toolbar() -> some View { HStack { - Button(action: { - dismiss() - }, label: { - HStack { - Text(cancelTitle) + Button( + action: { + dismiss() + }, + label: { + HStack { + Text(cancelTitle) + } } - }) - + ) + Spacer() - + Text(toolbarTitle) .font(.headline) .minimumScaleFactor(0.7) .lineLimit(1) .frame(maxWidth: .infinity, alignment: .center) - + Spacer() - - Button(action: { - viewModel.save() - }, label: { - HStack { - Text(saveTitle) + + Button( + action: { + viewModel.save() + }, + label: { + HStack { + Text(saveTitle) + } } - }) + ) } } - - func listSounds() -> some View { + + fileprivate func listSounds() -> some View { List { ForEach(viewModel.sounds, id: \.self) { sound in Button( diff --git a/Adamant/Modules/Settings/Notifications/NotificationSounds/NotificationSoundsViewModel.swift b/Adamant/Modules/Settings/Notifications/NotificationSounds/NotificationSoundsViewModel.swift index 7a0fc582b..73d390437 100644 --- a/Adamant/Modules/Settings/Notifications/NotificationSounds/NotificationSoundsViewModel.swift +++ b/Adamant/Modules/Settings/Notifications/NotificationSounds/NotificationSoundsViewModel.swift @@ -6,24 +6,27 @@ // Copyright © 2024 Adamant. All rights reserved. // -import SwiftUI -import CommonKit -import Combine import AVFoundation +import Combine +import CommonKit +import SwiftUI @MainActor final class NotificationSoundsViewModel: ObservableObject { private let notificationsService: NotificationsService private var notificationTarget: NotificationTarget private let dialogService: DialogService - - let dismissAction = PassthroughSubject() + + let dismissAction = PassthroughSubject() @Published var isPresented: Bool = false @Published var selectedSound: NotificationSound = .inputDefault - @Published var sounds: [NotificationSound] = [.none, .noteDefault, .inputDefault, .proud, .relax, .success, .note, .antic, .cheers, .chord, .droplet, .handoff, .milestone, .passage, .portal, .rattle, .rebound, .slide, .welcome] - + @Published var sounds: [NotificationSound] = [ + .none, .noteDefault, .inputDefault, .proud, .relax, .success, .note, .antic, .cheers, .chord, .droplet, .handoff, .milestone, .passage, .portal, + .rattle, .rebound, .slide, .welcome + ] + private var audioPlayer: AVAudioPlayer? - + init( notificationsService: NotificationsService, target: NotificationTarget, @@ -32,7 +35,7 @@ final class NotificationSoundsViewModel: ObservableObject { self.notificationsService = notificationsService self.notificationTarget = target self.dialogService = dialogService - + switch notificationTarget { case .baseMessage: self.selectedSound = notificationsService.notificationsSound @@ -40,25 +43,25 @@ final class NotificationSoundsViewModel: ObservableObject { self.selectedSound = notificationsService.notificationsReactionSound } } - + func setup(notificationTarget: NotificationTarget) { self.notificationTarget = notificationTarget } - + func save() { setNotificationSound(selectedSound) dismissAction.send() } - + func setNotificationSound(_ sound: NotificationSound) { notificationsService.setNotificationSound(sound, for: notificationTarget) } - + func selectSound(_ sound: NotificationSound) { selectedSound = sound playSound(sound) } - + func playSound(_ sound: NotificationSound) { switch sound { case .none: @@ -69,12 +72,12 @@ final class NotificationSoundsViewModel: ObservableObject { } } -private extension NotificationSoundsViewModel { - func playSound(by fileName: String) { +extension NotificationSoundsViewModel { + fileprivate func playSound(by fileName: String) { guard let url = Bundle.main.url(forResource: fileName.replacingOccurrences(of: ".mp3", with: ""), withExtension: "mp3") else { return } - + do { try AVAudioSession.sharedInstance().setCategory(.playback) try AVAudioSession.sharedInstance().setActive(true) diff --git a/Adamant/Modules/Settings/Notifications/NotificationsFactory.swift b/Adamant/Modules/Settings/Notifications/NotificationsFactory.swift index 34eedfa7b..c42650b61 100644 --- a/Adamant/Modules/Settings/Notifications/NotificationsFactory.swift +++ b/Adamant/Modules/Settings/Notifications/NotificationsFactory.swift @@ -6,35 +6,36 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Swinject import SwiftUI +import Swinject @MainActor struct NotificationsFactory { private let parent: Assembler private let assemblies = [NotificationsAssembly()] - + init(parent: Assembler) { self.parent = parent } - + @MainActor - func makeViewController() -> UIViewController { + func makeViewController(screensFactory: ScreensFactory) -> UIViewController { let assembler = Assembler(assemblies, parent: parent) let viewModel = { assembler.resolver.resolve(NotificationsViewModel.self)! } - + let baseSoundsFactory = NotificationSoundsFactory(parent: assembler) let reactionSoundsFactory = NotificationSoundsFactory(parent: assembler) - + let baseSoundsView = { baseSoundsFactory.makeView(target: .baseMessage).eraseToAnyView() } let reactionSoundsView = { reactionSoundsFactory.makeView(target: .reaction).eraseToAnyView() } - + let view = NotificationsView( viewModel: viewModel, baseSoundsView: baseSoundsView, - reactionSoundsView: reactionSoundsView + reactionSoundsView: reactionSoundsView, + screensFactory: screensFactory ) - + return UIHostingController( rootView: view ) @@ -46,7 +47,8 @@ private struct NotificationsAssembly: MainThreadAssembly { container.register(NotificationsViewModel.self) { r in NotificationsViewModel( dialogService: r.resolve(DialogService.self)!, - notificationsService: r.resolve(NotificationsService.self)! + notificationsService: r.resolve(NotificationsService.self)!, + accountService: r.resolve(AccountService.self)! ) }.inObjectScope(.transient) } diff --git a/Adamant/Modules/Settings/Notifications/NotificationsView.swift b/Adamant/Modules/Settings/Notifications/NotificationsView.swift index a7ff49e2a..ae525fcd0 100644 --- a/Adamant/Modules/Settings/Notifications/NotificationsView.swift +++ b/Adamant/Modules/Settings/Notifications/NotificationsView.swift @@ -6,24 +6,27 @@ // Copyright © 2024 Adamant. All rights reserved. // -import SwiftUI import CommonKit +import SwiftUI struct NotificationsView: View { @StateObject var viewModel: NotificationsViewModel private let baseSoundsView: () -> AnyView private let reactionSoundsView: () -> AnyView - + private let screensFactory: ScreensFactory + init( viewModel: @escaping () -> NotificationsViewModel, baseSoundsView: @escaping () -> AnyView, - reactionSoundsView: @escaping () -> AnyView + reactionSoundsView: @escaping () -> AnyView, + screensFactory: ScreensFactory ) { _viewModel = .init(wrappedValue: viewModel()) self.baseSoundsView = baseSoundsView self.reactionSoundsView = reactionSoundsView + self.screensFactory = screensFactory } - + var body: some View { Form { notificationsSection() @@ -41,20 +44,31 @@ struct NotificationsView: View { toolbar() } } - .sheet(isPresented: $viewModel.presentSoundsPicker, content: { - NavigationView(content: { baseSoundsView() }) - }) - .sheet(isPresented: $viewModel.presentReactionSoundsPicker, content: { - NavigationView(content: { reactionSoundsView() }) - }) + .sheet( + isPresented: $viewModel.presentSoundsPicker, + content: { + NavigationView(content: { baseSoundsView() }) + } + ) + .sheet( + isPresented: $viewModel.presentReactionSoundsPicker, + content: { + NavigationView(content: { reactionSoundsView() }) + } + ) .fullScreenCover(isPresented: $viewModel.openSafariURL) { SafariWebView(url: viewModel.safariURL).ignoresSafeArea() } + .fullScreenCover(isPresented: $viewModel.presentBuyAndSell) { + screensFactory.makeBuyAndSellView(action: { + viewModel.presentBuyAndSell = false + }) + } } } -private extension NotificationsView { - func toolbar() -> some View { +extension NotificationsView { + fileprivate func toolbar() -> some View { HStack { Text(viewModel.notificationsTitle) .font(.headline) @@ -63,8 +77,8 @@ private extension NotificationsView { } .frame(alignment: .center) } - - func notificationsSection() -> some View { + + fileprivate func notificationsSection() -> some View { Section { NavigationButton(action: { viewModel.showAlert() }) { HStack { @@ -78,8 +92,8 @@ private extension NotificationsView { Text(viewModel.notificationsTitle) } } - - func messageSoundSection() -> some View { + + fileprivate func messageSoundSection() -> some View { Section { NavigationButton(action: { viewModel.presentNotificationSoundsPicker() }) { HStack { @@ -93,8 +107,8 @@ private extension NotificationsView { Text(messagesHeader) } } - - func messageReactionsSection() -> some View { + + fileprivate func messageReactionsSection() -> some View { Section { NavigationButton(action: { viewModel.presentReactionNotificationSoundsPicker() }) { HStack { @@ -108,19 +122,19 @@ private extension NotificationsView { Text(reactionsHeader) } } - - func inAppNotificationsSection() -> some View { + + fileprivate func inAppNotificationsSection() -> some View { Section { Toggle(isOn: $viewModel.inAppSounds) { Text(soundsTitle) } .tint(.init(uiColor: .adamant.active)) - + Toggle(isOn: $viewModel.inAppVibrate) { Text(vibrateTitle) } .tint(.init(uiColor: .adamant.active)) - + Toggle(isOn: $viewModel.inAppToasts) { Text(toastsTitle) } @@ -129,8 +143,8 @@ private extension NotificationsView { Text(inAppNotifications) } } - - func settingsSection() -> some View { + + fileprivate func settingsSection() -> some View { Section { NavigationButton(action: { viewModel.openAppSettings() }) { HStack { @@ -142,8 +156,8 @@ private extension NotificationsView { Text(settingsHeader) } } - - func moreDetailsSection() -> some View { + + fileprivate func moreDetailsSection() -> some View { Section { if let description = viewModel.parsedMarkdownDescription { Text(description) diff --git a/Adamant/Modules/Settings/Notifications/NotificationsViewModel.swift b/Adamant/Modules/Settings/Notifications/NotificationsViewModel.swift index 1f3d26f35..82efbbc30 100644 --- a/Adamant/Modules/Settings/Notifications/NotificationsViewModel.swift +++ b/Adamant/Modules/Settings/Notifications/NotificationsViewModel.swift @@ -6,15 +6,15 @@ // Copyright © 2024 Adamant. All rights reserved. // -import SwiftUI +import Combine import CommonKit -import SafariServices import MarkdownKit -import Combine +import SafariServices +import SwiftUI @MainActor final class NotificationsViewModel: ObservableObject { - + @Published var notificationsMode: NotificationsMode = .disabled @Published var notificationSound: NotificationSound = .inputDefault @Published var notificationReactionSound: NotificationSound = .none @@ -24,56 +24,59 @@ final class NotificationsViewModel: ObservableObject { @Published var inAppSounds: Bool = false @Published var inAppVibrate: Bool = true @Published var inAppToasts: Bool = true - + @Published var presentBuyAndSell: Bool = false + let notificationsTitle: String = .localized("SecurityPage.Row.Notifications") let safariURL = URL(string: "https://github.com/Adamant-im")! - + private let descriptionText: String = .localized("SecurityPage.Row.Notifications.ModesDescription") - + private let dialogService: DialogService private let notificationsService: NotificationsService - + private let accountService: AccountService + private var subscriptions = Set() private var cancellables = Set() - + var parsedMarkdownDescription: AttributedString? { guard let attributedString = parseMarkdown(descriptionText) else { return nil } return AttributedString(attributedString) } - - init(dialogService: DialogService, notificationsService: NotificationsService) { + + init(dialogService: DialogService, notificationsService: NotificationsService, accountService: AccountService) { self.dialogService = dialogService self.notificationsService = notificationsService + self.accountService = accountService configure() addObservers() } - + func presentNotificationSoundsPicker() { presentSoundsPicker = true } - + func presentReactionNotificationSoundsPicker() { presentReactionSoundsPicker = true } - + func presentSafariURL() { openSafariURL = true } - + func applyInAppSounds(value: Bool) { notificationsService.setInAppSound(value) } - + func applyInAppVibrate(value: Bool) { notificationsService.setInAppVibrate(value) } - + func applyInAppToasts(value: Bool) { notificationsService.setInAppToasts(value) } - + func showAlert() { dialogService.showAlert( title: notificationsTitle, @@ -103,22 +106,27 @@ final class NotificationsViewModel: ObservableObject { from: nil ) } - + func setNotificationMode(_ mode: NotificationsMode) { guard mode != notificationsService.notificationsMode else { return } - - notificationsMode = mode + notificationsService.setNotificationsMode(mode) { [weak self] result in DispatchQueue.onMainAsync { switch result { case .success: + self?.notificationsMode = mode return case .failure(let error): switch error { case .notEnoughMoney, .notStayedLoggedIn: - self?.dialogService.showRichError(error: error) + self?.dialogService.showFreeTokenAlert( + url: self?.accountService.account?.address, + type: .notification, + showVC: { self?.presentBuyAndSell = true } + ) + case .denied: self?.presentNotificationsDeniedError() } @@ -126,7 +134,7 @@ final class NotificationsViewModel: ObservableObject { } } } - + func openAppSettings() { if let settingsURL = URL(string: UIApplication.openSettingsURLString) { if UIApplication.shared.canOpenURL(settingsURL) { @@ -134,7 +142,7 @@ final class NotificationsViewModel: ObservableObject { } } } - + func parseMarkdown(_ text: String) -> NSAttributedString? { let parser = MarkdownParser( font: UIFont.systemFont(ofSize: UIFont.systemFontSize), @@ -145,33 +153,33 @@ final class NotificationsViewModel: ObservableObject { } } -private extension NotificationsViewModel { - func addObservers() { +extension NotificationsViewModel { + fileprivate func addObservers() { NotificationCenter.default .notifications(named: .AdamantNotificationService.notificationsSoundChanged) .sink { [weak self] _ in await self?.configure() } .store(in: &subscriptions) - + $inAppSounds .sink { [weak self] value in self?.applyInAppSounds(value: value) } .store(in: &cancellables) - + $inAppVibrate .sink { [weak self] value in self?.applyInAppVibrate(value: value) } .store(in: &cancellables) - + $inAppToasts .sink { [weak self] value in self?.applyInAppToasts(value: value) } .store(in: &cancellables) } - - func configure() { + + fileprivate func configure() { notificationsMode = notificationsService.notificationsMode notificationSound = notificationsService.notificationsSound notificationReactionSound = notificationsService.notificationsReactionSound @@ -181,24 +189,24 @@ private extension NotificationsViewModel { } } -private extension NotificationsViewModel { - func makeAction(title: String, action: ((UIAlertAction) -> Void)?) -> UIAlertAction { +extension NotificationsViewModel { + fileprivate func makeAction(title: String, action: ((UIAlertAction) -> Void)?) -> UIAlertAction { .init( title: title, style: .default, handler: action ) } - - func makeCancelAction() -> UIAlertAction { + + fileprivate func makeCancelAction() -> UIAlertAction { .init( title: .adamant.alert.cancel, style: .cancel, handler: nil ) } - - func presentNotificationsDeniedError() { + + fileprivate func presentNotificationsDeniedError() { dialogService.showAlert( title: nil, message: NotificationStrings.notificationsDisabled, diff --git a/Adamant/Modules/Settings/PKGenerator/PKGeneratorFactory.swift b/Adamant/Modules/Settings/PKGenerator/PKGeneratorFactory.swift index eac865843..57d15ddbe 100644 --- a/Adamant/Modules/Settings/PKGenerator/PKGeneratorFactory.swift +++ b/Adamant/Modules/Settings/PKGenerator/PKGeneratorFactory.swift @@ -6,17 +6,17 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Swinject import SwiftUI +import Swinject struct PKGeneratorFactory { private let parent: Assembler private let assemblies = [PKGeneratorAssembly()] - + init(parent: Assembler) { self.parent = parent } - + func makeViewController() -> UIViewController { let assembler = Assembler(assemblies, parent: parent) let viewModel = { assembler.resolver.resolve(PKGeneratorViewModel.self)! } diff --git a/Adamant/Modules/Settings/PKGenerator/PKGeneratorState.swift b/Adamant/Modules/Settings/PKGenerator/PKGeneratorState.swift index dc28a23ce..b96c0e4c5 100644 --- a/Adamant/Modules/Settings/PKGenerator/PKGeneratorState.swift +++ b/Adamant/Modules/Settings/PKGenerator/PKGeneratorState.swift @@ -13,7 +13,7 @@ struct PKGeneratorState { var keys: [KeyInfo] var buttonDescription: AttributedString var isLoading: Bool - + static let `default` = Self( passphrase: .empty, keys: .init(), @@ -25,7 +25,7 @@ struct PKGeneratorState { extension PKGeneratorState { struct KeyInfo: Identifiable { var id: String { title } - + let title: String let description: String let icon: UIImage diff --git a/Adamant/Modules/Settings/PKGenerator/PKGeneratorView.swift b/Adamant/Modules/Settings/PKGenerator/PKGeneratorView.swift index 336558c6e..d08c0be7c 100644 --- a/Adamant/Modules/Settings/PKGenerator/PKGeneratorView.swift +++ b/Adamant/Modules/Settings/PKGenerator/PKGeneratorView.swift @@ -6,18 +6,18 @@ // Copyright © 2024 Adamant. All rights reserved. // -import SwiftUI import CommonKit +import SwiftUI struct PKGeneratorView: View { @StateObject private var viewModel: PKGeneratorViewModel - + var body: some View { List { if !viewModel.state.keys.isEmpty { keysSection } - + inputSection } .withoutListBackground() @@ -25,42 +25,42 @@ struct PKGeneratorView: View { .navigationTitle(String.adamant.pkGenerator.title) .navigationBarTitleDisplayMode(.inline) } - + init(viewModel: @escaping () -> PKGeneratorViewModel) { _viewModel = .init(wrappedValue: viewModel()) } } -private extension PKGeneratorView { - var loadingBackground: some View { +extension PKGeneratorView { + fileprivate var loadingBackground: some View { HStack { Spacer() - + if viewModel.state.isLoading { ProgressView() } } } - - var keysSection: some View { + + fileprivate var keysSection: some View { Section { ForEach(viewModel.state.keys, content: keyView) .listRowBackground(Color(uiColor: .adamant.cellColor)) } } - - var inputSection: some View { + + fileprivate var inputSection: some View { Section { Group { Text(viewModel.state.buttonDescription) .multilineTextAlignment(.center) .padding(.vertical, 5) - + AdamantSecureField( placeholder: .adamant.qrGenerator.passphrasePlaceholder, text: $viewModel.state.passphrase ) - + Button(action: { viewModel.generateKeys() }) { Text(String.adamant.pkGenerator.generateButton) .foregroundStyle(Color(uiColor: .adamant.primary)) @@ -71,25 +71,25 @@ private extension PKGeneratorView { }.listRowBackground(Color(uiColor: .adamant.cellColor)) } } - - func keyView(_ keyInfo: PKGeneratorState.KeyInfo) -> some View { + + fileprivate func keyView(_ keyInfo: PKGeneratorState.KeyInfo) -> some View { NavigationButton(action: { viewModel.onTap(key: keyInfo.key) }) { HStack { Image(uiImage: keyInfo.icon) .renderingMode(.template) - .resizable() + .resizable() .frame(squareSize: 25) .foregroundStyle(Color(uiColor: .adamant.tableRowIcons)) - + VStack(alignment: .leading) { Text(keyInfo.title) Text(keyInfo.description) .foregroundStyle(Color(uiColor: .adamant.secondary)) .font(.system(size: 12, weight: .ultraLight)) } - + Spacer(minLength: .zero) - + Text(keyInfo.key).lineLimit(1) .foregroundStyle(Color(uiColor: .adamant.secondary)) } diff --git a/Adamant/Modules/Settings/PKGenerator/PKGeneratorViewModel.swift b/Adamant/Modules/Settings/PKGenerator/PKGeneratorViewModel.swift index dec39e087..9afe49397 100644 --- a/Adamant/Modules/Settings/PKGenerator/PKGeneratorViewModel.swift +++ b/Adamant/Modules/Settings/PKGenerator/PKGeneratorViewModel.swift @@ -6,10 +6,10 @@ // Copyright © 2024 Adamant. All rights reserved. // -import SwiftUI import Combine import CommonKit import MarkdownKit +import SwiftUI // MARK: - Localization extension String.adamant { @@ -32,7 +32,7 @@ extension String.adamant { comment: "PrivateKeyGenerator: Generate button" ) } - + static func keyFormat(_ format: String) -> String { .localizedStringWithFormat( .localized( @@ -48,10 +48,10 @@ extension String.adamant { @MainActor final class PKGeneratorViewModel: ObservableObject { @Published var state: PKGeneratorState = .default - + private let dialogService: DialogService private let walletServiceCompose: WalletServiceCompose - + nonisolated init( dialogService: DialogService, walletServiceCompose: WalletServiceCompose @@ -60,7 +60,7 @@ final class PKGeneratorViewModel: ObservableObject { self.walletServiceCompose = walletServiceCompose Task { @MainActor in configure() } } - + func onTap(key: String) { dialogService.presentShareAlertFor( string: key, @@ -79,15 +79,15 @@ final class PKGeneratorViewModel: ObservableObject { completion: nil ) } - + func generateKeys() { guard !state.isLoading else { return } withAnimation { state.isLoading = true } let passphrase = state.passphrase.lowercased() - + Task { defer { withAnimation { state.isLoading = false } } - + do { let keys = try await Task.detached { [walletServiceCompose] in try generatePrivateKeys( @@ -95,7 +95,7 @@ final class PKGeneratorViewModel: ObservableObject { walletServiceCompose: walletServiceCompose ) }.value - + withAnimation { state.keys = keys } } catch { dialogService.showToastMessage(error.localizedDescription) @@ -104,30 +104,30 @@ final class PKGeneratorViewModel: ObservableObject { } } -private extension PKGeneratorViewModel { - func configure() { +extension PKGeneratorViewModel { + fileprivate func configure() { state.buttonDescription = getButtonDescription() } - - func getButtonDescription() -> AttributedString { + + fileprivate func getButtonDescription() -> AttributedString { let parser = MarkdownParser( font: UIFont.systemFont(ofSize: UIFont.systemFontSize), color: .adamant.primary ) - + let style = NSMutableParagraphStyle() style.alignment = NSTextAlignment.center - + let mutableText = NSMutableAttributedString( attributedString: parser.parse(.adamant.pkGenerator.alert) ) - + mutableText.addAttribute( .paragraphStyle, value: style, range: .init(location: .zero, length: mutableText.length) ) - + return .init(mutableText) } } @@ -138,13 +138,13 @@ private func generatePrivateKeys( ) throws -> [PKGeneratorState.KeyInfo] { guard AdamantUtilities.validateAdamantPassphrase(passphrase: passphrase) else { throw AdamantError(message: .adamant.qrGenerator.wrongPassphraseError) } - + return walletServiceCompose.getWallets().compactMap { guard let generator = $0.core as? PrivateKeyGenerator, let key = generator.generatePrivateKeyFor(passphrase: passphrase) else { return nil } - + return PKGeneratorState.KeyInfo( title: generator.rowTitle, description: .adamant.pkGenerator.keyFormat(generator.keyFormat.rawValue), diff --git a/Adamant/Modules/Settings/PrivateKeyGenerator.swift b/Adamant/Modules/Settings/PrivateKeyGenerator.swift index c7415ad4b..b57bf8bbc 100644 --- a/Adamant/Modules/Settings/PrivateKeyGenerator.swift +++ b/Adamant/Modules/Settings/PrivateKeyGenerator.swift @@ -17,6 +17,6 @@ protocol PrivateKeyGenerator { var rowTitle: String { get } var rowImage: UIImage? { get } var keyFormat: KeyFormat { get } - + func generatePrivateKeyFor(passphrase: String) -> String? } diff --git a/Adamant/Modules/Settings/QRGeneratorViewController.swift b/Adamant/Modules/Settings/QRGeneratorViewController.swift index 6f3eee2a1..82cb6af42 100644 --- a/Adamant/Modules/Settings/QRGeneratorViewController.swift +++ b/Adamant/Modules/Settings/QRGeneratorViewController.swift @@ -6,11 +6,11 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit +import CommonKit import EFQRCode import Eureka import Photos -import CommonKit +import UIKit // MARK: - Localization extension String.adamant { @@ -28,22 +28,25 @@ extension String.adamant { String.localized("QrGeneratorScene.Error.InvalidPassphrase", comment: "QRGenerator: user typed in invalid passphrase") } static var internalError: String { - String.localized("QrGeneratorScene.Error.InternalErrorFormat", comment: "QRGenerator: Bad Internal generator error message format. Using %@ for error description") + String.localized( + "QrGeneratorScene.Error.InternalErrorFormat", + comment: "QRGenerator: Bad Internal generator error message format. Using %@ for error description" + ) } } } // MARK: - final class QRGeneratorViewController: FormViewController { - + // MARK: Dependencies var dialogService: DialogService! - + private enum Rows { case qr case passphrase case generateButton - + var tag: String { switch self { case .qr: return "qr" @@ -52,11 +55,11 @@ final class QRGeneratorViewController: FormViewController { } } } - + private enum Sections { case qr case passphrase - + var tag: String { switch self { case .qr: return "qrs" @@ -64,119 +67,127 @@ final class QRGeneratorViewController: FormViewController { } } } - + // MARK: Init - + init() { super.init(style: .insetGrouped) } - + required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() - + navigationItem.largeTitleDisplayMode = .always navigationItem.title = String.adamant.qrGenerator.title navigationOptions = .Disabled - + tableView.showsVerticalScrollIndicator = false tableView.showsHorizontalScrollIndicator = false - + // MARK: QR section form +++ Section { $0.tag = Sections.qr.tag } - <<< QrRow { - $0.tag = Rows.qr.tag - $0.cell.tipLabel.text = String.adamant.qrGenerator.tapToSaveTip - }.onCellSelection { [weak self] (cell, row) in - if let tableView = self?.tableView, let indexPath = tableView.indexPathForSelectedRow { - tableView.deselectRow(at: indexPath, animated: true) - } - - guard let qr = row.value else { - return - } - - let save = UIAlertAction(title: String.adamant.alert.saveToPhotolibrary, style: .default, handler: { _ in - - switch PHPhotoLibrary.authorizationStatus() { - case .authorized, .limited: - UIImageWriteToSavedPhotosAlbum(qr, self, #selector(self?.image(_: didFinishSavingWithError: contextInfo:)), nil) - - case .notDetermined: - UIImageWriteToSavedPhotosAlbum(qr, self, #selector(self?.image(_: didFinishSavingWithError: contextInfo:)), nil) - - case .restricted, .denied: - self?.dialogService.presentGoToSettingsAlert(title: nil, message: String.adamant.shared.photolibraryNotAuthorized) - @unknown default: - break + <<< QrRow { + $0.tag = Rows.qr.tag + $0.cell.tipLabel.text = String.adamant.qrGenerator.tapToSaveTip + }.onCellSelection { [weak self] (cell, row) in + if let tableView = self?.tableView, let indexPath = tableView.indexPathForSelectedRow { + tableView.deselectRow(at: indexPath, animated: true) } - }) - - let share = UIAlertAction(title: String.adamant.alert.share, style: .default, handler: { _ in - let vc = UIActivityViewController(activityItems: [qr], applicationActivities: nil) - vc.excludedActivityTypes = ShareContentType.passphrase.excludedActivityTypes - vc.completionWithItemsHandler = { (_, completed: Bool, _, error: Error?) in - if completed { - self?.dialogService.showSuccess(withMessage: String.adamant.alert.done) - } else if let error = error { - self?.dialogService.showToastMessage(error.localizedDescription) - } + + guard let qr = row.value else { + return } - vc.modalPresentationStyle = .overFullScreen - self?.present(vc, animated: true, completion: nil) - }) - - let cancel = UIAlertAction(title: String.adamant.alert.cancel, style: .cancel, handler: nil) - - let alert = UIAlertController( - title: nil, - message: nil, - preferredStyleSafe: .actionSheet, - source: .view(cell) - ) - - alert.addAction(save) - alert.addAction(share) - alert.addAction(cancel) - alert.modalPresentationStyle = .overFullScreen - self?.present(alert, animated: true, completion: nil) - } - + + let save = UIAlertAction( + title: String.adamant.alert.saveToPhotolibrary, + style: .default, + handler: { _ in + + switch PHPhotoLibrary.authorizationStatus() { + case .authorized, .limited: + UIImageWriteToSavedPhotosAlbum(qr, self, #selector(self?.image(_:didFinishSavingWithError:contextInfo:)), nil) + + case .notDetermined: + UIImageWriteToSavedPhotosAlbum(qr, self, #selector(self?.image(_:didFinishSavingWithError:contextInfo:)), nil) + + case .restricted, .denied: + self?.dialogService.presentGoToSettingsAlert(title: nil, message: String.adamant.shared.photolibraryNotAuthorized) + @unknown default: + break + } + } + ) + + let share = UIAlertAction( + title: String.adamant.alert.share, + style: .default, + handler: { _ in + let vc = UIActivityViewController(activityItems: [qr], applicationActivities: nil) + vc.excludedActivityTypes = ShareContentType.passphrase.excludedActivityTypes + vc.completionWithItemsHandler = { (_, completed: Bool, _, error: Error?) in + if completed { + self?.dialogService.showSuccess(withMessage: String.adamant.alert.done) + } else if let error = error { + self?.dialogService.showToastMessage(error.localizedDescription) + } + } + vc.modalPresentationStyle = .overFullScreen + self?.present(vc, animated: true, completion: nil) + } + ) + + let cancel = UIAlertAction(title: String.adamant.alert.cancel, style: .cancel, handler: nil) + + let alert = UIAlertController( + title: nil, + message: nil, + preferredStyleSafe: .actionSheet, + source: .view(cell) + ) + + alert.addAction(save) + alert.addAction(share) + alert.addAction(cancel) + alert.modalPresentationStyle = .overFullScreen + self?.present(alert, animated: true, completion: nil) + } + if let section = form.sectionBy(tag: Sections.qr.tag) { section.hidden = Condition.predicate(NSPredicate(format: "$\(Rows.qr.tag) == nil")) section.evaluateHidden() } - + // MARK: Passphrase section form +++ Section { $0.tag = Sections.passphrase.tag } - <<< PasswordRow { - $0.placeholder = String.adamant.qrGenerator.passphrasePlaceholder - $0.cell.textField.enablePasswordToggle() - $0.tag = Rows.passphrase.tag - } - - <<< ButtonRow { - $0.title = String.adamant.alert.generateQr - $0.tag = Rows.generateButton.tag - }.onCellSelection { [weak self] (_, _) in - self?.generateQr() - } - + <<< PasswordRow { + $0.placeholder = String.adamant.qrGenerator.passphrasePlaceholder + $0.cell.textField.enablePasswordToggle() + $0.tag = Rows.passphrase.tag + } + + <<< ButtonRow { + $0.title = String.adamant.alert.generateQr + $0.tag = Rows.generateButton.tag + }.onCellSelection { [weak self] (_, _) in + self?.generateQr() + } + setColors() } - + override func insertAnimation(forSections sections: [Section]) -> UITableView.RowAnimation { return .top } - + override func insertAnimation(forRows rows: [BaseRow]) -> UITableView.RowAnimation { return .top } - + @objc private func image(_ image: UIImage, didFinishSavingWithError error: NSError?, contextInfo: UnsafeRawPointer) { if error != nil { dialogService.presentGoToSettingsAlert(title: String.adamant.shared.photolibraryNotAuthorized, message: nil) @@ -184,9 +195,9 @@ final class QRGeneratorViewController: FormViewController { dialogService.showSuccess(withMessage: String.adamant.alert.done) } } - + // MARK: - Other - + private func setColors() { view.backgroundColor = UIColor.adamant.secondBackgroundColor tableView.backgroundColor = .clear @@ -197,37 +208,41 @@ final class QRGeneratorViewController: FormViewController { extension QRGeneratorViewController { func generateQr() { guard let row: PasswordRow = form.rowBy(tag: Rows.passphrase.tag), - let passphrase = row.value?.lowercased(), - AdamantUtilities.validateAdamantPassphrase(passphrase: passphrase) + let passphrase = row.value?.lowercased(), + AdamantUtilities.validateAdamantPassphrase(passphrase: passphrase) else { dialogService.showToastMessage(String.adamant.qrGenerator.wrongPassphraseError) return } - + let encodedPassphrase = AdamantUriTools.encode(request: AdamantUri.passphrase(passphrase: passphrase)) - + switch AdamantQRTools.generateQrFrom(string: encodedPassphrase) { case .success(let qr): setQr(image: qr) - + case .failure(let error): - dialogService.showError(withMessage: String.localizedStringWithFormat(String.adamant.qrGenerator.internalError, error.localizedDescription), supportEmail: true, error: error) + dialogService.showError( + withMessage: String.localizedStringWithFormat(String.adamant.qrGenerator.internalError, error.localizedDescription), + supportEmail: true, + error: error + ) } } - + func setQr(image: UIImage?) { guard let row: QrRow = form.rowBy(tag: Rows.qr.tag) else { return } - + guard let image = image else { row.value = nil return } - + row.value = image row.updateCell() - + form.sectionBy(tag: Sections.qr.tag)?.evaluateHidden() } } diff --git a/Adamant/Modules/Settings/SecurityViewController+StayIn.swift b/Adamant/Modules/Settings/SecurityViewController+StayIn.swift index af2f0db0a..b1a4344d8 100644 --- a/Adamant/Modules/Settings/SecurityViewController+StayIn.swift +++ b/Adamant/Modules/Settings/SecurityViewController+StayIn.swift @@ -6,18 +6,18 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation +import CommonKit import Eureka +import Foundation import MyLittlePinpad -import CommonKit extension SecurityViewController { func setStayLoggedIn(enabled: Bool) { guard accountService.hasStayInAccount != enabled else { return } - - if enabled { // Create pin and turn on Stay In + + if enabled { // Create pin and turn on Stay In pinpadRequest = .createPin let pinpad = PinpadViewController.adamantPinpad(biometryButton: .hidden) pinpad.commentLabel.text = String.adamant.pinpad.createPin @@ -25,7 +25,7 @@ extension SecurityViewController { pinpad.delegate = self pinpad.modalPresentationStyle = .overFullScreen present(pinpad, animated: true, completion: nil) - } else { // Validate pin and turn off Stay In + } else { // Validate pin and turn off Stay In pinpadRequest = .turnOffPin let biometryButton: PinpadBiometryButtonType = accountService.useBiometry ? localAuth.biometryType.pinpadButtonType : .hidden let pinpad = PinpadViewController.adamantPinpad(biometryButton: biometryButton) @@ -36,20 +36,21 @@ extension SecurityViewController { present(pinpad, animated: true, completion: nil) } } - + // MARK: Use biometry func setBiometry(enabled: Bool) { guard showLoggedInOptions, accountService.hasStayInAccount, accountService.useBiometry != enabled else { return } - + let reason = enabled ? String.adamant.security.biometryOnReason : String.adamant.security.biometryOffReason - localAuth.authorizeUser(reason: reason) { [weak self] result in + Task { @MainActor in + let result = await localAuth.authorizeUser(reason: reason) switch result { case .success: - self?.dialogService.showSuccess(withMessage: String.adamant.alert.done) - self?.accountService.updateUseBiometry(enabled) - + self.dialogService.showSuccess(withMessage: String.adamant.alert.done) + self.accountService.updateUseBiometry(enabled) + case .cancel: DispatchQueue.main.async { [weak self] in if let row: SwitchRow = self?.form.rowBy(tag: Rows.biometry.tag) { @@ -57,43 +58,40 @@ extension SecurityViewController { row.updateCell() } } - + case .fallback: let pinpad = PinpadViewController.adamantPinpad(biometryButton: .hidden) - + if enabled { pinpad.commentLabel.text = String.adamant.security.biometryOnReason - self?.pinpadRequest = .turnOnBiometry + self.pinpadRequest = .turnOnBiometry } else { pinpad.commentLabel.text = String.adamant.security.biometryOffReason - self?.pinpadRequest = .turnOffBiometry + self.pinpadRequest = .turnOffBiometry } - + pinpad.commentLabel.isHidden = false pinpad.delegate = self - + DispatchQueue.main.async { pinpad.modalPresentationStyle = .overFullScreen - self?.present(pinpad, animated: true, completion: nil) + self.present(pinpad, animated: true, completion: nil) } - + case .failed: DispatchQueue.main.async { - if let row: SwitchRow = self?.form.rowBy(tag: Rows.biometry.tag) { - if let value = self?.accountService.useBiometry { - row.value = value - } else { - row.value = false - } - + if let row: SwitchRow = self.form.rowBy(tag: Rows.biometry.tag) { + row.value = self.accountService.useBiometry row.updateCell() row.evaluateHidden() } - - if let section = self?.form.sectionBy(tag: Sections.notifications.tag) { + + if let section = self.form.sectionBy(tag: Sections.notifications.tag) { section.evaluateHidden() } } + case .biometryLockout: + break } } } @@ -104,14 +102,14 @@ extension SecurityViewController: PinpadViewControllerDelegate { nonisolated func pinpad(_ pinpad: PinpadViewController, didEnterPin pin: String) { MainActor.assumeIsolatedSafe { switch pinpadRequest { - + // MARK: User has entered new pin first time. Request re-enter pin case .createPin?: pinpadRequest = .reenterPin(pin: pin) pinpad.commentLabel.text = String.adamant.pinpad.reenterPin pinpad.clearPin() return - + // MARK: User has reentered pin. Save pin. case .reenterPin(let pinToVerify)?: guard pin == pinToVerify else { @@ -119,34 +117,33 @@ extension SecurityViewController: PinpadViewControllerDelegate { pinpad.clearPin() break } - - accountService.setStayLoggedIn(pin: pin) { [weak self] result in - Task { @MainActor in - switch result { - case .success: - self?.pinpadRequest = nil - if let row: SwitchRow = self?.form.rowBy(tag: Rows.biometry.tag) { - row.value = false - row.updateCell() - row.evaluateHidden() - } - - if let section = self?.form.sectionBy(tag: Sections.notifications.tag) { - section.evaluateHidden() - } - - if let section = self?.form.sectionBy(tag: Sections.aboutNotificationTypes.tag) { - section.evaluateHidden() - } - - pinpad.dismiss(animated: true, completion: nil) - - case .failure(let error): - self?.dialogService.showRichError(error: error) + + let result = accountService.setStayLoggedIn(pin: pin) + Task { @MainActor in + switch result { + case .success: + self.pinpadRequest = nil + if let row: SwitchRow = self.form.rowBy(tag: Rows.biometry.tag) { + row.value = false + row.updateCell() + row.evaluateHidden() + } + + if let section = self.form.sectionBy(tag: Sections.notifications.tag) { + section.evaluateHidden() } + + if let section = self.form.sectionBy(tag: Sections.aboutNotificationTypes.tag) { + section.evaluateHidden() + } + + pinpad.dismiss(animated: true, completion: nil) + + case .failure(let error): + self.dialogService.showRichError(error: error) } } - + // MARK: Users want to turn off the pin. Validate and turn off. case .turnOffPin?: guard accountService.validatePin(pin) else { @@ -154,11 +151,11 @@ extension SecurityViewController: PinpadViewControllerDelegate { pinpad.clearPin() break } - + accountService.dropSavedAccount() - + pinpad.dismiss(animated: true, completion: nil) - + // MARK: User wants to turn on biometry case .turnOnBiometry?: guard accountService.validatePin(pin) else { @@ -166,10 +163,10 @@ extension SecurityViewController: PinpadViewControllerDelegate { pinpad.clearPin() break } - + accountService.updateUseBiometry(true) pinpad.dismiss(animated: true, completion: nil) - + // MARK: User wants to turn off biometry case .turnOffBiometry?: guard accountService.validatePin(pin) else { @@ -177,89 +174,85 @@ extension SecurityViewController: PinpadViewControllerDelegate { pinpad.clearPin() break } - + accountService.updateUseBiometry(false) pinpad.dismiss(animated: true, completion: nil) - + default: pinpad.dismiss(animated: true, completion: nil) } } } - + nonisolated func pinpadDidTapBiometryButton(_ pinpad: PinpadViewController) { - MainActor.assumeIsolatedSafe { + Task { @MainActor in switch pinpadRequest { - + // MARK: User wants to turn of StayIn with his face. Or finger. case .turnOffPin?: - localAuth.authorizeUser(reason: String.adamant.security.stayInTurnOff, completion: { [weak self] result in - switch result { - case .success: - self?.accountService.dropSavedAccount() - - DispatchQueue.main.async { - if let row: SwitchRow = self?.form.rowBy(tag: Rows.biometry.tag) { - row.value = false - row.updateCell() - row.evaluateHidden() - } - - if let section = self?.form.sectionBy(tag: Sections.notifications.tag) { - section.evaluateHidden() - } - - pinpad.dismiss(animated: true, completion: nil) + let result = await localAuth.authorizeUser(reason: String.adamant.security.stayInTurnOff) + switch result { + case .success: + self.accountService.dropSavedAccount() + + DispatchQueue.main.async { + if let row: SwitchRow = self.form.rowBy(tag: Rows.biometry.tag) { + row.value = false + row.updateCell() + row.evaluateHidden() } - - case .cancel: break - case .fallback: break - case .failed: break + + if let section = self.form.sectionBy(tag: Sections.notifications.tag) { + section.evaluateHidden() + } + + pinpad.dismiss(animated: true, completion: nil) } - }) - + + case .cancel, .fallback, .failed, .biometryLockout: break + } default: - return + break } } } - + nonisolated func pinpadDidCancel(_ pinpad: PinpadViewController) { MainActor.assumeIsolatedSafe { switch pinpadRequest { - + // MARK: User canceled turning on StayIn case .createPin?, .reenterPin(pin: _)?: if let row: SwitchRow = form.rowBy(tag: Rows.stayIn.tag) { row.value = false row.updateCell() } - + // MARK: User canceled turning off StayIn case .turnOffPin?: if let row: SwitchRow = form.rowBy(tag: Rows.stayIn.tag) { row.value = true row.updateCell() } - + // MARK: User canceled Biometry On case .turnOnBiometry?: if let row: SwitchRow = form.rowBy(tag: Rows.biometry.tag) { row.value = false row.updateCell() } - + // MARK: User canceled Biometry Off case .turnOffBiometry?: if let row: SwitchRow = form.rowBy(tag: Rows.biometry.tag) { row.value = true row.updateCell() } - + default: break } - + pinpadRequest = nil pinpad.dismiss(animated: true, completion: nil) } diff --git a/Adamant/Modules/Settings/SecurityViewController+notifications.swift b/Adamant/Modules/Settings/SecurityViewController+notifications.swift index 51abc3e12..3ae885029 100644 --- a/Adamant/Modules/Settings/SecurityViewController+notifications.swift +++ b/Adamant/Modules/Settings/SecurityViewController+notifications.swift @@ -6,31 +6,31 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import Eureka import CommonKit +import Eureka +import UIKit extension SecurityViewController { func setNotificationMode(_ mode: NotificationsMode) { guard mode != notificationsService.notificationsMode else { return } - + notificationsService.setNotificationsMode(mode) { [weak self] result in switch result { case .success: return - + case .failure(let error): if let row: SwitchRow = self?.form.rowBy(tag: Rows.notificationsMode.tag) { row.value = false row.updateCell() } - + switch error { case .notEnoughMoney, .notStayedLoggedIn: self?.dialogService.showRichError(error: error) - + case .denied: DispatchQueue.main.async { self?.presentNotificationsDeniedError() @@ -39,7 +39,7 @@ extension SecurityViewController { } } } - + private func presentNotificationsDeniedError() { let alert = UIAlertController( title: nil, @@ -47,15 +47,17 @@ extension SecurityViewController { preferredStyleSafe: .alert, source: nil ) - - alert.addAction(UIAlertAction(title: String.adamant.alert.settings, style: .default) { _ in - DispatchQueue.main.async { - if let settingsURL = URL(string: UIApplication.openSettingsURLString) { - UIApplication.shared.open(settingsURL) + + alert.addAction( + UIAlertAction(title: String.adamant.alert.settings, style: .default) { _ in + DispatchQueue.main.async { + if let settingsURL = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(settingsURL) + } } } - }) - + ) + alert.addAction(UIAlertAction(title: String.adamant.alert.cancel, style: .cancel, handler: nil)) alert.modalPresentationStyle = .overFullScreen present(alert, animated: true, completion: nil) diff --git a/Adamant/Modules/Settings/SecurityViewController.swift b/Adamant/Modules/Settings/SecurityViewController.swift index e54d7278b..b965a2d17 100644 --- a/Adamant/Modules/Settings/SecurityViewController.swift +++ b/Adamant/Modules/Settings/SecurityViewController.swift @@ -6,11 +6,11 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation +import CommonKit import Eureka -import SafariServices +import Foundation import MarkdownKit -import CommonKit +import SafariServices // MARK: - Localization extension String.adamant { @@ -18,7 +18,7 @@ extension String.adamant { static var title: String { String.localized("SecurityPage.Title", comment: "Security: scene title") } - + static var stayInTurnOff: String { String.localized("SecurityPage.DoNotStayLoggedIn", comment: "Security: turn off 'Stay Logged In' confirmation") } @@ -40,7 +40,7 @@ extension NotificationsMode: CustomStringConvertible { // MARK: - SecurityViewController final class SecurityViewController: FormViewController { - + enum PinpadRequest { case createPin case reenterPin(pin: String) @@ -48,14 +48,14 @@ final class SecurityViewController: FormViewController { case turnOnBiometry case turnOffBiometry } - + // MARK: - Section & Rows - + enum Sections { case security case notifications case aboutNotificationTypes - + var tag: String { switch self { case .security: return "ss" @@ -63,7 +63,7 @@ final class SecurityViewController: FormViewController { case .aboutNotificationTypes: return "ans" } } - + var localized: String { switch self { case .security: return .localized("SecurityPage.Section.Security", comment: "Security: Security section") @@ -72,12 +72,12 @@ final class SecurityViewController: FormViewController { } } } - + enum Rows { case generateQr, stayIn, biometry case notificationsMode case description, github - + var tag: String { switch self { case .generateQr: return "qr" @@ -88,52 +88,53 @@ final class SecurityViewController: FormViewController { case .github: return "git" } } - + var localized: String { switch self { case .generateQr: return .localized("SecurityPage.Row.GenerateQr", comment: "Security: Generate QR with passphrase row") case .stayIn: return .localized("SecurityPage.Row.StayLoggedIn", comment: "Security: Stay logged option") - case .biometry: return "" // localAuth.biometryType.localized + case .biometry: return "" // localAuth.biometryType.localized case .notificationsMode: return .localized("SecurityPage.Row.Notifications", comment: "Security: Show notifications") - case .description: return .localized("SecurityPage.Row.Notifications.ModesDescription", comment: "Security: Notification modes description. Markdown supported.") + case .description: + return .localized("SecurityPage.Row.Notifications.ModesDescription", comment: "Security: Notification modes description. Markdown supported.") case .github: return .localized("SecurityPage.Row.VisitGithub", comment: "Security: Visit Github") } } } - + // MARK: - Dependencies - + var accountService: AccountService! var dialogService: DialogService! var notificationsService: NotificationsService! var localAuth: LocalAuthentication! var screensFactory: ScreensFactory! - + // MARK: - Properties var showLoggedInOptions: Bool { return accountService.hasStayInAccount } - + var showBiometryOptions: Bool { switch localAuth.biometryType { case .none: return false - + case .touchID, .faceID: return showLoggedInOptions } } - + var pinpadRequest: SecurityViewController.PinpadRequest? - + // MARK: - Lifecycle - + override func viewDidLoad() { super.viewDidLoad() - + navigationItem.title = String.adamant.security.title navigationOptions = .Disabled - + // MARK: StayIn // Generate QR let qrRow = LabelRow { @@ -146,7 +147,7 @@ final class SecurityViewController: FormViewController { guard let vc = self?.screensFactory.makeQRGenerator() else { return } self?.navigationController?.pushViewController(vc, animated: true) } - + // Stay logged in let stayInRow = SwitchRow { $0.tag = Rows.stayIn.tag @@ -156,39 +157,42 @@ final class SecurityViewController: FormViewController { guard let enabled = row.value else { return } - + self?.setStayLoggedIn(enabled: enabled) }.cellUpdate({ (cell, _) in cell.accessoryType = .disclosureIndicator }) - + // Biometry let biometryRow = SwitchRow { $0.tag = Rows.biometry.tag $0.title = localAuth.biometryType.localized $0.value = accountService.useBiometry - - $0.hidden = Condition.function([], { [weak self] _ -> Bool in - guard let showBiometry = self?.showBiometryOptions else { - return true + + $0.hidden = Condition.function( + [], + { [weak self] _ -> Bool in + guard let showBiometry = self?.showBiometryOptions else { + return true + } + + return !showBiometry } - - return !showBiometry - }) + ) }.onChange { [weak self] row in let value = row.value ?? false self?.setBiometry(enabled: value) }.cellUpdate({ (cell, _) in cell.accessoryType = .disclosureIndicator }) - + let stayInSection = Section(Sections.security.localized) { $0.tag = Sections.security.tag } - + stayInSection.append(contentsOf: [qrRow, stayInRow, biometryRow]) form.append(stayInSection) - + // MARK: Notifications // Type let nType = ActionSheetRow { @@ -203,22 +207,25 @@ final class SecurityViewController: FormViewController { let mode = row.value ?? NotificationsMode.disabled self?.setNotificationMode(mode) } - + // Section let notificationsSection = Section(Sections.notifications.localized) { $0.tag = Sections.notifications.tag - $0.hidden = Condition.function([], { [weak self] _ -> Bool in - guard let showNotifications = self?.showLoggedInOptions else { - return true + $0.hidden = Condition.function( + [], + { [weak self] _ -> Bool in + guard let showNotifications = self?.showLoggedInOptions else { + return true + } + + return !showNotifications } - - return !showNotifications - }) + ) } - + notificationsSection.append(nType) form.append(notificationsSection) - + // MARK: ANS Description // Description let descriptionRow = TextAreaRow { @@ -227,11 +234,11 @@ final class SecurityViewController: FormViewController { }.cellUpdate { (cell, _) in cell.textView.isSelectable = false cell.textView.isEditable = false - + let parser = MarkdownParser(font: UIFont.systemFont(ofSize: UIFont.systemFontSize)) cell.textView.attributedText = parser.parse(Rows.description.localized) } - + // Github readme let githubRow = LabelRow { $0.tag = Rows.github.tag @@ -246,27 +253,30 @@ final class SecurityViewController: FormViewController { guard let url = URL(string: AdamantResources.ansReadmeUrl) else { fatalError("Failed to build ANS URL") } - + let safari = SFSafariViewController(url: url) safari.preferredControlTintColor = UIColor.adamant.primary safari.modalPresentationStyle = .overFullScreen self?.present(safari, animated: true, completion: nil) } - + let ansSection = Section(Sections.aboutNotificationTypes.localized) { $0.tag = Sections.aboutNotificationTypes.tag - $0.hidden = Condition.function([], { [weak self] _ -> Bool in - guard let showNotifications = self?.showLoggedInOptions else { - return true + $0.hidden = Condition.function( + [], + { [weak self] _ -> Bool in + guard let showNotifications = self?.showLoggedInOptions else { + return true + } + + return !showNotifications } - - return !showNotifications - }) + ) } - + ansSection.append(contentsOf: [descriptionRow, githubRow]) form.append(ansSection) - + // MARK: Notifications NotificationCenter.default.addObserver( forName: Notification.Name.AdamantAccountService.userLoggedIn, @@ -275,7 +285,7 @@ final class SecurityViewController: FormViewController { ) { [weak self] _ in MainActor.assumeIsolatedSafe { self?.reloadForm() } } - + NotificationCenter.default.addObserver( forName: Notification.Name.AdamantAccountService.stayInChanged, object: nil, @@ -283,7 +293,7 @@ final class SecurityViewController: FormViewController { ) { [weak self] _ in MainActor.assumeIsolatedSafe { self?.reloadForm() } } - + NotificationCenter.default.addObserver( forName: Notification.Name.AdamantNotificationService.notificationsModeChanged, object: nil, @@ -293,38 +303,38 @@ final class SecurityViewController: FormViewController { guard let newMode = notification.userInfo?[AdamantUserInfoKey.NotificationsService.newNotificationsMode] as? NotificationsMode else { return } - + guard let row: ActionSheetRow = self?.form.rowBy(tag: Rows.notificationsMode.tag) else { return } - + row.value = newMode row.updateCell() } } } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if let indexPath = tableView.indexPathForSelectedRow { tableView.deselectRow(at: indexPath, animated: animated) } } - + private func reloadForm() { if let row: SwitchRow = form.rowBy(tag: Rows.stayIn.tag) { row.value = accountService.hasStayInAccount } - + if let row: SwitchRow = form.rowBy(tag: Rows.biometry.tag) { row.value = accountService.hasStayInAccount && accountService.useBiometry row.evaluateHidden() } - + if let section = form.sectionBy(tag: Sections.notifications.tag) { section.evaluateHidden() } - + if let section = form.sectionBy(tag: Sections.aboutNotificationTypes.tag) { section.evaluateHidden() } diff --git a/Adamant/Modules/Settings/SettingsFactory.swift b/Adamant/Modules/Settings/SettingsFactory.swift index e891df930..32abc40ec 100644 --- a/Adamant/Modules/Settings/SettingsFactory.swift +++ b/Adamant/Modules/Settings/SettingsFactory.swift @@ -6,14 +6,14 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit import CommonKit import Swinject +import UIKit @MainActor struct SettingsFactory { let assembler: Assembler - + func makeSecurityVC(screensFactory: ScreensFactory) -> UIViewController { let c = SecurityViewController() c.accountService = assembler.resolve(AccountService.self) @@ -23,13 +23,13 @@ struct SettingsFactory { c.screensFactory = screensFactory return c } - + func makeQRGeneratorVC() -> UIViewController { let c = QRGeneratorViewController() c.dialogService = assembler.resolve(DialogService.self) return c } - + func makeAboutVC(screensFactory: ScreensFactory) -> UIViewController { AboutViewController( accountService: assembler.resolve(AccountService.self)!, @@ -39,11 +39,12 @@ struct SettingsFactory { vibroService: assembler.resolve(VibroService.self)! ) } - + func makeVisibleWalletsVC() -> UIViewController { VisibleWalletsViewController( visibleWalletsService: assembler.resolve(VisibleWalletsService.self)!, - accountService: assembler.resolve(AccountService.self)! + accountService: assembler.resolve(AccountService.self)!, + walletsStoreService: assembler.resolve(WalletStoreServiceProviderProtocol.self)! ) } } diff --git a/Adamant/Modules/Settings/VisibleWallets/VisibleWalletsCheckmarkView.swift b/Adamant/Modules/Settings/VisibleWallets/VisibleWalletsCheckmarkView.swift index 944f0430c..b1a2ade6b 100644 --- a/Adamant/Modules/Settings/VisibleWallets/VisibleWalletsCheckmarkView.swift +++ b/Adamant/Modules/Settings/VisibleWallets/VisibleWalletsCheckmarkView.swift @@ -6,9 +6,9 @@ // Copyright © 2022 Adamant. All rights reserved. // +import CommonKit import SnapKit import UIKit -import CommonKit final class VisibleWalletsCheckmarkRowView: UIView { private let checkmarkView = CheckmarkView() @@ -17,9 +17,9 @@ final class VisibleWalletsCheckmarkRowView: UIView { private let captionLabel = makeCaptionLabel() private let logoImageView = UIImageView() private let balanceLabel = makeAdditionalLabel() - + private let awaitingValueString = "⏱" - + private lazy var horizontalStack: UIStackView = { let stack = UIStackView(arrangedSubviews: [captionLabel, subtitleLabel]) stack.axis = .horizontal @@ -28,22 +28,22 @@ final class VisibleWalletsCheckmarkRowView: UIView { stack.spacing = 6 return stack }() - + var title: String? { get { titleLabel.text } set { titleLabel.text = newValue } } - + var subtitle: String? { get { subtitleLabel.text } set { subtitleLabel.text = newValue } } - + var caption: String? { get { captionLabel.text } set { captionLabel.text = newValue } } - + var balance: Decimal? { didSet { if let balance = balance { @@ -59,26 +59,26 @@ final class VisibleWalletsCheckmarkRowView: UIView { } } } - + var onCheckmarkTap: (() -> Void)? { get { checkmarkView.onCheckmarkTap } set { checkmarkView.onCheckmarkTap = newValue } } - + var checkmarkImage: UIImage? { get { checkmarkView.image } set { checkmarkView.image = newValue } } - + var logoImage: UIImage? { get { logoImageView.image } set { logoImageView.image = newValue } } - + var isChecked: Bool { checkmarkView.isChecked } - + var checkmarkImageBorderColor: UIColor? { get { guard let imageBorderColor = checkmarkView.imageBorderColor else { return nil } @@ -86,26 +86,26 @@ final class VisibleWalletsCheckmarkRowView: UIView { } set { checkmarkView.imageBorderColor = newValue?.cgColor } } - + var checkmarkImageTintColor: UIColor? { get { checkmarkView.imageTintColor } set { checkmarkView.imageTintColor = newValue } } - + init() { super.init(frame: .zero) setupView() } - + required init?(coder: NSCoder) { super.init(coder: coder) setupView() } - + func setIsChecked(_ isChecked: Bool, animated: Bool) { checkmarkView.setIsChecked(isChecked, animated: animated) } - + private func setupView() { addSubview(logoImageView) logoImageView.snp.makeConstraints { @@ -113,25 +113,25 @@ final class VisibleWalletsCheckmarkRowView: UIView { $0.centerY.equalToSuperview() $0.leading.equalToSuperview().inset(8) } - + addSubview(checkmarkView) checkmarkView.snp.makeConstraints { $0.size.equalTo(44) $0.top.trailing.bottom.equalToSuperview().inset(2) } - + addSubview(titleLabel) titleLabel.snp.makeConstraints { $0.top.equalTo(checkmarkView) $0.leading.equalTo(logoImageView.snp.trailing).offset(8) } - + addSubview(horizontalStack) horizontalStack.snp.makeConstraints { $0.top.equalTo(titleLabel.snp.bottom).offset(2) $0.leading.equalTo(titleLabel) } - + addSubview(balanceLabel) balanceLabel.contentMode = .left balanceLabel.snp.makeConstraints { diff --git a/Adamant/Modules/Settings/VisibleWallets/VisibleWalletsResetTableViewCell.swift b/Adamant/Modules/Settings/VisibleWallets/VisibleWalletsResetTableViewCell.swift index f4527162b..60021a417 100644 --- a/Adamant/Modules/Settings/VisibleWallets/VisibleWalletsResetTableViewCell.swift +++ b/Adamant/Modules/Settings/VisibleWallets/VisibleWalletsResetTableViewCell.swift @@ -6,8 +6,8 @@ // Copyright © 2022 Adamant. All rights reserved. // -import UIKit import CommonKit +import UIKit final class VisibleWalletsResetTableViewCell: UITableViewCell { @@ -17,17 +17,17 @@ final class VisibleWalletsResetTableViewCell: UITableViewCell { label.text = .adamant.visibleWallets.reset return label }() - + required override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupView() } - + required init?(coder: NSCoder) { super.init(coder: coder) setupView() } - + func setupView() { contentView.addSubview(titleLabel) titleLabel.snp.makeConstraints { diff --git a/Adamant/Modules/Settings/VisibleWallets/VisibleWalletsTableViewCell.swift b/Adamant/Modules/Settings/VisibleWallets/VisibleWalletsTableViewCell.swift index e3b7b676c..5c32a4451 100644 --- a/Adamant/Modules/Settings/VisibleWallets/VisibleWalletsTableViewCell.swift +++ b/Adamant/Modules/Settings/VisibleWallets/VisibleWalletsTableViewCell.swift @@ -6,8 +6,8 @@ // Copyright © 2022 Adamant. All rights reserved. // -import UIKit import SnapKit +import UIKit // MARK: Cell's Delegate @MainActor @@ -18,7 +18,7 @@ protocol AdamantVisibleWalletsCellDelegate: AnyObject { // MARK: - Cell final class VisibleWalletsTableViewCell: UITableViewCell { private let checkmarkRowView = VisibleWalletsCheckmarkRowView() - + weak var delegate: AdamantVisibleWalletsCellDelegate? { didSet { checkmarkRowView.onCheckmarkTap = { [weak self] in @@ -29,53 +29,53 @@ final class VisibleWalletsTableViewCell: UITableViewCell { } } } - + var title: String? { get { checkmarkRowView.title } set { checkmarkRowView.title = newValue } } - + var subtitle: String? { get { checkmarkRowView.subtitle } set { checkmarkRowView.subtitle = newValue } } - + var isChecked: Bool { get { checkmarkRowView.isChecked } set { checkmarkRowView.setIsChecked(newValue, animated: false) } } - + var caption: String? { get { checkmarkRowView.caption } set { checkmarkRowView.caption = newValue } } - + var logoImage: UIImage? { get { checkmarkRowView.logoImage } set { checkmarkRowView.logoImage = newValue } } - + var balance: Decimal? { get { checkmarkRowView.balance } set { checkmarkRowView.balance = newValue } } - + var unicId: String? - + required override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupView() } - + required init?(coder: NSCoder) { super.init(coder: coder) setupView() } - + func setupView() { checkmarkRowView.checkmarkImage = .asset(named: "status_success") checkmarkRowView.checkmarkImageBorderColor = UIColor.adamant.secondary - + contentView.addSubview(checkmarkRowView) checkmarkRowView.snp.makeConstraints { $0.directionalEdges.equalToSuperview() diff --git a/Adamant/Modules/Settings/VisibleWallets/VisibleWalletsViewController.swift b/Adamant/Modules/Settings/VisibleWallets/VisibleWalletsViewController.swift index 302ada641..108d1550f 100644 --- a/Adamant/Modules/Settings/VisibleWallets/VisibleWalletsViewController.swift +++ b/Adamant/Modules/Settings/VisibleWallets/VisibleWalletsViewController.swift @@ -6,9 +6,10 @@ // Copyright © 2022 Adamant. All rights reserved. // -import UIKit -import SnapKit +import Combine import CommonKit +import SnapKit +import UIKit // MARK: - Localization extension String.adamant { @@ -39,7 +40,7 @@ final class VisibleWalletsViewController: KeyboardObservingViewController { tableView.refreshControl = refreshControl return tableView }() - + private lazy var searchController: UISearchController = { let controller = UISearchController(searchResultsController: nil) controller.searchResultsUpdater = self @@ -47,39 +48,47 @@ final class VisibleWalletsViewController: KeyboardObservingViewController { controller.hidesNavigationBarDuringPresentation = true return controller }() - + // MARK: - Dependencies - + var visibleWalletsService: VisibleWalletsService + var walletsStoreService: WalletStoreServiceProviderProtocol var accountService: AccountService - + // MARK: - Properties - + private lazy var refreshControl: UIRefreshControl = { let refreshControl = UIRefreshControl() refreshControl.transform = CGAffineTransform(scaleX: 0.75, y: 0.75) refreshControl.addTarget(self, action: #selector(updateBalances), for: UIControl.Event.valueChanged) return refreshControl }() - + private let cellIdentifier = "cell" private let cellResetIdentifier = "cellReset" private var filteredWallets: [WalletCoreProtocol]? private var wallets: [WalletCoreProtocol] = [] private var previousAppState: UIApplication.State? - + private var subscriptions = Set() // MARK: - Lifecycle - - init(visibleWalletsService: VisibleWalletsService, accountService: AccountService) { + + init( + visibleWalletsService: VisibleWalletsService, + accountService: AccountService, + walletsStoreService: WalletStoreServiceProviderProtocol + ) { self.visibleWalletsService = visibleWalletsService self.accountService = accountService + self.walletsStoreService = walletsStoreService super.init(nibName: nil, bundle: nil) } - + required init?(coder aDecoder: NSCoder) { fatalError() } - + deinit { + NotificationCenter.default.removeObserver(self) + } override func viewDidLoad() { super.viewDidLoad() loadWallets() @@ -88,27 +97,25 @@ final class VisibleWalletsViewController: KeyboardObservingViewController { updateBalances() setColors() } - + private func setColors() { view.backgroundColor = UIColor.adamant.secondBackgroundColor } - + private func addObservers() { - for wallet in wallets { - let notification = wallet.walletUpdatedNotification - - NotificationCenter.default.addObserver( - forName: notification, - object: wallet, - queue: OperationQueue.main - ) { [weak self] _ in - MainActor.assumeIsolatedSafe { + for (index, wallet) in wallets.enumerated() { + NotificationCenter.default.publisher(for: wallet.walletUpdatedNotification, object: wallet) + .removeDuplicates() + .receive(on: DispatchQueue.main) + .debounce(for: .milliseconds(500), scheduler: DispatchQueue.main) + .sink { [weak self] _ in guard let self = self else { return } - self.tableView.reloadData() + let indexPath = IndexPath(row: index, section: 0) + self.tableView.reloadRows(at: [indexPath], with: .none) } - } + .store(in: &subscriptions) } - + NotificationCenter.default.addObserver( forName: UIApplication.didBecomeActiveNotification, object: nil, @@ -116,13 +123,14 @@ final class VisibleWalletsViewController: KeyboardObservingViewController { ) { [weak self] _ in MainActor.assumeIsolatedSafe { if let previousAppState = self?.previousAppState, - previousAppState == .background { + previousAppState == .background + { self?.previousAppState = .active self?.updateBalances() } } } - + NotificationCenter.default.addObserver( forName: UIApplication.willResignActiveNotification, object: nil, @@ -133,50 +141,53 @@ final class VisibleWalletsViewController: KeyboardObservingViewController { } } } - + private func loadWallets() { - wallets = visibleWalletsService.sorted(includeInvisible: true).map { $0.core } + wallets = walletsStoreService.sorted(includeInvisible: true).map { $0.core } } - + @objc private func updateBalances() { refreshControl.endRefreshing() NotificationCenter.default.post(name: .AdamantAccountService.forceUpdateAllBalances, object: nil) } - + private func setupView() { navigationItem.title = String.adamant.visibleWallets.title navigationItem.searchController = searchController navigationItem.rightBarButtonItem = UIBarButtonItem.init(barButtonSystemItem: .search, target: self, action: #selector(activateSearch)) - + view.addSubview(tableView) tableView.snp.makeConstraints { make in make.top.left.right.bottom.equalToSuperview() } } - + @objc private func activateSearch() { if let bar = navigationItem.searchController?.searchBar, - !bar.isFirstResponder { + !bar.isFirstResponder + { bar.becomeFirstResponder() } } - - private func isInvisible(_ wallet: WalletCoreProtocol) -> Bool { - return visibleWalletsService.isInvisible(wallet) + + private func isInvisible(_ walletTokenUniqueID: String) -> Bool { + return visibleWalletsService.isInvisible(walletTokenUniqueID) } - + private func resetWalletsAction() { let alert = UIAlertController(title: String.adamant.visibleWallets.resetAlertTitle, message: nil, preferredStyleSafe: .alert, source: nil) alert.addAction(UIAlertAction(title: String.adamant.alert.cancel, style: .cancel, handler: nil)) - alert.addAction(UIAlertAction( - title: .adamant.visibleWallets.reset, - style: .destructive, - handler: { [weak self] _ in - self?.visibleWalletsService.reset() - self?.loadWallets() - self?.tableView.reloadSections(IndexSet(integer: 0), with: .automatic) - } - )) + alert.addAction( + UIAlertAction( + title: .adamant.visibleWallets.reset, + style: .destructive, + handler: { [weak self] _ in + self?.visibleWalletsService.reset() + self?.loadWallets() + self?.tableView.reloadSections(IndexSet(integer: 0), with: .automatic) + } + ) + ) alert.modalPresentationStyle = .overFullScreen present(alert, animated: true, completion: nil) } @@ -188,35 +199,35 @@ extension VisibleWalletsViewController: UITableViewDataSource, UITableViewDelega func numberOfSections(in tableView: UITableView) -> Int { sectionsCount } - + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { guard section == 0 else { return 1 } - + if let filtered = filteredWallets { return filtered.count } else { return wallets.count } } - + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { return UIView() } - + func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { return UIView() } - + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { return cellSpacing } - + func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { section == sectionsCount - 1 ? UITableView.automaticDimension : cellSpacing } - + // MARK: Cells func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard indexPath.section == 0 else { @@ -225,18 +236,18 @@ extension VisibleWalletsViewController: UITableViewDataSource, UITableViewDelega cell.backgroundColor = UIColor.adamant.cellColor return cell } - + guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? VisibleWalletsTableViewCell else { return UITableViewCell(style: .default, reuseIdentifier: cellIdentifier) } cell.selectionStyle = .none - + cell.separatorInset = UITableView.defaultSeparatorInset let maxCount = filteredWallets?.count ?? wallets.count if indexPath.row == maxCount - 1 { cell.separatorInset = .zero } - + let wallet: WalletCoreProtocol if let filtered = filteredWallets { wallet = filtered[indexPath.row] @@ -246,7 +257,7 @@ extension VisibleWalletsViewController: UITableViewDataSource, UITableViewDelega let isToken = ERC20Token.supportedTokens.contains(where: { token in return token.symbol == wallet.tokenSymbol }) - + cell.backgroundColor = UIColor.adamant.cellColor cell.title = wallet.tokenName cell.caption = !isToken ? "Blockchain" : type(of: wallet).tokenNetworkSymbol @@ -254,12 +265,12 @@ extension VisibleWalletsViewController: UITableViewDataSource, UITableViewDelega cell.logoImage = wallet.tokenLogo cell.balance = wallet.wallet?.balance cell.delegate = self - cell.isChecked = !isInvisible(wallet) - cell.unicId = wallet.tokenUnicID - + cell.isChecked = !isInvisible(wallet.tokenUniqueID) + cell.unicId = wallet.tokenUniqueID + return cell } - + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let cell = tableView.cellForRow(at: indexPath) as? VisibleWalletsTableViewCell else { @@ -268,45 +279,49 @@ extension VisibleWalletsViewController: UITableViewDataSource, UITableViewDelega return } let wallet = wallets[indexPath.row] - delegateCell(cell, didChangeCheckedStateTo: !isInvisible(wallet)) + delegateCell(cell, didChangeCheckedStateTo: !isInvisible(wallet.tokenUniqueID)) tableView.reloadRows(at: [indexPath], with: .none) } - + func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { return .none } - + func tableView(_ tableView: UITableView, shouldIndentWhileEditingRowAt indexPath: IndexPath) -> Bool { return false } - + func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { guard indexPath.section == 0 else { return false } return true } - - func tableView(_ tableView: UITableView, targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, toProposedIndexPath proposedDestinationIndexPath: IndexPath) -> IndexPath { + + func tableView( + _ tableView: UITableView, + targetIndexPathForMoveFromRowAt sourceIndexPath: IndexPath, + toProposedIndexPath proposedDestinationIndexPath: IndexPath + ) -> IndexPath { guard proposedDestinationIndexPath.section == 0 else { return sourceIndexPath } return proposedDestinationIndexPath } - + func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { guard destinationIndexPath.section == 0 else { return } let wallet = wallets.remove(at: sourceIndexPath.row) wallets.insert(wallet, at: destinationIndexPath.row) - visibleWalletsService.setIndexPositionWallets(wallets, includeInvisible: true) - visibleWalletsService.setIndexPositionWallets(wallets, includeInvisible: false) - NotificationCenter.default.post(name: Notification.Name.AdamantVisibleWalletsService.visibleWallets, object: nil) + let walletsTokenUniqueID = wallets.map { $0.tokenUniqueID } + visibleWalletsService.setIndexPositionWallets(walletsTokenUniqueID, includeInvisible: true) + visibleWalletsService.setIndexPositionWallets(walletsTokenUniqueID, includeInvisible: false) } - + func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { return indexPath.section == .zero && filteredWallets == nil } - + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return indexPath.section == 0 ? UITableView.automaticDimension : 45 } @@ -319,19 +334,19 @@ extension VisibleWalletsViewController: AdamantVisibleWalletsCellDelegate { didChangeCheckedStateTo state: Bool ) { let wallet = wallets.first(where: { - $0.tokenUnicID == cell.unicId + $0.tokenUniqueID == cell.unicId }) - - guard let wallet = wallet else { return } - - if !isInvisible(wallet) { - visibleWalletsService.addToInvisibleWallets(wallet) + + guard let tokenUniqueID = wallet?.tokenUniqueID else { return } + + if !isInvisible(tokenUniqueID) { + visibleWalletsService.addToInvisibleWallets(tokenUniqueID) } else { - visibleWalletsService.removeFromInvisibleWallets(wallet) + visibleWalletsService.removeFromInvisibleWallets(tokenUniqueID) } - visibleWalletsService.setIndexPositionWallets(wallets, includeInvisible: true) - visibleWalletsService.setIndexPositionWallets(wallets, includeInvisible: false) - NotificationCenter.default.post(name: Notification.Name.AdamantVisibleWalletsService.visibleWallets, object: nil) + let walletsTokenUniqueID = wallets.map { $0.tokenUniqueID } + visibleWalletsService.setIndexPositionWallets(walletsTokenUniqueID, includeInvisible: true) + visibleWalletsService.setIndexPositionWallets(walletsTokenUniqueID, includeInvisible: false) } } @@ -345,7 +360,7 @@ extension VisibleWalletsViewController: UISearchResultsUpdating { } else { filteredWallets = nil } - + tableView.reloadData() } } diff --git a/Adamant/Modules/ShareQR/ShareQRFactory.swift b/Adamant/Modules/ShareQR/ShareQRFactory.swift index 2ec561afe..a8d74c5a4 100644 --- a/Adamant/Modules/ShareQR/ShareQRFactory.swift +++ b/Adamant/Modules/ShareQR/ShareQRFactory.swift @@ -12,7 +12,7 @@ import UIKit @MainActor struct ShareQRFactory { let assembler: Assembler - + func makeViewController() -> ShareQrViewController { ShareQrViewController(dialogService: assembler.resolve(DialogService.self)!) } diff --git a/Adamant/Modules/ShareQR/ShareQrViewController.swift b/Adamant/Modules/ShareQR/ShareQrViewController.swift index 46510af80..2f613992e 100644 --- a/Adamant/Modules/ShareQR/ShareQrViewController.swift +++ b/Adamant/Modules/ShareQR/ShareQrViewController.swift @@ -6,27 +6,28 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit +import CommonKit import Eureka import Photos -import CommonKit +import UIKit extension String.adamant.shared { - static var photolibraryNotAuthorized: String { String.localized("ShareQR.photolibraryNotAuthorized", comment: "ShareQR scene: User had not authorized access to write images to photolibrary") + static var photolibraryNotAuthorized: String { + String.localized("ShareQR.photolibraryNotAuthorized", comment: "ShareQR scene: User had not authorized access to write images to photolibrary") } } final class ShareQrViewController: FormViewController { // MARK: - Dependencies private let dialogService: DialogService - + // MARK: - Rows private enum Rows { case qr case saveToPhotos case shareButton case cancelButton - + var tag: String { switch self { case .qr: return "qr" @@ -35,24 +36,24 @@ final class ShareQrViewController: FormViewController { case .cancelButton: return "cl" } } - + var localized: String { switch self { case .qr: return "" - + case .saveToPhotos: return String.adamant.alert.saveToPhotolibrary - + case .shareButton: return String.adamant.alert.share - + case .cancelButton: return String.adamant.alert.cancel } } } - + // MARK: - Properties var qrCode: UIImage? { didSet { @@ -61,7 +62,7 @@ final class ShareQrViewController: FormViewController { } } } - + var sharingTip: String? { didSet { if let row: QrRow = form.rowBy(tag: Rows.qr.tag) { @@ -77,30 +78,30 @@ final class ShareQrViewController: FormViewController { } } } - + var excludedActivityTypes: [UIActivity.ActivityType]? - + init(dialogService: DialogService) { self.dialogService = dialogService super.init(nibName: "ShareQrViewController", bundle: nil) } - + required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() - + // MARK: QR code let qrSection = Section() - + let qrRow = QrRow { $0.value = qrCode $0.tag = Rows.qr.tag $0.cell.selectionStyle = .none - + if let sharingTip = sharingTip { $0.cell.tipLabel.text = sharingTip $0.cell.tipLabel.lineBreakMode = .byTruncatingMiddle @@ -108,16 +109,16 @@ final class ShareQrViewController: FormViewController { $0.cell.tipLabelIsHidden = true } } - + if UIScreen.main.traitCollection.userInterfaceIdiom == .pad { qrRow.cell.height = { 450.0 } } - + qrSection.append(qrRow) - + // MARK: Buttons let buttonsSection = Section() - + // Photolibrary let photolibraryRow = ButtonRow { $0.tag = Rows.saveToPhotos.tag @@ -126,21 +127,21 @@ final class ShareQrViewController: FormViewController { guard let row: QrRow = self?.form.rowBy(tag: Rows.qr.tag), let qrCode = row.value else { return } - + switch PHPhotoLibrary.authorizationStatus() { case .authorized, .limited: - UIImageWriteToSavedPhotosAlbum(qrCode, self, #selector(self?.image(_: didFinishSavingWithError: contextInfo:)), nil) - + UIImageWriteToSavedPhotosAlbum(qrCode, self, #selector(self?.image(_:didFinishSavingWithError:contextInfo:)), nil) + case .notDetermined: - UIImageWriteToSavedPhotosAlbum(qrCode, self, #selector(self?.image(_: didFinishSavingWithError: contextInfo:)), nil) - + UIImageWriteToSavedPhotosAlbum(qrCode, self, #selector(self?.image(_:didFinishSavingWithError:contextInfo:)), nil) + case .restricted, .denied: self?.dialogService.presentGoToSettingsAlert(title: nil, message: String.adamant.shared.photolibraryNotAuthorized) @unknown default: break } } - + // Share let shareRow = ButtonRow { $0.tag = Rows.shareButton.tag @@ -149,17 +150,17 @@ final class ShareQrViewController: FormViewController { guard let row: QrRow = self?.form.rowBy(tag: Rows.qr.tag), let qrCode = row.value else { return } - + let vc = UIActivityViewController(activityItems: [qrCode], applicationActivities: nil) if let excludedActivityTypes = self?.excludedActivityTypes { vc.excludedActivityTypes = excludedActivityTypes } - + if let c = vc.popoverPresentationController { c.sourceView = cell c.sourceRect = cell.bounds } - + vc.completionWithItemsHandler = { [weak self] (_: UIActivity.ActivityType?, completed: Bool, _, error: Error?) in if completed { if let error = error { @@ -173,19 +174,19 @@ final class ShareQrViewController: FormViewController { vc.modalPresentationStyle = .overFullScreen self?.present(vc, animated: true, completion: nil) } - + let cancelRow = ButtonRow { $0.tag = Rows.cancelButton.tag $0.title = Rows.cancelButton.localized }.onCellSelection { [weak self] (_, _) in self?.close() } - + buttonsSection.append(contentsOf: [photolibraryRow, shareRow, cancelRow]) - + form.append(contentsOf: [qrSection, buttonsSection]) } - + func close() { if let nav = navigationController { nav.popViewController(animated: true) @@ -193,7 +194,7 @@ final class ShareQrViewController: FormViewController { dismiss(animated: true, completion: nil) } } - + @objc private func image(_ image: UIImage, didFinishSavingWithError error: NSError?, contextInfo: UnsafeRawPointer) { if error != nil { dialogService.presentGoToSettingsAlert(title: String.adamant.shared.photolibraryNotAuthorized, message: nil) diff --git a/Adamant/Modules/StorageUsage/StorageUsageFactory.swift b/Adamant/Modules/StorageUsage/StorageUsageFactory.swift index b097924ed..3df8d4aba 100644 --- a/Adamant/Modules/StorageUsage/StorageUsageFactory.swift +++ b/Adamant/Modules/StorageUsage/StorageUsageFactory.swift @@ -6,18 +6,18 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Swinject -import SwiftUI import FilesStorageKit +import SwiftUI +import Swinject @MainActor struct StorageUsageFactory { private let assembler: Assembler - + init(parent: Assembler) { assembler = .init([StorageUsageAssembly()], parent: parent) } - + func makeViewController() -> UIViewController { UIHostingController( rootView: StorageUsageView { @@ -32,7 +32,7 @@ private struct StorageUsageAssembly: MainThreadAssembly { container.register(StorageUsageViewModel.self) { StorageUsageViewModel( filesStorage: $0.resolve(FilesStorageProtocol.self)!, - dialogService: $0.resolve(DialogService.self)!, + dialogService: $0.resolve(DialogService.self)!, filesStorageProprieties: $0.resolve(FilesStorageProprietiesProtocol.self)! ) }.inObjectScope(.weak) diff --git a/Adamant/Modules/StorageUsage/StorageUsageView.swift b/Adamant/Modules/StorageUsage/StorageUsageView.swift index daab84c31..ff05514d7 100644 --- a/Adamant/Modules/StorageUsage/StorageUsageView.swift +++ b/Adamant/Modules/StorageUsage/StorageUsageView.swift @@ -6,17 +6,17 @@ // Copyright © 2024 Adamant. All rights reserved. // -import SwiftUI -import CommonKit import Charts +import CommonKit +import SwiftUI struct StorageUsageView: View { @StateObject private var viewModel: StorageUsageViewModel - + init(viewModel: @escaping () -> StorageUsageViewModel) { _viewModel = .init(wrappedValue: viewModel()) } - + var body: some View { VStack { List { @@ -26,9 +26,9 @@ struct StorageUsageView: View { } .listStyle(.insetGrouped) .navigationTitle(storageTitle) - + Spacer() - + makeClearButton() } .alert( @@ -46,8 +46,8 @@ struct StorageUsageView: View { } } -private extension StorageUsageView { - var storageSection: some View { +extension StorageUsageView { + fileprivate var storageSection: some View { Section( content: { content @@ -56,8 +56,8 @@ private extension StorageUsageView { footer: { Text(verbatim: storageDescription) } ) } - - var autoDownloadSection: some View { + + fileprivate var autoDownloadSection: some View { Section( content: { autoDownloadContent(for: .preview) @@ -69,8 +69,8 @@ private extension StorageUsageView { footer: { Text(verbatim: autDownloadDescription) } ) } - - var saveEncryptedSection: some View { + + fileprivate var saveEncryptedSection: some View { Section( content: { Toggle( @@ -87,8 +87,8 @@ private extension StorageUsageView { footer: { Text(verbatim: saveEncryptedDescription) } ) } - - var content: some View { + + fileprivate var content: some View { HStack { Image(uiImage: storageImage) Text(verbatim: storageUsedTitle) @@ -101,8 +101,8 @@ private extension StorageUsageView { } } } - - func autoDownloadContent( + + fileprivate func autoDownloadContent( for type: StorageUsageViewModel.AutoDownloadMediaType ) -> some View { Button { @@ -111,22 +111,22 @@ private extension StorageUsageView { HStack { Image(uiImage: previewImage) Text(type.title) - + Spacer() - + switch type { case .preview: Text(viewModel.autoDownloadPreview.title) case .fullMedia: Text(viewModel.autoDownloadFullMedia.title) } - + NavigationLink(destination: { EmptyView() }, label: { EmptyView() }).fixedSize() } } } - - func makeClearButton() -> some View { + + fileprivate func makeClearButton() -> some View { Button(action: showClearAlert) { Text(clearTitle) .expanded(axes: .horizontal) @@ -137,8 +137,8 @@ private extension StorageUsageView { .clipShape(.rect(cornerRadius: 8.0)) .padding() } - - func showClearAlert() { + + fileprivate func showClearAlert() { viewModel.isRemoveAlertShown = true } } diff --git a/Adamant/Modules/StorageUsage/StorageUsageViewModel.swift b/Adamant/Modules/StorageUsage/StorageUsageViewModel.swift index 1de5afb90..9911e31dc 100644 --- a/Adamant/Modules/StorageUsage/StorageUsageViewModel.swift +++ b/Adamant/Modules/StorageUsage/StorageUsageViewModel.swift @@ -6,17 +6,17 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit -import SwiftUI import FilesStorageKit +import Foundation +import SwiftUI @MainActor final class StorageUsageViewModel: ObservableObject { private let filesStorage: FilesStorageProtocol private let dialogService: DialogService private let filesStorageProprieties: FilesStorageProprietiesProtocol - + @Published var storageUsedDescription: String? @Published var autoDownloadPreview: DownloadPolicy = .everybody @Published var autoDownloadFullMedia: DownloadPolicy = .everybody @@ -26,7 +26,7 @@ final class StorageUsageViewModel: ObservableObject { enum AutoDownloadMediaType { case preview case fullMedia - + var title: String { switch self { case .preview: @@ -36,7 +36,7 @@ final class StorageUsageViewModel: ObservableObject { } } } - + init( filesStorage: FilesStorageProtocol, dialogService: DialogService, @@ -46,19 +46,19 @@ final class StorageUsageViewModel: ObservableObject { self.dialogService = dialogService self.filesStorageProprieties = filesStorageProprieties } - + func loadData() { autoDownloadPreview = filesStorageProprieties.autoDownloadPreviewPolicy() autoDownloadFullMedia = filesStorageProprieties.autoDownloadFullMediaPolicy() saveEncrypted = filesStorageProprieties.saveFileEncrypted() updateCacheSize() } - + func saveFileEncrypted(_ value: Bool) { filesStorageProprieties.setSaveFileEncrypted(value) saveEncrypted = value } - + func clearStorage() { do { dialogService.showProgress(withMessage: nil, userInteractionEnable: false) @@ -76,11 +76,11 @@ final class StorageUsageViewModel: ObservableObject { ) } } - + func presentPicker(for type: AutoDownloadMediaType) { let action: ((DownloadPolicy) -> Void)? = { [weak self] policy in guard let self = self else { return } - + switch type { case .preview: self.filesStorageProprieties.setAutoDownloadPreview(policy) @@ -91,7 +91,7 @@ final class StorageUsageViewModel: ObservableObject { } NotificationCenter.default.post(name: .Storage.storageProprietiesUpdated, object: nil) } - + dialogService.showAlert( title: nil, message: nil, @@ -116,25 +116,25 @@ final class StorageUsageViewModel: ObservableObject { } } -private extension StorageUsageViewModel { - func updateCacheSize() { +extension StorageUsageViewModel { + fileprivate func updateCacheSize() { Task.detached { [filesStorage] in let size = (try? filesStorage.getCacheSize().get()) ?? .zero - + Task { @MainActor [weak self] in guard let self = self else { return } storageUsedDescription = formatSize(size) } } } - - func formatSize(_ bytes: Int64) -> String { + + fileprivate func formatSize(_ bytes: Int64) -> String { if #available(iOS 16.0, *) { let count = Measurement( value: Double(bytes), unit: UnitInformationStorage.bytes ) - + let style = Measurement.FormatStyle.ByteCount( style: .file, allowedUnits: .all, @@ -142,10 +142,10 @@ private extension StorageUsageViewModel { includesActualByteCount: false, locale: String.locale() ) - + return style.format(count) } - + let formatter = ByteCountFormatter() formatter.allowedUnits = .useAll formatter.countStyle = .file @@ -153,16 +153,16 @@ private extension StorageUsageViewModel { } } -private extension StorageUsageViewModel { - func makeAction(title: String, action: ((UIAlertAction) -> Void)?) -> UIAlertAction { +extension StorageUsageViewModel { + fileprivate func makeAction(title: String, action: ((UIAlertAction) -> Void)?) -> UIAlertAction { .init( title: title, style: .default, handler: action ) } - - func makeCancelAction() -> UIAlertAction { + + fileprivate func makeCancelAction() -> UIAlertAction { .init(title: .adamant.alert.cancel, style: .cancel, handler: nil) } } diff --git a/Adamant/Modules/SwiftyOnboard/SwiftyOnboard.swift b/Adamant/Modules/SwiftyOnboard/SwiftyOnboard.swift index 4af42a2dd..98275338c 100644 --- a/Adamant/Modules/SwiftyOnboard/SwiftyOnboard.swift +++ b/Adamant/Modules/SwiftyOnboard/SwiftyOnboard.swift @@ -10,49 +10,49 @@ import UIKit @MainActor public protocol SwiftyOnboardDataSource: AnyObject { - + func swiftyOnboardBackgroundColorFor(_ swiftyOnboard: SwiftyOnboard, atIndex index: Int) -> UIColor? func swiftyOnboardNumberOfPages(_ swiftyOnboard: SwiftyOnboard) -> Int func swiftyOnboardViewForBackground(_ swiftyOnboard: SwiftyOnboard) -> UIView? func swiftyOnboardPageForIndex(_ swiftyOnboard: SwiftyOnboard, index: Int) -> SwiftyOnboardPage? func swiftyOnboardViewForOverlay(_ swiftyOnboard: SwiftyOnboard) -> SwiftyOnboardOverlay? func swiftyOnboardOverlayForPosition(_ swiftyOnboard: SwiftyOnboard, overlay: SwiftyOnboardOverlay, for position: Double) - + } -public extension SwiftyOnboardDataSource { - - func swiftyOnboardBackgroundColorFor(_ swiftyOnboard: SwiftyOnboard,atIndex index: Int) -> UIColor? { +extension SwiftyOnboardDataSource { + + public func swiftyOnboardBackgroundColorFor(_ swiftyOnboard: SwiftyOnboard, atIndex index: Int) -> UIColor? { return nil } - - func swiftyOnboardViewForBackground(_ swiftyOnboard: SwiftyOnboard) -> UIView? { + + public func swiftyOnboardViewForBackground(_ swiftyOnboard: SwiftyOnboard) -> UIView? { return nil } - - func swiftyOnboardOverlayForPosition(_ swiftyOnboard: SwiftyOnboard, overlay: SwiftyOnboardOverlay, for position: Double) {} - - func swiftyOnboardViewForOverlay(_ swiftyOnboard: SwiftyOnboard) -> SwiftyOnboardOverlay? { + + public func swiftyOnboardOverlayForPosition(_ swiftyOnboard: SwiftyOnboard, overlay: SwiftyOnboardOverlay, for position: Double) {} + + public func swiftyOnboardViewForOverlay(_ swiftyOnboard: SwiftyOnboard) -> SwiftyOnboardOverlay? { return SwiftyOnboardOverlay() } } public protocol SwiftyOnboardDelegate: AnyObject { - + func swiftyOnboard(_ swiftyOnboard: SwiftyOnboard, currentPage index: Int) func swiftyOnboard(_ swiftyOnboard: SwiftyOnboard, leftEdge position: Double) func swiftyOnboard(_ swiftyOnboard: SwiftyOnboard, tapped index: Int) - + } -public extension SwiftyOnboardDelegate { - func swiftyOnboard(_ swiftyOnboard: SwiftyOnboard, currentPage index: Int) {} - func swiftyOnboard(_ swiftyOnboard: SwiftyOnboard, leftEdge position: Double) {} - func swiftyOnboard(_ swiftyOnboard: SwiftyOnboard, tapped index: Int) {} +extension SwiftyOnboardDelegate { + public func swiftyOnboard(_ swiftyOnboard: SwiftyOnboard, currentPage index: Int) {} + public func swiftyOnboard(_ swiftyOnboard: SwiftyOnboard, leftEdge position: Double) {} + public func swiftyOnboard(_ swiftyOnboard: SwiftyOnboard, tapped index: Int) {} } public class SwiftyOnboard: UIView, UIScrollViewDelegate { - + open weak var dataSource: SwiftyOnboardDataSource? { didSet { if let color = dataSource?.swiftyOnboardBackgroundColorFor(self, atIndex: 0) { @@ -61,9 +61,9 @@ public class SwiftyOnboard: UIView, UIScrollViewDelegate { dataSourceSet = true } } - + open weak var delegate: SwiftyOnboardDelegate? - + fileprivate var containerView: UIScrollView = { let scrollView = UIScrollView() scrollView.isPagingEnabled = true @@ -75,25 +75,25 @@ public class SwiftyOnboard: UIView, UIScrollViewDelegate { scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: false) return scrollView }() - + fileprivate var dataSourceSet: Bool = false fileprivate var pageCount = 0 fileprivate var overlay: SwiftyOnboardOverlay? fileprivate var pages = [SwiftyOnboardPage]() - + open var style: SwiftyOnboardStyle = .dark open var shouldSwipe: Bool = true open var fadePages: Bool = true - + public init(frame: CGRect, style: SwiftyOnboardStyle = .dark) { super.init(frame: frame) self.style = style } - + required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } - + private func loadView() { setBackgroundView() setUpContainerView() @@ -101,7 +101,7 @@ public class SwiftyOnboard: UIView, UIScrollViewDelegate { setOverlayView() containerView.isScrollEnabled = shouldSwipe } - + override open func layoutSubviews() { super.layoutSubviews() if dataSourceSet { @@ -109,7 +109,7 @@ public class SwiftyOnboard: UIView, UIScrollViewDelegate { dataSourceSet = false } } - + fileprivate func setUpContainerView() { self.addSubview(containerView) self.containerView.frame = self.frame @@ -117,7 +117,7 @@ public class SwiftyOnboard: UIView, UIScrollViewDelegate { let tap = UITapGestureRecognizer(target: self, action: #selector(tappedPage)) containerView.addGestureRecognizer(tap) } - + fileprivate func setBackgroundView() { if let dataSource = dataSource { if let background = dataSource.swiftyOnboardViewForBackground(self) { @@ -126,7 +126,7 @@ public class SwiftyOnboard: UIView, UIScrollViewDelegate { } } } - + fileprivate func setUpPages() { if let dataSource = dataSource { pageCount = dataSource.swiftyOnboardNumberOfPages(self) @@ -144,7 +144,7 @@ public class SwiftyOnboard: UIView, UIScrollViewDelegate { containerView.contentSize = CGSize(width: self.frame.width * CGFloat(pageCount), height: self.frame.height) } } - + fileprivate func setOverlayView() { if let dataSource = dataSource { if let overlay = dataSource.swiftyOnboardViewForOverlay(self) { @@ -158,92 +158,101 @@ public class SwiftyOnboard: UIView, UIScrollViewDelegate { } } } - + @objc internal func tappedPage() { let currentpage = Int(getCurrentPosition()) self.delegate?.swiftyOnboard(self, tapped: currentpage) } - + fileprivate func getCurrentPosition() -> CGFloat { let boundsWidth = containerView.bounds.width let contentOffset = containerView.contentOffset.x let currentPosition = contentOffset / boundsWidth return currentPosition } - + fileprivate func colorForPosition(_ pos: CGFloat) -> UIColor? { let percentage: CGFloat = pos - CGFloat(Int(pos)) - + let currentIndex = Int(pos - percentage) - + if currentIndex < pageCount - 1 { let color1 = dataSource?.swiftyOnboardBackgroundColorFor(self, atIndex: currentIndex) let color2 = dataSource?.swiftyOnboardBackgroundColorFor(self, atIndex: currentIndex + 1) - + if let color1 = color1, - let color2 = color2 { + let color2 = color2 + { return colorFrom(start: color1, end: color2, percent: percentage) } } return nil } - + fileprivate func colorFrom(start color1: UIColor, end color2: UIColor, percent: CGFloat) -> UIColor { - func cofd(_ color1: CGFloat,_ color2: CGFloat,_ percent: CGFloat) -> CGFloat { + func cofd(_ color1: CGFloat, _ color2: CGFloat, _ percent: CGFloat) -> CGFloat { let c1 = CGFloat(color1) let c2 = CGFloat(color2) return (c1 + ((c2 - c1) * percent)) } - return UIColor(red: cofd(color1.cgColor.components![0], - color2.cgColor.components![0], - percent), - green: cofd(color1.cgColor.components![1], - color2.cgColor.components![1], - percent), - blue: cofd(color1.cgColor.components![2], - color2.cgColor.components![2], - percent), - alpha: 1) + return UIColor( + red: cofd( + color1.cgColor.components![0], + color2.cgColor.components![0], + percent + ), + green: cofd( + color1.cgColor.components![1], + color2.cgColor.components![1], + percent + ), + blue: cofd( + color1.cgColor.components![2], + color2.cgColor.components![2], + percent + ), + alpha: 1 + ) } - + fileprivate func fadePageTransitions(containerView: UIScrollView, currentPage: Int) { - for (index,page) in pages.enumerated() { + for (index, page) in pages.enumerated() { page.alpha = 1 - abs(abs(containerView.contentOffset.x) - page.frame.width * CGFloat(index)) / page.frame.width } } - + @objc open func didTapPageControl(_ sender: Any) { let pager = sender as! UIPageControl let page = pager.currentPage self.goToPage(index: page, animated: true) } - + open var currentPage: Int { return Int(getCurrentPosition()) } - + open func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { let currentPage = Int(getCurrentPosition()) self.delegate?.swiftyOnboard(self, currentPage: currentPage) } - + open func scrollViewDidScroll(_ scrollView: UIScrollView) { let currentPosition = Double(getCurrentPosition()) self.overlay?.currentPage(index: Int(round(currentPosition))) if self.fadePages { fadePageTransitions(containerView: scrollView, currentPage: Int(getCurrentPosition())) } - + self.delegate?.swiftyOnboard(self, leftEdge: currentPosition) if let overlayView = self.overlay { self.dataSource?.swiftyOnboardOverlayForPosition(self, overlay: overlayView, for: currentPosition) } - + if let color = colorForPosition(CGFloat(currentPosition)) { self.backgroundColor = color } } - + open func goToPage(index: Int, animated: Bool) { if index < self.pageCount { let index = CGFloat(index) @@ -256,7 +265,7 @@ public enum SwiftyOnboardStyle { case light case dark case custom(color: UIColor) - + var color: UIColor { switch self { case .light: return UIColor.white diff --git a/Adamant/Modules/SwiftyOnboard/SwiftyOnboardOverlay.swift b/Adamant/Modules/SwiftyOnboard/SwiftyOnboardOverlay.swift index bafa60b68..9d3dc4b93 100644 --- a/Adamant/Modules/SwiftyOnboard/SwiftyOnboardOverlay.swift +++ b/Adamant/Modules/SwiftyOnboard/SwiftyOnboardOverlay.swift @@ -6,11 +6,11 @@ // Copyright © 2017 Juan Pablo Fernandez. All rights reserved. // -import UIKit import CommonKit +import UIKit open class SwiftyOnboardOverlay: UIView { - + open var pageControl: UIPageControl = { let pageControl = UIPageControl() pageControl.currentPage = 0 @@ -18,7 +18,7 @@ open class SwiftyOnboardOverlay: UIView { pageControl.currentPageIndicatorTintColor = UIColor.adamant.textColor return pageControl }() - + open var continueButton: UIButton = { let button = UIButton(type: .system) button.setTitle("Continue", for: .normal) @@ -26,7 +26,7 @@ open class SwiftyOnboardOverlay: UIView { button.setTitleColor(UIColor.adamant.active, for: .normal) return button }() - + open var skipButton: UIButton = { let button = UIButton(type: .system) button.setTitle("Skip", for: .normal) @@ -34,16 +34,16 @@ open class SwiftyOnboardOverlay: UIView { button.setTitleColor(UIColor.adamant.active, for: .normal) return button }() - + override public init(frame: CGRect) { super.init(frame: frame) setUp() } - + required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } - + override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool { for subview in subviews { if !subview.isHidden && subview.alpha > 0 && subview.isUserInteractionEnabled && subview.point(inside: convert(point, to: subview), with: event) { @@ -52,32 +52,32 @@ open class SwiftyOnboardOverlay: UIView { } return false } - + open func page(count: Int) { pageControl.numberOfPages = count } - + open func currentPage(index: Int) { pageControl.currentPage = index } - + func setUp() { self.addSubview(pageControl) - + let margin = self.layoutMarginsGuide pageControl.translatesAutoresizingMaskIntoConstraints = false pageControl.heightAnchor.constraint(equalToConstant: 15).isActive = true pageControl.bottomAnchor.constraint(equalTo: margin.bottomAnchor, constant: -10).isActive = true pageControl.leftAnchor.constraint(equalTo: margin.leftAnchor, constant: 10).isActive = true pageControl.rightAnchor.constraint(equalTo: margin.rightAnchor, constant: -10).isActive = true - + self.addSubview(continueButton) continueButton.translatesAutoresizingMaskIntoConstraints = false continueButton.heightAnchor.constraint(equalToConstant: 20).isActive = true continueButton.bottomAnchor.constraint(equalTo: pageControl.topAnchor, constant: -20).isActive = true continueButton.leftAnchor.constraint(equalTo: margin.leftAnchor, constant: 10).isActive = true continueButton.rightAnchor.constraint(equalTo: margin.rightAnchor, constant: -10).isActive = true - + self.addSubview(skipButton) skipButton.translatesAutoresizingMaskIntoConstraints = false skipButton.heightAnchor.constraint(equalToConstant: 20).isActive = true @@ -85,5 +85,5 @@ open class SwiftyOnboardOverlay: UIView { skipButton.leftAnchor.constraint(equalTo: margin.leftAnchor, constant: 10).isActive = true skipButton.rightAnchor.constraint(equalTo: margin.rightAnchor, constant: -20).isActive = true } - + } diff --git a/Adamant/Modules/SwiftyOnboard/SwiftyOnboardPage.swift b/Adamant/Modules/SwiftyOnboard/SwiftyOnboardPage.swift index 9bfbddb4a..fc498b762 100644 --- a/Adamant/Modules/SwiftyOnboard/SwiftyOnboardPage.swift +++ b/Adamant/Modules/SwiftyOnboard/SwiftyOnboardPage.swift @@ -9,28 +9,28 @@ import UIKit open class SwiftyOnboardPage: UIView { - + public var imageView: UIImageView = { let imageView = UIImageView() imageView.contentMode = .scaleAspectFit return imageView }() - + required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } - + override init(frame: CGRect) { super.init(frame: frame) setUp() } - + func set(style: SwiftyOnboardStyle) { } - + func setUp() { self.addSubview(imageView) - + let margin = self.layoutMarginsGuide imageView.translatesAutoresizingMaskIntoConstraints = false imageView.leftAnchor.constraint(equalTo: margin.leftAnchor, constant: 30).isActive = true diff --git a/Adamant/Modules/TestVibration/VibrationSelectionFactory.swift b/Adamant/Modules/TestVibration/SettingSelectionFactory.swift similarity index 73% rename from Adamant/Modules/TestVibration/VibrationSelectionFactory.swift rename to Adamant/Modules/TestVibration/SettingSelectionFactory.swift index fe90d2dfa..6678bc7d5 100644 --- a/Adamant/Modules/TestVibration/VibrationSelectionFactory.swift +++ b/Adamant/Modules/TestVibration/SettingSelectionFactory.swift @@ -1,28 +1,28 @@ // -// VibrationSelectionFactory.swift +// SettingSelectionFactory.swift // Adamant // // Created by Stanislav Jelezoglo on 07.09.2023. // Copyright © 2023 Adamant. All rights reserved. // -import Swinject import SwiftUI +import Swinject @MainActor -struct VibrationSelectionFactory { +struct SettingSelectionFactory { private let parent: Assembler private let assemblies = [VibrationSelectionAssembly()] - + init(parent: Assembler) { self.parent = parent } - + @MainActor - func makeViewController() -> UIViewController { + func makeViewController(onSettingsSelect: @escaping (SettingsView.SettingsType) -> Void) -> UIViewController { let assembler = Assembler(assemblies, parent: parent) let viewModel = { assembler.resolver.resolve(VibrationSelectionViewModel.self)! } - return UIHostingController(rootView: VibrationSelectionView(viewModel: viewModel)) + return UIHostingController(rootView: SettingsView(viewModel: viewModel, onSettingsSelect: onSettingsSelect)) } } diff --git a/Adamant/Modules/TestVibration/VibrationSelectionView.swift b/Adamant/Modules/TestVibration/SettingsView.swift similarity index 52% rename from Adamant/Modules/TestVibration/VibrationSelectionView.swift rename to Adamant/Modules/TestVibration/SettingsView.swift index 7452705f0..623b222d6 100644 --- a/Adamant/Modules/TestVibration/VibrationSelectionView.swift +++ b/Adamant/Modules/TestVibration/SettingsView.swift @@ -1,37 +1,54 @@ // -// VibrationSelectionView.swift +// SettingsView.swift // Adamant // // Created by Stanislav Jelezoglo on 07.09.2023. // Copyright © 2023 Adamant. All rights reserved. // -import SwiftUI import CommonKit +import SwiftUI + +struct SettingsView: View { + enum SettingsType: String, CaseIterable { + case adamantWallets = "Adamant-Wallets" + } -struct VibrationSelectionView: View { @StateObject var viewModel: VibrationSelectionViewModel - - init(viewModel: @escaping () -> VibrationSelectionViewModel) { + private let onSettingsSelect: (SettingsType) -> Void + + init(viewModel: @escaping () -> VibrationSelectionViewModel, onSettingsSelect: @escaping (SettingsType) -> Void) { _viewModel = .init(wrappedValue: viewModel()) + self.onSettingsSelect = onSettingsSelect } - + var body: some View { List { - ForEach(AdamantVibroType.allCases, id: \.self) { type in - Button { - viewModel.type = type - } label: { - Text(vibrationTypeDescription(type)) + Section("Vibrations") { + ForEach(AdamantVibroType.allCases, id: \.self) { type in + Button { + viewModel.type = type + } label: { + Text(vibrationTypeDescription(type)) + } + } + } + Section("Adamant-Wallets") { + ForEach(SettingsType.allCases, id: \.rawValue) { type in + Button { + onSettingsSelect(type) + } label: { + Text(type.rawValue) + } } } } .withoutListBackground() .background(Color(.adamant.secondBackgroundColor)) - .navigationTitle("Vibrations") + .navigationTitle("Preferrences") .navigationBarTitleDisplayMode(.inline) } - + private func vibrationTypeDescription(_ type: AdamantVibroType) -> String { switch type { case .light: diff --git a/Adamant/Modules/TestVibration/VibrationSelectionViewModel.swift b/Adamant/Modules/TestVibration/VibrationSelectionViewModel.swift index b3b45b293..89006bafd 100644 --- a/Adamant/Modules/TestVibration/VibrationSelectionViewModel.swift +++ b/Adamant/Modules/TestVibration/VibrationSelectionViewModel.swift @@ -6,25 +6,25 @@ // Copyright © 2023 Adamant. All rights reserved. // -import SwiftUI import Combine import CommonKit +import SwiftUI @MainActor final class VibrationSelectionViewModel: ObservableObject { private let vibroService: VibroService private var subscriptions = Set() - + @Published var type: AdamantVibroType? - + init(vibroService: VibroService) { self.vibroService = vibroService setup() } } -private extension VibrationSelectionViewModel { - func setup() { +extension VibrationSelectionViewModel { + fileprivate func setup() { $type .compactMap { $0 } .sink { [weak vibroService] in vibroService?.applyVibration($0) } diff --git a/Adamant/Modules/TransactionsStatusService/Services/TransactionsStatusServiceCompose.swift b/Adamant/Modules/TransactionsStatusService/Services/TransactionsStatusServiceCompose.swift index 6947c995d..da0bd4b3c 100644 --- a/Adamant/Modules/TransactionsStatusService/Services/TransactionsStatusServiceCompose.swift +++ b/Adamant/Modules/TransactionsStatusService/Services/TransactionsStatusServiceCompose.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Adamant. All rights reserved. // -import CoreData import CommonKit +import CoreData @TransactionsStatusActor final class TransactionsStatusServiceCompose: NSObject, TransactionsStatusServiceComposeProtocol { @@ -15,7 +15,7 @@ final class TransactionsStatusServiceCompose: NSObject, TransactionsStatusServic private let walletServiceCompose: WalletServiceCompose private lazy var controller = getRichTransactionsController() private var observers = [TransferIdentifier: TxStatusServiceProtocol]() - + nonisolated init( coreDataStack: CoreDataStack, walletServiceCompose: WalletServiceCompose @@ -23,16 +23,16 @@ final class TransactionsStatusServiceCompose: NSObject, TransactionsStatusServic self.coreDataStack = coreDataStack self.walletServiceCompose = walletServiceCompose } - + func forceUpdate(transaction: CoinTransaction) async { guard let transferIdentifier = transaction.transferIdentifier else { return } await observers[transferIdentifier]?.forceUpdate(transaction: transaction) } - + func startObserving() { controller.delegate = self try? controller.performFetch() - + controller.fetchedObjects?.forEach { add(transaction: $0) } @@ -52,13 +52,13 @@ extension TransactionsStatusServiceCompose: NSFetchedResultsControllerDelegate { } } -private extension TransactionsStatusServiceCompose { - struct TransferIdentifier: Hashable { +extension TransactionsStatusServiceCompose { + fileprivate struct TransferIdentifier: Hashable { let transferHash: String let blockchain: String } - - func processCoreDataChange(type: NSFetchedResultsChangeType, transaction: CoinTransaction) { + + fileprivate func processCoreDataChange(type: NSFetchedResultsChangeType, transaction: CoinTransaction) { switch type { case .insert, .update: add(transaction: transaction) @@ -70,66 +70,67 @@ private extension TransactionsStatusServiceCompose { break } } - - func add(transaction: CoinTransaction) { + + fileprivate func add(transaction: CoinTransaction) { guard let transferIdentifier = transaction.transferIdentifier else { return } - + guard let observer = observers[transferIdentifier] else { - return observers[transferIdentifier] = makeObserver(transaction: transaction) + observers[transferIdentifier] = makeObserver(transaction: transaction) + return } - + observer.add(transaction: transaction) } - - func remove(transaction: CoinTransaction) { + + fileprivate func remove(transaction: CoinTransaction) { guard let transferIdentifier = transaction.transferIdentifier, let observer = observers[transferIdentifier] else { return } - + observer.remove(transaction: transaction) } - - func makeObserver(transaction: CoinTransaction) -> TxStatusServiceProtocol? { + + fileprivate func makeObserver(transaction: CoinTransaction) -> TxStatusServiceProtocol? { guard let walletService = walletServiceCompose.getWallet(by: transaction.blockchainType) else { return nil } - + return TxStatusService( transaction: transaction, walletService: walletService, saveStatus: { [weak self] in self?.saveStatus(objectID: $0, status: $1) }, - dismissService: { [weak self] in self?.dismissService($0)} + dismissService: { [weak self] in self?.dismissService($0) } ) } - - func dismissService(_ service: AnyObject) { + + fileprivate func dismissService(_ service: AnyObject) { let key = observers.first { $0.value === service }?.key guard let key else { return } observers.removeValue(forKey: key) } - - func saveStatus( + + fileprivate func saveStatus( objectID: NSManagedObjectID, status: TransactionStatus ) { let privateContext = NSManagedObjectContext( concurrencyType: .privateQueueConcurrencyType ) - + privateContext.parent = coreDataStack.container.viewContext let transaction = privateContext.object(with: objectID) as? CoinTransaction - + guard let transaction = transaction else { return } transaction.transactionStatus = status try? privateContext.save() } - - func getRichTransactionsController() -> NSFetchedResultsController { + + fileprivate func getRichTransactionsController() -> NSFetchedResultsController { let request: NSFetchRequest = NSFetchRequest( entityName: CoinTransaction.entityCoinName ) - + request.sortDescriptors = [] return NSFetchedResultsController( fetchRequest: request, @@ -140,15 +141,16 @@ private extension TransactionsStatusServiceCompose { } } -private extension CoinTransaction { - var transferIdentifier: TransactionsStatusServiceCompose.TransferIdentifier? { - let hash = switch self { - case let transaction as RichMessageTransaction: - transaction.getRichValue(for: RichContentKeys.transfer.hash) - default: - txId - } - +extension CoinTransaction { + fileprivate var transferIdentifier: TransactionsStatusServiceCompose.TransferIdentifier? { + let hash = + switch self { + case let transaction as RichMessageTransaction: + transaction.getRichValue(for: RichContentKeys.transfer.hash) + default: + txId + } + guard let hash else { return nil } return .init(transferHash: hash, blockchain: blockchainType) } diff --git a/Adamant/Modules/TransactionsStatusService/Services/TxStatusService.swift b/Adamant/Modules/TransactionsStatusService/Services/TxStatusService.swift index 0d02972fa..61a40c41d 100644 --- a/Adamant/Modules/TransactionsStatusService/Services/TxStatusService.swift +++ b/Adamant/Modules/TransactionsStatusService/Services/TxStatusService.swift @@ -6,24 +6,24 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation +import Combine import CommonKit import CoreData -import Combine +import Foundation @TransactionsStatusActor final class TxStatusService: TxStatusServiceProtocol { private let walletService: WalletService private let saveStatus: @TransactionsStatusActor (NSManagedObjectID, TransactionStatus) -> Void private let dismissService: @TransactionsStatusActor (AnyObject) -> Void - + private var originalTransaction: CoinTransaction? private var richTransactions: [String: RichMessageTransaction] = .init() private var status: TransactionStatus = .notInitiated private var oldPendingAttempts: Int = .zero private var txSentDate: Date? private var subscription: AnyCancellable? - + init( transaction: CoinTransaction, walletService: WalletService, @@ -35,31 +35,31 @@ final class TxStatusService: TxStatusServiceProtocol { self.dismissService = dismissService configure(transaction: transaction) } - + func add(transaction: CoinTransaction) { if let transaction = transaction as? RichMessageTransaction { richTransactions[transaction.transactionId] = transaction } else { originalTransaction = transaction } - + saveStatuses() } - + func remove(transaction: CoinTransaction) { if let transaction = transaction as? RichMessageTransaction { richTransactions.removeValue(forKey: transaction.transactionId) } else if transaction.transactionId == originalTransaction?.transactionId { originalTransaction = transaction } - + if isEmpty { dismissService(self) } else { saveStatuses() } } - + func forceUpdate(transaction: CoinTransaction) async { status = .notInitiated saveStatuses() @@ -67,19 +67,19 @@ final class TxStatusService: TxStatusServiceProtocol { } } -private extension TxStatusService { - enum StatusState { +extension TxStatusService { + fileprivate enum StatusState { case new case old case registered case final } - - var isEmpty: Bool { + + fileprivate var isEmpty: Bool { richTransactions.isEmpty && originalTransaction == nil } - - var statusState: StatusState { + + fileprivate var statusState: StatusState { switch status { case .inconsistent, .failed, .success: return .final @@ -89,20 +89,20 @@ private extension TxStatusService { guard let sentDate = originalTransaction?.dateValue ?? validRichTransaction?.sentDate else { return .old } - + let sentInterval = Date.now.timeIntervalSince1970 - sentDate.timeIntervalSince1970 - + let oldTxInterval = TimeInterval( walletService.core.newPendingInterval * .init(walletService.core.newPendingAttempts) ) - + return sentInterval < oldTxInterval ? .new : .old } } - - var nextUpdateInterval: TimeInterval? { + + fileprivate var nextUpdateInterval: TimeInterval? { switch statusState { case .registered: walletService.core.registeredInterval @@ -114,77 +114,78 @@ private extension TxStatusService { nil } } - - var richStatus: TransactionStatus { + + fileprivate var richStatus: TransactionStatus { guard let transactionDate = txSentDate, let messageDate = validRichTransaction?.sentDate else { return status } - + let timeDifference = abs(transactionDate.timeIntervalSince(messageDate)) return timeDifference <= walletService.core.consistencyMaxTime ? status : .inconsistent(.time) } - - var validRichTransactionId: String? { + + fileprivate var validRichTransactionId: String? { richTransactions.min { ($0.value.dateValue ?? .now) < ($1.value.dateValue ?? .now) }?.key } - - var validRichTransaction: RichMessageTransaction? { + + fileprivate var validRichTransaction: RichMessageTransaction? { validRichTransactionId.flatMap { richTransactions[$0] } } - - var dubbedRichTransactions: [RichMessageTransaction] { + + fileprivate var dubbedRichTransactions: [RichMessageTransaction] { guard let validRichTransactionId = validRichTransactionId else { return .init() } return richTransactions.values.filter { $0.transactionId != validRichTransactionId } } - - func updateStatus() async { + + fileprivate func updateStatus() async { guard let transaction = originalTransaction ?? validRichTransaction else { return } let info = await walletService.core.statusInfoFor(transaction: transaction) txSentDate = info.sentDate - + switch info.status { case .pending: - status = oldPendingAttempts < walletService.core.oldPendingAttempts + status = + oldPendingAttempts < walletService.core.oldPendingAttempts ? info.status : .failed case .success, .failed, .inconsistent, .registered, .notInitiated: status = info.status } - + saveStatuses() } - - func configure(transaction: CoinTransaction) { + + fileprivate func configure(transaction: CoinTransaction) { add(transaction: transaction) - + subscription = Task { [weak self] in while await self?.observationIteration() == true { try Task.checkCancellation() } }.eraseToAnyCancellable() } - - func saveStatuses() { + + fileprivate func saveStatuses() { if let originalTransaction { saveStatus(originalTransaction.objectID, status) } - + if let validRichTransaction { saveStatus(validRichTransaction.objectID, richStatus) } - + for tx in dubbedRichTransactions { saveStatus(tx.objectID, .inconsistent(.duplicate)) } } - + /// Returns `false` if it's the last iteration. Otherwise it's `true`. - func observationIteration() async -> Bool { + fileprivate func observationIteration() async -> Bool { await updateStatus() - + switch statusState { case .new, .registered: break @@ -193,7 +194,7 @@ private extension TxStatusService { case .final: return false } - + guard let interval = nextUpdateInterval else { return false } try? await Task.sleep(interval: interval) return true diff --git a/Adamant/Modules/TransactionsStatusService/TransactionsStatusActor.swift b/Adamant/Modules/TransactionsStatusService/TransactionsStatusActor.swift index 21ff32374..87aea3154 100644 --- a/Adamant/Modules/TransactionsStatusService/TransactionsStatusActor.swift +++ b/Adamant/Modules/TransactionsStatusService/TransactionsStatusActor.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation @globalActor actor TransactionsStatusActor: GlobalActor { diff --git a/Adamant/Modules/Wallets/Adamant/AdmTransactionDetailsViewController.swift b/Adamant/Modules/Wallets/Adamant/AdmTransactionDetailsViewController.swift index 1fabfba5f..8cae0a937 100644 --- a/Adamant/Modules/Wallets/Adamant/AdmTransactionDetailsViewController.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmTransactionDetailsViewController.swift @@ -6,44 +6,44 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import Eureka -import CommonKit import Combine +import CommonKit +import Eureka +import UIKit final class AdmTransactionDetailsViewController: TransactionDetailsViewControllerBase { - + // MARK: - Dependencies - + let transfersProvider: TransfersProvider let screensFactory: ScreensFactory - + // MARK: - Properties private let autoupdateInterval: TimeInterval = 5.0 - + var showToChat: Bool = false - + private var timerSubscription: AnyCancellable? - + private lazy var refreshControl: UIRefreshControl = { let control = UIRefreshControl() control.tintColor = .adamant.primary control.addTarget(self, action: #selector(refresh), for: UIControl.Event.valueChanged) return control }() - + override var transaction: TransactionDetails? { get { super.transaction } set { assertionFailure("Use adamant transaction") } } - + var adamantTransaction: AdamantTransactionDetails? { get { super.transaction as? AdamantTransactionDetails } set { super.transaction = newValue } } - + // MARK: - Lifecycle - + init( accountService: AccountService, transfersProvider: TransfersProvider, @@ -55,31 +55,32 @@ final class AdmTransactionDetailsViewController: TransactionDetailsViewControlle ) { self.transfersProvider = transfersProvider self.screensFactory = screensFactory - + super.init( dialogService: dialogService, currencyInfo: currencyInfo, addressBookService: addressBookService, - accountService: accountService, + accountService: accountService, walletService: nil, languageService: languageService ) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func viewDidLoad() { currencySymbol = AdmWalletService.currencySymbol - + super.viewDidLoad() - + if showToChat, - adamantTransaction?.chatRoom != nil, - let section = form.sectionBy(tag: Sections.actions.tag) { + adamantTransaction?.chatRoom != nil, + let section = form.sectionBy(tag: Sections.actions.tag) + { let chatLabel = String.adamant.transactionList.toChat - + // MARK: Open chat let row = LabelRow { $0.tag = Rows.openChat.tag @@ -92,30 +93,30 @@ final class AdmTransactionDetailsViewController: TransactionDetailsViewControlle }.onCellSelection { [weak self] (_, _) in self?.goToChat() } - + section.append(row) } - + tableView.refreshControl = refreshControl - + refresh(silent: true) - + startUpdate() } - + // MARK: - Overrides - + override func explorerUrl(for transaction: TransactionDetails) -> URL? { let id = transaction.txId - + return URL(string: "\(AdmWalletService.explorerTx)\(id)") } - + override func getName(by adamantAddress: String?) -> String? { let name = super.getName(by: adamantAddress) return name ?? adamantTransaction?.partnerName } - + func goToChat() { guard let chatroom = adamantTransaction?.chatRoom else { dialogService.showError(withMessage: "AdmTransactionDetailsViewController: Failed to get chatroom for transaction.", supportEmail: true, error: nil) @@ -142,14 +143,14 @@ final class AdmTransactionDetailsViewController: TransactionDetailsViewControlle self.present(vc, animated: true) } } - + @MainActor @objc func refresh(silent: Bool = false) { refreshTask = Task { guard let id = transaction?.txId else { return } - + do { try await transfersProvider.refreshTransfer(id: id) adamantTransaction = await transfersProvider.getTransfer(id: id) @@ -162,11 +163,12 @@ final class AdmTransactionDetailsViewController: TransactionDetailsViewControlle } } } - + // MARK: - Autoupdate - + func startUpdate() { - timerSubscription = Timer + timerSubscription = + Timer .publish(every: autoupdateInterval, on: .main, in: .common) .autoconnect() .sink { [weak self] _ in diff --git a/Adamant/Modules/Wallets/Adamant/AdmTransactionsViewController.swift b/Adamant/Modules/Wallets/Adamant/AdmTransactionsViewController.swift index fffb4340c..5ad9a7e19 100644 --- a/Adamant/Modules/Wallets/Adamant/AdmTransactionsViewController.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmTransactionsViewController.swift @@ -6,23 +6,23 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -@preconcurrency import CoreData import CommonKit +@preconcurrency import CoreData +import UIKit final class AdmTransactionsViewController: TransactionsListViewControllerBase { // MARK: - Dependencies - + let accountService: AccountService let transfersProvider: TransfersProvider let chatsProvider: ChatsProvider let stack: CoreDataStack let addressBookService: AddressBookService - + // MARK: - Properties - + var controller: NSFetchedResultsController? - + /* In SplitViewController on iPhones, viewController can still present in memory, but not on screen. In this cases not visible viewController will still mark messages isUnread = false @@ -30,9 +30,9 @@ final class AdmTransactionsViewController: TransactionsListViewControllerBase { /// ViewController currently is ontop of the screen. private var isOnTop = false private let transactionsPerRequest = 100 - + // MARK: - Lifecycle - + init( accountService: AccountService, transfersProvider: TransfersProvider, @@ -49,7 +49,7 @@ final class AdmTransactionsViewController: TransactionsListViewControllerBase { self.chatsProvider = chatsProvider self.stack = stack self.addressBookService = addressBookService - + super.init( walletService: walletService, dialogService: dialogService, @@ -57,34 +57,34 @@ final class AdmTransactionsViewController: TransactionsListViewControllerBase { screensFactory: screensFactory ) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func viewDidLoad() { super.viewDidLoad() - + if accountService.account != nil { reloadData() } - + setupObserver() } - + override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) isOnTop = true markTransfersAsRead() } - + override func viewDidDisappear(_ animated: Bool) { super.viewWillDisappear(animated) isOnTop = false } - + // MARK: - Overrides - + @MainActor override func reloadData() { guard reachabilityMonitor.connection else { @@ -95,26 +95,27 @@ final class AdmTransactionsViewController: TransactionsListViewControllerBase { ) return } - + Task { controller = await transfersProvider.transfersController() - + do { try controller?.performFetch() - let transactions: [SimpleTransactionDetails] = controller?.fetchedObjects?.compactMap { - getTransactionDetails(by: $0) - } ?? [] - + let transactions: [SimpleTransactionDetails] = + controller?.fetchedObjects?.compactMap { + getTransactionDetails(by: $0) + } ?? [] + update(transactions) } catch { dialogService.showError(withMessage: "Failed to get transactions. Please, report a bug", supportEmail: true, error: error) controller = nil } - + isBusy = false } } - + @MainActor override func handleRefresh() { guard reachabilityMonitor.connection else { @@ -128,38 +129,38 @@ final class AdmTransactionsViewController: TransactionsListViewControllerBase { Task { self.isBusy = true self.emptyLabel.isHidden = true - + let result = await self.transfersProvider.update() - + guard let result = result else { refreshControl.endRefreshing() return } - + switch result { case .success: refreshControl.endRefreshing() tableView.reloadData() emptyLabel.isHidden = transactions.count > .zero - + case .failure(let error): refreshControl.endRefreshing() - + dialogService.showRichError(error: error) } - + self.isBusy = false }.stored(in: taskManager) } - + override func loadData(silent: Bool) { isBusy = true emptyLabel.isHidden = true - + guard let address = accountService.account?.address else { return } - + Task { @MainActor in do { let count = try await transfersProvider.getTransactions( @@ -169,79 +170,79 @@ final class AdmTransactionsViewController: TransactionsListViewControllerBase { limit: transactionsPerRequest, orderByTime: true ) - + if count > 0 { await transfersProvider.updateOffsetTransactions( transfersProvider.offsetTransactions + transactionsPerRequest ) } - + isNeedToLoadMoore = count >= transactionsPerRequest emptyLabel.isHidden = transactions.count > .zero } catch { isNeedToLoadMoore = false emptyLabel.isHidden = transactions.count > .zero - + if !silent { dialogService.showRichError(error: error) emptyLabel.isHidden = true } } - + isBusy = false emptyLabel.isHidden = !transactions.isEmpty refreshControl.endRefreshing() }.stored(in: taskManager) } - + private func markTransfersAsRead() { let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) privateContext.parent = self.stack.container.viewContext - + let request = NSFetchRequest(entityName: TransferTransaction.entityName) request.predicate = NSPredicate(format: "isUnread == true") request.sortDescriptors = [NSSortDescriptor(key: "transactionId", ascending: false)] - + if let result = try? privateContext.fetch(request) { result.forEach { $0.isUnread = false } - + if privateContext.hasChanges { try? privateContext.save() } } } - + func getTransactionDetails(by transaction: TransferTransaction) -> SimpleTransactionDetails { - let partnerId = ( - transaction.isOutgoing - ? transaction.recipientId - : transaction.senderId - ) ?? "" - + let partnerId = + (transaction.isOutgoing + ? transaction.recipientId + : transaction.senderId) ?? "" + var simple = SimpleTransactionDetails(transaction) simple.partnerName = getPartnerName(for: partnerId, tx: transaction) return simple } - + func getPartnerName( for partnerId: String, tx: TransferTransaction ) -> String? { var partnerName = addressBookService.getName(for: partnerId) - + if let address = accountService.account?.address, - partnerId == address { + partnerId == address + { partnerName = String.adamant.transactionDetails.yourAddress } - + return partnerName ?? tx.partnerName } - + // MARK: - UITableView func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let transaction = transactions[safe: indexPath.row] else { return } - + let controller = screensFactory.makeAdmTransactionDetails() controller.adamantTransaction = transaction controller.comment = transaction.comment @@ -249,40 +250,40 @@ final class AdmTransactionsViewController: TransactionsListViewControllerBase { if let address = accountService.account?.address { let partnerName = transaction.partnerName - + if address == transaction.senderAddress { controller.senderName = String.adamant.transactionDetails.yourAddress } else { controller.senderName = partnerName } - + if address == transaction.recipientAddress { controller.recipientName = String.adamant.transactionDetails.yourAddress } else { controller.recipientName = partnerName } } - + navigationController?.pushViewController(controller, animated: true) } - + func tableView( _ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath - ) -> UISwipeActionsConfiguration? { + ) -> UISwipeActionsConfiguration? { guard let transaction = transactions[safe: indexPath.row], - transaction.showToChat == true, - let chatroom = transaction.chatRoom + transaction.showToChat == true, + let chatroom = transaction.chatRoom else { return nil } - + let toChat = UIContextualAction(style: .normal, title: "") { [weak self] (_, _, _) in guard let self = self, let account = accountService.account else { return } - + let vc = screensFactory.makeChat() vc.hidesBottomBarWhenPushed = true vc.viewModel.setup( @@ -290,7 +291,7 @@ final class AdmTransactionsViewController: TransactionsListViewControllerBase { chatroom: chatroom, messageIdToShow: nil ) - + if let nav = self.navigationController { nav.pushViewController(vc, animated: true) } else { @@ -298,47 +299,48 @@ final class AdmTransactionsViewController: TransactionsListViewControllerBase { present(vc, animated: true) } } - + toChat.image = .asset(named: "chats_tab") toChat.backgroundColor = UIColor.adamant.primary return UISwipeActionsConfiguration(actions: [toChat]) } - + private func toShowChat(for transaction: TransferTransaction) -> Bool { guard let partner = transaction.partner as? CoreDataAccount, let chatroom = partner.chatroom, !chatroom.isReadonly else { return false } - + return true } } -private extension AdmTransactionsViewController { - func setupObserver() { +extension AdmTransactionsViewController { + fileprivate func setupObserver() { NotificationCenter.default.publisher( for: .NSManagedObjectContextObjectsDidChange, object: stack.container.viewContext ) .sink { [weak self] notification in guard let self = self else { return } - + let changes = notification.managedObjectContextChanges(of: TransferTransaction.self) if let inserted = changes.inserted, !inserted.isEmpty { let maped: [SimpleTransactionDetails] = inserted.map { self.getTransactionDetails(by: $0) } - + var transactions = self.transactions transactions.append(contentsOf: maped) self.update(transactions) } - + if let updated = changes.updated, !updated.isEmpty { updated.forEach { transaction in - guard let index = self.transactions.firstIndex(where: { - $0.txId == transaction.txId - }) + guard + let index = self.transactions.firstIndex(where: { + $0.txId == transaction.txId + }) else { return } var transactions: [SimpleTransactionDetails] = self.transactions transactions[index] = self.getTransactionDetails(by: transaction) diff --git a/Adamant/Modules/Wallets/Adamant/AdmTransferViewController.swift b/Adamant/Modules/Wallets/Adamant/AdmTransferViewController.swift index 4748e14d8..2d8d4ca91 100644 --- a/Adamant/Modules/Wallets/Adamant/AdmTransferViewController.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmTransferViewController.swift @@ -6,62 +6,78 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit +import CommonKit import Eureka import SafariServices -import CommonKit +import UIKit // MARK: - Localization extension String.adamant { enum transferAdm { static func accountNotFoundAlertTitle(for address: String) -> String { - return String.localizedStringWithFormat(.localized("TransferScene.unsafeTransferAlert.title", comment: "Transfer: Alert title: Account not found or not initiated. Alert user that he still can send money, but need to double ckeck address"), address) + return String.localizedStringWithFormat( + .localized( + "TransferScene.unsafeTransferAlert.title", + comment: + "Transfer: Alert title: Account not found or not initiated. Alert user that he still can send money, but need to double ckeck address" + ), + address + ) } - - static var accountNotFoundAlertBody: String { String.localized("TransferScene.unsafeTransferAlert.body", comment: "Transfer: Alert body: Account not found or not initiated. Alert user that he still can send money, but need to double ckeck address") + + static var accountNotFoundAlertBody: String { + String.localized( + "TransferScene.unsafeTransferAlert.body", + comment: "Transfer: Alert body: Account not found or not initiated. Alert user that he still can send money, but need to double ckeck address" + ) } - - static var accountNotFoundChatAlertBody: String { String.localized("TransferScene.unsafeChatAlert.body", comment: "Transfer: Alert body: Account is not initiated. It's not possible to start a chat, as the Blockchain doesn't store the account's public key to encrypt messages.") + + static var accountNotFoundChatAlertBody: String { + String.localized( + "TransferScene.unsafeChatAlert.body", + comment: + "Transfer: Alert body: Account is not initiated. It's not possible to start a chat, as the Blockchain doesn't store the account's public key to encrypt messages." + ) } } } final class AdmTransferViewController: TransferViewControllerBase { // MARK: Properties - + private var skipValueChange: Bool = false - + static let invalidCharactersSet = CharacterSet.decimalDigits.inverted - + // MARK: Sending - + @MainActor override func sendFunds() { guard let service = walletCore as? AdmWalletService, - let recipient = recipientAddress, - let amount = amount + let recipient = recipientAddress, + let amount = amount else { return } - + let comments: String if let row: TextAreaRow = form.rowBy(tag: BaseRows.comments.tag), let text = row.value { comments = text } else { comments = "" } - + dialogService.showProgress(withMessage: String.adamant.transfer.transferProcessingMessage, userInteractionEnable: false) - + // Check recipient Task { do { let account = try await accountsProvider.getAccount(byAddress: recipient) - + guard !account.isDummy else { throw AccountsProviderError.dummy(account) } - + sendFundsInternal( service: service, recipient: recipient, @@ -72,7 +88,7 @@ final class AdmTransferViewController: TransferViewControllerBase { switch error { case .notFound, .notInitiated, .dummy: self.dialogService.dismissProgress() - + dialogService.presentDummyAlert( for: recipient, from: view, @@ -82,7 +98,7 @@ final class AdmTransferViewController: TransferViewControllerBase { withMessage: String.adamant.transfer.transferProcessingMessage, userInteractionEnable: false ) - + self?.sendFundsInternal( service: service, recipient: recipient, @@ -98,7 +114,7 @@ final class AdmTransferViewController: TransferViewControllerBase { } } } - + @MainActor private func sendFundsInternal( service: AdmWalletService, @@ -114,12 +130,12 @@ final class AdmTransferViewController: TransferViewControllerBase { comments: comments, replyToMessageId: replyToMessageId ) - + service.update() dialogService.dismissProgress() - + dialogService.showSuccess(withMessage: String.adamant.transfer.transferSuccess) - + openDetailVC( result: result, vc: self, @@ -132,7 +148,7 @@ final class AdmTransferViewController: TransferViewControllerBase { } } } - + private func openDetailVC( result: AdamantTransactionDetails, vc: AdmTransferViewController, @@ -141,20 +157,20 @@ final class AdmTransferViewController: TransferViewControllerBase { ) { let detailsVC = screensFactory.makeAdmTransactionDetails() detailsVC.adamantTransaction = result - + if comments.count > 0 { detailsVC.comment = comments } - + // MARK: Sender, you detailsVC.senderName = String.adamant.transactionDetails.yourAddress - + // MARK: Get recipient if let recipientName = recipientName { detailsVC.recipientName = recipientName vc.delegate?.transferViewController(vc, didFinishWithTransfer: result, detailsViewController: detailsVC) } else { - Task { + Task { do { let account = try await accountsProvider.getAccount(byAddress: recipient) detailsVC.recipientName = account.name @@ -165,9 +181,9 @@ final class AdmTransferViewController: TransferViewControllerBase { } } } - + // MARK: Overrides - + override var recipientAddress: String? { set { let _recipient: String? @@ -176,7 +192,7 @@ final class AdmTransferViewController: TransferViewControllerBase { } else { _recipient = newValue } - + if let row: TextRow = form.rowBy(tag: BaseRows.address.tag) { row.value = _recipient row.updateCell() @@ -185,32 +201,32 @@ final class AdmTransferViewController: TransferViewControllerBase { get { let recipient: String? = form.rowBy(tag: BaseRows.address.tag)?.value guard let recipient = recipient, - let first = recipient.first, - first != "U" + let first = recipient.first, + first != "U" else { return recipient } - + return "U\(recipient)" } } - + override func recipientRow() -> BaseRow { let row = TextRow { $0.tag = BaseRows.address.tag $0.cell.textField.placeholder = String.adamant.newChat.addressPlaceholder $0.cell.textField.setPopupKeyboardType(.numberPad) $0.cell.textField.setLineBreakMode() - + if let recipient = recipientAddress { let trimmed = recipient.components(separatedBy: AdmTransferViewController.invalidCharactersSet).joined() $0.value = trimmed } - + let prefix = UILabel() prefix.text = "U" prefix.sizeToFit() - + let view = UIView() view.addSubview(prefix) view.frame = prefix.frame @@ -226,7 +242,7 @@ final class AdmTransferViewController: TransferViewControllerBase { cell.textField.text = text.components(separatedBy: AdmTransferViewController.invalidCharactersSet).joined() guard self?.recipientIsReadonly == false else { return } - + cell.textField.leftView?.subviews.forEach { view in guard let label = view as? UILabel else { return } label.textColor = UIColor.adamant.primary @@ -237,7 +253,7 @@ final class AdmTransferViewController: TransferViewControllerBase { self?.skipValueChange = false return } - + if let text = row.value { var trimmed = "" if let admAddress = text.getAdamantAddress() { @@ -247,10 +263,10 @@ final class AdmTransferViewController: TransferViewControllerBase { } else { trimmed = text.components(separatedBy: AdmTransferViewController.invalidCharactersSet).joined() } - + if text != trimmed { self?.skipValueChange = true - + DispatchQueue.main.async { row.value = trimmed row.updateCell() @@ -261,10 +277,10 @@ final class AdmTransferViewController: TransferViewControllerBase { }.onCellSelection { [weak self] (cell, _) in self?.shareValue(self?.recipientAddress, from: cell) } - + return row } - + override func validateRecipient(_ address: String) -> AddressValidationResult { let fixedAddress: String if let first = address.first, first != "U" { @@ -272,20 +288,20 @@ final class AdmTransferViewController: TransferViewControllerBase { } else { fixedAddress = address } - + switch AdamantUtilities.validateAdamantAddress(address: fixedAddress) { case .valid: return .valid - + case .system, .invalid: return .invalid(description: nil) } } - + override func handleRawAddress(_ address: String) -> Bool { if let admAddress = address.getAdamantAddress() { recipientAddress = admAddress.address - + if let row: SafeDecimalRow = form.rowBy(tag: BaseRows.amount.tag) { row.value = admAddress.amount row.updateCell() @@ -301,12 +317,12 @@ final class AdmTransferViewController: TransferViewControllerBase { } return true } - + return false } - + override func defaultSceneTitle() -> String? { return String.adamant.wallets.sendAdm } - + } diff --git a/Adamant/Modules/Wallets/Adamant/AdmWallet.swift b/Adamant/Modules/Wallets/Adamant/AdmWallet.swift index 20bcc6b89..60a6aa11e 100644 --- a/Adamant/Modules/Wallets/Adamant/AdmWallet.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmWallet.swift @@ -6,19 +6,19 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation final class AdmWallet: WalletAccount, @unchecked Sendable { let unicId: String let address: String - + @Atomic var balance: Decimal = 0 @Atomic var notifications: Int = 0 @Atomic var minBalance: Decimal = 0 @Atomic var minAmount: Decimal = 0 @Atomic var isBalanceInitialized: Bool = false - + init(unicId: String, address: String) { self.unicId = unicId self.address = address diff --git a/Adamant/Modules/Wallets/Adamant/AdmWalletFactory.swift b/Adamant/Modules/Wallets/Adamant/AdmWalletFactory.swift index 71c147234..17248b4fb 100644 --- a/Adamant/Modules/Wallets/Adamant/AdmWalletFactory.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmWalletFactory.swift @@ -6,16 +6,16 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Swinject -import UIKit import CommonKit +import SwiftUI +import Swinject struct AdmWalletFactory: WalletFactory { typealias Service = WalletService - + let typeSymbol: String = AdmWalletService.richMessageType let assembler: Assembler - + func makeWalletVC(service: Service, screensFactory: ScreensFactory) -> WalletViewController { AdmWalletViewController( dialogService: assembler.resolve(DialogService.self)!, @@ -26,7 +26,7 @@ struct AdmWalletFactory: WalletFactory { service: service ) } - + func makeTransferListVC(service: Service, screensFactory: ScreensFactory) -> UIViewController { AdmTransactionsViewController( accountService: assembler.resolve(AccountService.self)!, @@ -40,7 +40,7 @@ struct AdmWalletFactory: WalletFactory { reachabilityMonitor: assembler.resolve(ReachabilityMonitor.self)! ) } - + func makeTransferVC(service: Service, screensFactory: ScreensFactory) -> TransferViewControllerBase { AdmTransferViewController( chatsProvider: assembler.resolve(ChatsProvider.self)!, @@ -56,17 +56,17 @@ struct AdmWalletFactory: WalletFactory { apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)! ) } - + func makeDetailsVC(service: Service, transaction: RichMessageTransaction) -> UIViewController? { nil } - + func makeDetailsVC(service: Service) -> TransactionDetailsViewControllerBase { fatalError("ScreensFactory in necessary for AdmTransactionDetailsViewController") } - + func makeDetailsVC(screensFactory: ScreensFactory) -> AdmTransactionDetailsViewController { makeTransactionDetailsVC(screensFactory: screensFactory) } - + func makeDetailsVC(transaction: TransferTransaction, screensFactory: ScreensFactory) -> UIViewController { let controller = makeTransactionDetailsVC(screensFactory: screensFactory) controller.adamantTransaction = transaction @@ -75,17 +75,40 @@ struct AdmWalletFactory: WalletFactory { controller.recipientId = transaction.recipientId return controller } - - func makeBuyAndSellVC() -> UIViewController { + + func makeBuyAndSellVC(screenFactory: ScreensFactory) -> UIViewController { let c = BuyAndSellViewController() c.accountService = assembler.resolve(AccountService.self) c.dialogService = assembler.resolve(DialogService.self) + c.chatsProvider = assembler.resolve(ChatsProvider.self) + c.screenFactory = screenFactory return c } + func makeBuyAndSellView(screenFactory: ScreensFactory, action: @escaping () -> Void) -> AnyView { + AnyView( + NavigationView { + UIViewControllerWrapper(screenFactory.makeBuyAndSell()) + .navigationBarTitle(AdmWalletViewController.Rows.buyTokens.localized, displayMode: .inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button( + action: { + action() + }, + label: { + Image(systemName: "chevron.left") + .font(.system(size: 18, weight: .medium)) + } + ) + } + } + } + ) + } } -private extension AdmWalletFactory { - func makeTransactionDetailsVC(screensFactory: ScreensFactory) -> AdmTransactionDetailsViewController { +extension AdmWalletFactory { + fileprivate func makeTransactionDetailsVC(screensFactory: ScreensFactory) -> AdmTransactionDetailsViewController { AdmTransactionDetailsViewController( accountService: assembler.resolve(AccountService.self)!, transfersProvider: assembler.resolve(TransfersProvider.self)!, diff --git a/Adamant/Modules/Wallets/Adamant/AdmWalletService+DynamicConstants.swift b/Adamant/Modules/Wallets/Adamant/AdmWalletService+DynamicConstants.swift deleted file mode 100644 index c4db39344..000000000 --- a/Adamant/Modules/Wallets/Adamant/AdmWalletService+DynamicConstants.swift +++ /dev/null @@ -1,131 +0,0 @@ -import Foundation -import BigInt -import CommonKit - -extension AdmWalletService { - // MARK: - Constants - static let fixedFee: Decimal = 0.5 - static let currencySymbol = "ADM" - static let currencyExponent: Int = -8 - static let qqPrefix: String = "adm" - - static let healthCheckParameters = CoinHealthCheckParameters( - normalUpdateInterval: 300, - crucialUpdateInterval: 30, - onScreenUpdateInterval: 10, - threshold: 10, - normalServiceUpdateInterval: 300, - crucialServiceUpdateInterval: 30, - onScreenServiceUpdateInterval: 10 - ) - - static var adamantTimestampCorrection: TimeInterval { - 0.5 - } - - static var newPendingInterval: Int { - 4000 - } - - static var oldPendingInterval: Int { - 4000 - } - - static var registeredInterval: Int { - 4000 - } - - var tokenName: String { - "ADAMANT Messenger" - } - - var consistencyMaxTime: Double { - 0 - } - - var minBalance: Decimal { - 0 - } - - var minAmount: Decimal { - 0 - } - - var defaultVisibility: Bool { - true - } - - var defaultOrdinalLevel: Int? { - 0 - } - - static var minNodeVersion: String? { - "0.8.0" - } - - var transferDecimals: Int { - 8 - } - - static let explorerTx = "https://explorer.adamant.im/tx/" - static let explorerAddress = "https://explorer.adamant.im/address/" - static var nodes: [Node] { - [ - Node.makeDefaultNode(url: URL(string: "https://clown.adamant.im")!), - Node.makeDefaultNode(url: URL(string: "https://lake.adamant.im")!), - Node.makeDefaultNode( - url: URL(string: "https://endless.adamant.im")!, - altUrl: URL(string: "http://149.102.157.15:36666") - ), - Node.makeDefaultNode(url: URL(string: "https://bid.adamant.im")!), - Node.makeDefaultNode(url: URL(string: "https://unusual.adamant.im")!), - Node.makeDefaultNode( - url: URL(string: "https://debate.adamant.im")!, - altUrl: URL(string: "http://95.216.161.113:36666") - ), - Node.makeDefaultNode(url: URL(string: "http://78.47.205.206:36666")!), - Node.makeDefaultNode(url: URL(string: "http://5.161.53.74:36666")!), - Node.makeDefaultNode(url: URL(string: "http://184.94.215.92:45555")!), - Node.makeDefaultNode( - url: URL(string: "https://node1.adamant.business")!, - altUrl: URL(string: "http://194.233.75.29:45555") - ), - Node.makeDefaultNode(url: URL(string: "https://node2.blockchain2fa.io")!), - Node.makeDefaultNode( - url: URL(string: "https://phecda.adm.im")!, - altUrl: URL(string: "http://46.250.234.248:36666") - ), - Node.makeDefaultNode(url: URL(string: "https://tegmine.adm.im")!), - Node.makeDefaultNode( - url: URL(string: "https://tauri.adm.im")!, - altUrl: URL(string: "http://154.26.159.245:36666") - ), - Node.makeDefaultNode(url: URL(string: "https://dschubba.adm.im")!), - Node.makeDefaultNode( - url: URL(string: "https://tauri.bbry.org")!, - altUrl: URL(string: "http://54.197.36.175:36666") - ), - Node.makeDefaultNode( - url: URL(string: "https://endless.bbry.org")!, - altUrl: URL(string: "http://54.197.36.175:46666") - ), - Node.makeDefaultNode( - url: URL(string: "https://debate.bbry.org")!, - altUrl: URL(string: "http://54.197.36.175:56666") - ) - ] - } - - static var serviceNodes: [Node] { - [ - Node.makeDefaultNode( - url: URL(string: "https://info.adamant.im")!, - altUrl: URL(string: "http://5.161.98.136:33088")! - ), - Node.makeDefaultNode( - url: URL(string: "https://info2.adm.im")!, - altUrl: URL(string: "http://207.180.210.95:33088")! - ) - ] - } -} diff --git a/Adamant/Modules/Wallets/Adamant/AdmWalletService+RichMessageProvider.swift b/Adamant/Modules/Wallets/Adamant/AdmWalletService+RichMessageProvider.swift index 30f5baf17..55f466983 100644 --- a/Adamant/Modules/Wallets/Adamant/AdmWalletService+RichMessageProvider.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmWalletService+RichMessageProvider.swift @@ -6,64 +6,64 @@ // Copyright © 2018 Adamant. All rights reserved. // +import CommonKit import Foundation import MessageKit import UIKit -import CommonKit extension AdmWalletService { var newPendingInterval: TimeInterval { .zero } - + var oldPendingInterval: TimeInterval { .zero } - + var registeredInterval: TimeInterval { .zero } - + var newPendingAttempts: Int { .zero } - + var oldPendingAttempts: Int { .zero } - + var dynamicRichMessageType: String { return type(of: self).richMessageType } - + // MARK: Events - + func richMessageTapped(for transaction: RichMessageTransaction, in chat: ChatViewController) { return } - + // MARK: Short description private static let formatter: NumberFormatter = { return AdamantBalanceFormat.currencyFormatter(for: .full, currencySymbol: currencySymbol) }() - + func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { guard let balance = transaction.amount as Decimal? else { return NSAttributedString(string: "") } - + return NSAttributedString(string: shortDescription(isOutgoing: transaction.isOutgoing, balance: balance)) } - + /// For ADM transfers func shortDescription(for transaction: TransferTransaction) -> String { guard let balance = transaction.amount as Decimal? else { return "" } - + return shortDescription(isOutgoing: transaction.isOutgoing, balance: balance) } - + private func shortDescription(isOutgoing: Bool, balance: Decimal) -> String { if isOutgoing { return "⬅️ \(AdmWalletService.formatter.string(from: balance)!)" diff --git a/Adamant/Modules/Wallets/Adamant/AdmWalletService+Send.swift b/Adamant/Modules/Wallets/Adamant/AdmWalletService+Send.swift index e8db50d6e..9156726d1 100644 --- a/Adamant/Modules/Wallets/Adamant/AdmWalletService+Send.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmWalletService+Send.swift @@ -6,13 +6,13 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit import CommonKit +import UIKit extension AdmWalletService: WalletServiceSimpleSend { /// Transaction ID typealias T = Int - + func sendMoney( recipient: String, amount: Decimal, @@ -26,7 +26,7 @@ extension AdmWalletService: WalletServiceSimpleSend { comment: comments, replyToMessageId: replyToMessageId ) - + return transaction } catch let error as TransfersProviderError { throw error.asWalletServiceError() diff --git a/Adamant/Modules/Wallets/Adamant/AdmWalletService.swift b/Adamant/Modules/Wallets/Adamant/AdmWalletService.swift index 3a0b79bcb..a5f53ca39 100644 --- a/Adamant/Modules/Wallets/Adamant/AdmWalletService.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmWalletService.swift @@ -6,79 +6,90 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation -import UIKit -import Swinject -@preconcurrency import CoreData -import MessageKit import Combine import CommonKit +@preconcurrency import CoreData +import Foundation +import MessageKit +import Swinject +import UIKit -final class AdmWalletService: NSObject, WalletCoreProtocol, @unchecked Sendable { +final class AdmWalletService: NSObject, WalletCoreProtocol, WalletStaticCoreProtocol, @unchecked Sendable { + static let currencySymbol = "ADM" // MARK: - Constants let addressRegex = try! NSRegularExpression(pattern: "^U([0-9]{6,20})$") - + static let currencyLogo = UIImage.asset(named: "adamant_wallet") ?? .init() + static var correctedDate: Date { + Date() - 0.5 + } var tokenSymbol: String { return type(of: self).currencySymbol } - + var tokenLogo: UIImage { return type(of: self).currencyLogo } - + var transactionFee: Decimal { return AdmWalletService.fixedFee } - + static var tokenNetworkSymbol: String { return Self.currencySymbol } - + var tokenContract: String { return "" } - - var tokenUnicID: String { + + var tokenUniqueID: String { Self.tokenNetworkSymbol + tokenSymbol } - + var richMessageType: String { return Self.richMessageType - } + } var qqPrefix: String { return Self.qqPrefix } - + var nodeGroups: [NodeGroup] { [.adm] } - + var explorerAddress: String { Self.explorerAddress } - - // MARK: - Dependencies - weak var accountService: AccountService? - var apiService: AdamantApiServiceProtocol! - var transfersProvider: TransfersProvider! + + // MARK: - Dependencies + weak var accountService: AccountService? + var apiService: AdamantApiServiceProtocol! + var transfersProvider: TransfersProvider! var coreDataStack: CoreDataStack! var vibroService: VibroService! - + // MARK: - Notifications let walletUpdatedNotification = Notification.Name("adamant.admWallet.updated") let serviceEnabledChanged = Notification.Name("adamant.admWallet.enabledChanged") let transactionFeeUpdated = Notification.Name("adamant.admWallet.feeUpdated") let serviceStateChanged = Notification.Name("adamant.admWallet.stateChanged") - + + @MainActor + private let walletUpdateSender = ObservableSender() + @MainActor + var walletUpdatePublisher: AnyObservable { + walletUpdateSender.eraseToAnyPublisher() + } + // MARK: RichMessageProvider properties - static let richMessageType = "adm_transaction" // not used - + static let richMessageType = "adm_transaction" // not used + // MARK: - Properties let enabled: Bool = true - + private var transfersController: NSFetchedResultsController? @Atomic private(set) var isWarningGasPrice = false @Atomic private var subscriptions = Set() @@ -89,41 +100,41 @@ final class AdmWalletService: NSObject, WalletCoreProtocol, @unchecked Sendable var transactionsPublisher: AnyObservable<[TransactionDetails]> { $transactions.eraseToAnyPublisher() } - + var hasMoreOldTransactionsPublisher: AnyObservable { $hasMoreOldTransactions.eraseToAnyPublisher() } - + @MainActor var hasEnabledNode: Bool { apiService.hasEnabledNode } - + @MainActor var hasEnabledNodePublisher: AnyObservable { apiService.hasEnabledNodePublisher } - + private(set) lazy var coinStorage: CoinStorageService = AdamantCoinStorageService( - coinId: tokenUnicID, + coinId: tokenUniqueID, coreDataStack: coreDataStack, blockchainType: richMessageType ) - + // MARK: - State @Atomic private(set) var state: WalletServiceState = .upToDate @Atomic private(set) var admWallet: AdmWallet? - + var wallet: WalletAccount? { admWallet } - + // MARK: - Logic override init() { super.init() - + // Notifications addObservers() } - + func addObservers() { NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedIn, object: nil) @@ -131,14 +142,14 @@ final class AdmWalletService: NSObject, WalletCoreProtocol, @unchecked Sendable self?.update() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantAccountService.accountDataUpdated, object: nil) .sink { @MainActor [weak self] _ in self?.update() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedOut, object: nil) .sink { @MainActor [weak self] _ in @@ -146,86 +157,94 @@ final class AdmWalletService: NSObject, WalletCoreProtocol, @unchecked Sendable } .store(in: &subscriptions) } - + func update() { guard let accountService = accountService, let account = accountService.account else { admWallet = nil return } - + let isRaised: Bool - + if let wallet = admWallet { isRaised = (wallet.balance < account.balance) && wallet.isBalanceInitialized wallet.balance = account.balance } else { - let wallet = AdmWallet(unicId: tokenUnicID, address: account.address) + let wallet = AdmWallet(unicId: tokenUniqueID, address: account.address) wallet.balance = account.balance - + admWallet = wallet isRaised = false } - + admWallet?.isBalanceInitialized = !accountService.isBalanceExpired - - if isRaised { - Task { @MainActor in vibroService.applyVibration(.success) } - } - + if let wallet = wallet { postUpdateNotification(with: wallet) } + + if isRaised { + Task { @MainActor in vibroService.applyVibration(.success) } + } } - + // MARK: - Tools func getBalance(address: String) async throws -> Decimal { let account = try await apiService.getAccount(byAddress: address).get() return account.balance } - + func validate(address: String) -> AddressValidationResult { addressRegex.perfectMatch(with: address) ? .valid : .invalid(description: nil) } - + private func postUpdateNotification(with wallet: WalletAccount) { NotificationCenter.default.post( name: walletUpdatedNotification, object: self, userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet] ) + Task { @MainActor in + walletUpdateSender.send() + } } - + func getWalletAddress(byAdamantAddress address: String) async throws -> String { return address } - + func loadTransactions(offset: Int, limit: Int) async throws -> Int { .zero } - + func getTransactionsHistory(offset: Int, limit: Int) async throws -> [TransactionDetails] { [] } - + func getLocalTransactionHistory() -> [TransactionDetails] { [] } - - func updateStatus(for id: String, status: TransactionStatus?) { } - + + func updateStatus(for id: String, status: TransactionStatus?) {} + func statusInfoFor(transaction: CoinTransaction) async -> TransactionStatusInfo { .init(sentDate: nil, status: .notInitiated) } - - func initWallet(withPassphrase: String) async throws -> WalletAccount { + + func initWallet(withPassphrase: String, withPassword: String) async throws -> WalletAccount { throw InternalAPIError.unknownError } - - func setInitiationFailed(reason: String) { } - + + func setInitiationFailed(reason: String) {} } // MARK: - NSFetchedResultsControllerDelegate extension AdmWalletService: NSFetchedResultsControllerDelegate { - func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { + func controller( + _ controller: NSFetchedResultsController, + didChange anObject: Any, + at indexPath: IndexPath?, + for type: NSFetchedResultsChangeType, + newIndexPath: IndexPath? + ) { guard let newCount = controller.fetchedObjects?.count, let wallet = wallet as? AdmWallet else { return } - + if newCount != wallet.notifications { wallet.notifications = newCount postUpdateNotification(with: wallet) @@ -242,18 +261,24 @@ extension AdmWalletService: SwinjectDependentService { transfersProvider = container.resolve(TransfersProvider.self) coreDataStack = container.resolve(CoreDataStack.self) vibroService = container.resolve(VibroService.self) - + Task { let controller = await transfersProvider.unreadTransfersController() - + do { try controller.performFetch() } catch { print("AdmWalletService: Error performing fetch: \(error)") } - + controller.delegate = self transfersController = controller } } } + +extension AdmWalletService { + static var adamantTimestampCorrection: TimeInterval { + 0.5 + } +} diff --git a/Adamant/Modules/Wallets/Adamant/AdmWalletViewController.swift b/Adamant/Modules/Wallets/Adamant/AdmWalletViewController.swift index ed7f63e58..c9c1dfa83 100644 --- a/Adamant/Modules/Wallets/Adamant/AdmWalletViewController.swift +++ b/Adamant/Modules/Wallets/Adamant/AdmWalletViewController.swift @@ -6,20 +6,20 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import SafariServices -import Eureka import CommonKit +import Eureka +import SafariServices +import UIKit extension String.adamant.wallets { static var adamant: String { String.localized("AccountTab.Wallets.adamant_wallet", comment: "Account tab: Adamant wallet") } - + static var sendAdm: String { String.localized("AccountTab.Row.SendAdm", comment: "Account tab: 'Send ADM tokens' button") } - + static var buyAdmTokens: String { String.localized("AccountTab.Row.AnonymouslyBuyADM", comment: "Account tab: Anonymously buy ADM tokens") } @@ -27,24 +27,30 @@ extension String.adamant.wallets { static var exchangeInChatAdmTokens: String { String.localized("AccountTab.Row.ExchangeADMInChat", comment: "Account tab: Exchange ADM in chat") } - + static var exchangesOnCoinMarketCap: String { String.localized("AccountTab.Row.ExchangesOnCoinMarketCap", comment: "Account tab: Exchanges on CMC") } - + static var exchangesOnCoinGecko: String { String.localized("AccountTab.Row.ExchangesOnCoinGecko", comment: "Account tab: Exchanges on CoinGecko") } - + // URLs static func getFreeTokensUrl(for address: String) -> String { - return String.localizedStringWithFormat(.localized("AccountTab.FreeTokens.UrlFormat", comment: "Account tab: A full 'Get free tokens' link, with %@ as address"), address) + return String.localizedStringWithFormat( + .localized("AccountTab.FreeTokens.UrlFormat", comment: "Account tab: A full 'Get free tokens' link, with %@ as address"), + address + ) } - + static func buyTokensUrl(for address: String) -> String { - return String.localizedStringWithFormat(.localized("AccountTab.BuyTokens.UrlFormat", comment: "Account tab: A full 'Buy tokens' link, with %@ as address"), address) + return String.localizedStringWithFormat( + .localized("AccountTab.BuyTokens.UrlFormat", comment: "Account tab: A full 'Buy tokens' link, with %@ as address"), + address + ) } - + static let getFreeTokensUrlFormat = "" static let buyTokensUrlFormat = "" } @@ -53,7 +59,7 @@ final class AdmWalletViewController: WalletViewControllerBase { // MARK: - Rows & Sections enum Rows { case stakeAdm, buyTokens, freeTokens - + var tag: String { switch self { case .stakeAdm: return "stakeAdm" @@ -61,7 +67,7 @@ final class AdmWalletViewController: WalletViewControllerBase { case .freeTokens: return "frrTkns" } } - + var localized: String { switch self { case .stakeAdm: return .localized("AccountTab.Row.StakeAdm", comment: "Stake ADM tokens' row") @@ -69,7 +75,7 @@ final class AdmWalletViewController: WalletViewControllerBase { case .freeTokens: return .localized("AccountTab.Row.FreeTokens", comment: "Account tab: 'Get free tokens' button") } } - + var image: UIImage? { switch self { case .stakeAdm: return .asset(named: "row_stake") @@ -78,27 +84,27 @@ final class AdmWalletViewController: WalletViewControllerBase { } } } - + // MARK: - Props & Deps var hideFreeTokensRow = false - + // MARK: - Lifecycle - + override func viewDidLoad() { super.viewDidLoad() - + if let balance = service?.core.wallet?.balance { hideFreeTokensRow = balance > 0 } else { hideFreeTokensRow = true } - + guard let section = form.allSections.last else { return } - + // MARK: Rows - + let stakeAdmRow = LabelRow { $0.tag = Rows.stakeAdm.tag $0.title = Rows.stakeAdm.localized @@ -108,7 +114,7 @@ final class AdmWalletViewController: WalletViewControllerBase { $0.cell.backgroundColor = UIColor.adamant.cellColor }.cellUpdate { (cell, row) in cell.accessoryType = .disclosureIndicator - + row.title = Rows.stakeAdm.localized }.onCellSelection { [weak self] (_, row) in guard let self = self else { return } @@ -116,13 +122,13 @@ final class AdmWalletViewController: WalletViewControllerBase { row.deselect() if let split = splitViewController { - let details = UINavigationController(rootViewController:vc) + let details = UINavigationController(rootViewController: vc) split.showDetailViewController(details, sender: self) } else { navigationController?.pushViewController(vc, animated: true) } } - + let buyTokensRow = LabelRow { $0.tag = Rows.buyTokens.tag $0.title = Rows.buyTokens.localized @@ -142,22 +148,25 @@ final class AdmWalletViewController: WalletViewControllerBase { row.deselect() if let split = splitViewController { - let details = UINavigationController(rootViewController:vc) + let details = UINavigationController(rootViewController: vc) split.showDetailViewController(details, sender: self) } else { navigationController?.pushViewController(vc, animated: true) } } - + let freeTokensRow = LabelRow { $0.tag = Rows.freeTokens.tag $0.title = Rows.freeTokens.localized $0.cell.imageView?.image = Rows.freeTokens.image $0.cell.imageView?.tintColor = UIColor.adamant.tableRowIcons $0.cell.selectionStyle = .gray - $0.hidden = Condition.function([], { [weak self] _ -> Bool in - return self?.hideFreeTokensRow ?? true - }) + $0.hidden = Condition.function( + [], + { [weak self] _ -> Bool in + return self?.hideFreeTokensRow ?? true + } + ) $0.cell.backgroundColor = UIColor.adamant.cellColor }.cellUpdate { (cell, row) in cell.accessoryType = .disclosureIndicator @@ -175,19 +184,19 @@ final class AdmWalletViewController: WalletViewControllerBase { ) return } - + let safari = SFSafariViewController(url: url) safari.preferredControlTintColor = UIColor.adamant.primary safari.modalPresentationStyle = .overFullScreen self?.present(safari, animated: true, completion: nil) } } - + section.append(stakeAdmRow) section.append(buyTokensRow) section.append(freeTokensRow) - - // Notifications + + // Notifications if let service = service { NotificationCenter.default.addObserver( forName: service.core.walletUpdatedNotification, @@ -196,7 +205,7 @@ final class AdmWalletViewController: WalletViewControllerBase { using: { [weak self] _ in MainActor.assumeIsolatedSafe { self?.updateRows() } } ) } - + NotificationCenter.default.addObserver( forName: .AdamantAccountService.userLoggedIn, object: nil, @@ -207,18 +216,18 @@ final class AdmWalletViewController: WalletViewControllerBase { self?.tableView.reloadData() } } - + setColors() } - + override func sendRowLocalizedLabel() -> NSAttributedString { return NSAttributedString(string: String.adamant.wallets.sendAdm) } - + override func encodeForQr(address: String) -> String? { return AdamantUriTools.encode(request: AdamantUri.address(address: address, params: nil)) } - + override func adressRow() -> LabelRow { let addressRow = LabelRow { $0.tag = BaseRows.address.tag @@ -242,51 +251,54 @@ final class AdmWalletViewController: WalletViewControllerBase { } if let address = self?.service?.core.wallet?.address, - let explorerAddress = self?.service?.core.explorerAddress, - let explorerAddressUrl = URL(string: explorerAddress + address) { + let explorerAddress = self?.service?.core.explorerAddress, + let explorerAddressUrl = URL(string: explorerAddress + address) + { let encodedAddress = AdamantUriTools.encode(request: AdamantUri.address(address: address, params: nil)) self?.dialogService.presentShareAlertFor( stringForPasteboard: address, stringForShare: encodedAddress, stringForQR: encodedAddress, - types: [.copyToPasteboard, - .share, - .generateQr( - encodedContent: encodedAddress, - sharingTip: address, - withLogo: true - ), - .openInExplorer(url: explorerAddressUrl) + types: [ + .copyToPasteboard, + .share, + .generateQr( + encodedContent: encodedAddress, + sharingTip: address, + withLogo: true + ), + .openInExplorer(url: explorerAddressUrl) ], excludedActivityTypes: ShareContentType.address.excludedActivityTypes, animated: true, from: cell, - completion: completion) + completion: completion + ) } } return addressRow } - + override func setTitle() { walletTitleLabel.text = String.adamant.wallets.adamant } - + func updateRows() { guard let admService = service?.core as? AdmWalletService, - let wallet = admService.wallet as? AdmWallet + let wallet = admService.wallet as? AdmWallet else { return } - + hideFreeTokensRow = wallet.balance > 0 - + if let row: LabelRow = form.rowBy(tag: Rows.freeTokens.tag) { row.evaluateHidden() } NotificationCenter.default.post(name: Notification.Name.WalletViewController.heightUpdated, object: self) } - + override func includeLogoInQR() -> Bool { return true } diff --git a/Adamant/Modules/Wallets/Adamant/BuyAndSellViewController.swift b/Adamant/Modules/Wallets/Adamant/BuyAndSellViewController.swift index ea7a02a8a..c6a44ffd6 100644 --- a/Adamant/Modules/Wallets/Adamant/BuyAndSellViewController.swift +++ b/Adamant/Modules/Wallets/Adamant/BuyAndSellViewController.swift @@ -6,10 +6,10 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UIKit +import CommonKit import Eureka import SafariServices -import CommonKit +import UIKit final class BuyAndSellViewController: FormViewController { // MARK: Rows @@ -18,7 +18,7 @@ final class BuyAndSellViewController: FormViewController { case adamantSite case coinMarketCap case coinGecko - + var tag: String { switch self { case .adamantMessage: return "admChat" @@ -27,7 +27,7 @@ final class BuyAndSellViewController: FormViewController { case .coinGecko: return "coinGecko" } } - + var image: UIImage? { switch self { case .adamantMessage: return .asset(named: "row_logo") @@ -36,7 +36,7 @@ final class BuyAndSellViewController: FormViewController { case .coinGecko: return .asset(named: "row_coingecko") } } - + var localized: String { switch self { case .adamantMessage: return String.adamant.wallets.exchangeInChatAdmTokens @@ -45,7 +45,7 @@ final class BuyAndSellViewController: FormViewController { case .coinGecko: return String.adamant.wallets.exchangesOnCoinGecko } } - + var url: String { switch self { case .adamantMessage: return "" @@ -55,65 +55,72 @@ final class BuyAndSellViewController: FormViewController { } } } - + // MARK: - Props & Dependencies - + var accountService: AccountService! var dialogService: DialogService! - + var screenFactory: ScreensFactory! + var chatsProvider: ChatsProvider! // MARK: Init - + init() { super.init(style: .insetGrouped) } - + required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: - Lifecycle - + override func viewDidLoad() { super.viewDidLoad() navigationItem.title = AdmWalletViewController.Rows.buyTokens.localized - + let section = Section() - + // MARK: Adamant let admUrl: String - + if let account = accountService.account { admUrl = String.adamant.wallets.buyTokensUrl(for: account.address) } else { admUrl = Rows.adamantSite.url } - + // MARK: Adamant Chat - let admChatRow = buildUrlRow(title: Rows.adamantMessage.localized, value: nil, tag: Rows.adamantMessage.tag, urlRaw: admUrl, image: Rows.adamantMessage.image) - + let admChatRow = buildUrlRow( + title: Rows.adamantMessage.localized, + value: nil, + tag: Rows.adamantMessage.tag, + urlRaw: admUrl, + image: Rows.adamantMessage.image + ) + section.append(admChatRow) - + // MARK: Adamant Site let admRow = buildUrlRow(title: Rows.adamantSite.localized, value: nil, tag: Rows.adamantSite.tag, urlRaw: admUrl, image: Rows.adamantSite.image) - + section.append(admRow) - + // MARK: CoinMarketCap let coinMarketCap = buildUrlRow(for: .coinMarketCap) section.append(coinMarketCap) - + // MARK: CoinGecko let coinGecko = buildUrlRow(for: .coinGecko) section.append(coinGecko) - + form.append(section) - + setColors() } - + // MARK: - Tools - + private func buildUrlRow(for row: Rows) -> LabelRow { return buildUrlRow( title: row.localized, @@ -123,7 +130,7 @@ final class BuyAndSellViewController: FormViewController { image: row.image ) } - + private func buildUrlRow(title: String, value: String?, tag: String, urlRaw: String, image: UIImage?) -> LabelRow { let row = LabelRow { $0.tag = tag @@ -137,71 +144,53 @@ final class BuyAndSellViewController: FormViewController { }.onCellSelection { [weak self] (_, row) in row.deselect() if tag == Rows.adamantMessage.tag { - self?.openExchangeChat() + Task { + await self?.openExchangeChat() + } return } guard let url = URL(string: urlRaw) else { self?.dialogService.showError(withMessage: "Failed to create URL with string: \(urlRaw)", supportEmail: true, error: nil) return } - + let safari = SFSafariViewController(url: url) safari.preferredControlTintColor = UIColor.adamant.primary safari.modalPresentationStyle = .overFullScreen self?.present(safari, animated: true, completion: nil) } - + return row } - - private func openExchangeChat() { - var chatList: UINavigationController? - var chatDetail: ChatListViewController? - - guard let tabbar = self.tabBarController else { return } - - if let split = tabbar.viewControllers?.first as? UISplitViewController, - let navigation = split.viewControllers.first as? UINavigationController, - let vc = navigation.viewControllers.first as? ChatListViewController { - chatList = navigation - chatDetail = vc - } - - if let navigation = tabbar.viewControllers?.first as? UINavigationController, - let vc = navigation.viewControllers.first as? ChatListViewController { - chatList = navigation - chatDetail = vc - } - let chatroom = chatDetail?.chatsController?.fetchedObjects?.first(where: { room in - return room.partner?.address == AdamantContacts.adamantExchange.address - }) - - guard let chatroom = chatroom, - let chatDetail = chatDetail - else { + @MainActor + private func openExchangeChat() async { + guard let chatroom = await chatsProvider.getChatroom(for: AdamantContacts.adamantExchange.address) else { return } - - chatList?.popToRootViewController(animated: false) - chatList?.dismiss(animated: false, completion: nil) - tabbar.selectedIndex = 0 - - let vc = chatDetail.chatViewController(for: chatroom) - - if let split = chatDetail.splitViewController { - let chat = UINavigationController(rootViewController: vc) - split.showDetailViewController(chat, sender: self) - } else if let nav = chatDetail.navigationController { - nav.pushViewController(vc, animated: true) + + let vc = chatViewController(for: chatroom) + + if let navigationController = navigationController { + navigationController.pushViewController(vc, animated: true) } else { - vc.modalPresentationStyle = .overFullScreen - chatDetail.present(vc, animated: true) + vc.modalPresentationStyle = .fullScreen + present(vc, animated: true) } } - + + private func chatViewController(for chatroom: Chatroom, with messageId: String? = nil) -> ChatViewController { + let vc = screenFactory.makeChat() + vc.hidesBottomBarWhenPushed = true + vc.viewModel.setup( + account: accountService.account, + chatroom: chatroom, + messageIdToShow: messageId + ) + return vc + } // MARK: - Other - + private func setColors() { view.backgroundColor = UIColor.adamant.secondBackgroundColor tableView.backgroundColor = .clear diff --git a/Adamant/Modules/Wallets/BalanceTableViewCell.swift b/Adamant/Modules/Wallets/BalanceTableViewCell.swift index 4a782dbdd..f0a9f5c40 100644 --- a/Adamant/Modules/Wallets/BalanceTableViewCell.swift +++ b/Adamant/Modules/Wallets/BalanceTableViewCell.swift @@ -6,10 +6,10 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit import Eureka import FreakingSimpleRoundImageView import SnapKit +import UIKit // MARK: - Value struct public struct BalanceRowValue: Equatable { @@ -20,16 +20,16 @@ public struct BalanceRowValue: Equatable { // MARK: - Cell public final class BalanceTableViewCell: Cell, CellType { - + // MARK: Constants static let compactHeight: CGFloat = 50.0 static let fullHeight: CGFloat = 58.0 - + // MARK: IBOutlets @IBOutlet var cryptoBalanceLabel: UILabel! @IBOutlet var fiatBalanceLabel: UILabel! @IBOutlet var alertLabel: RoundedLabel! - + lazy var titleLabel: UILabel = { let label = UILabel() label.font = .systemFont(ofSize: 17) @@ -37,7 +37,7 @@ public final class BalanceTableViewCell: Cell, CellType { label.textColor = .adamant.textColor return label }() - + // MARK: Properties var cryptoValue: String? { get { @@ -47,7 +47,7 @@ public final class BalanceTableViewCell: Cell, CellType { cryptoBalanceLabel.text = newValue } } - + var fiatValue: String? { get { return fiatBalanceLabel.text @@ -57,7 +57,7 @@ public final class BalanceTableViewCell: Cell, CellType { fiatBalanceLabel.isHidden = newValue == nil } } - + var alertValue: Int? { get { if let raw = alertLabel.text { @@ -75,17 +75,17 @@ public final class BalanceTableViewCell: Cell, CellType { } } } - + public required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setupView() } - + required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupView() } - + private func setupView() { addSubview(titleLabel) titleLabel.snp.makeConstraints { make in @@ -93,18 +93,18 @@ public final class BalanceTableViewCell: Cell, CellType { make.centerY.equalToSuperview() } } - + // MARK: Update public override func update() { super.update() alertLabel.clipsToBounds = true alertLabel.textInsets = UIEdgeInsets(top: 1, left: 5, bottom: 1, right: 5) - + if let value = row.value { cryptoValue = value.crypto fiatValue = value.fiat alertValue = value.alert - + if let r = row as? BalanceRow { alertLabel.backgroundColor = r.alertBackgroundColor alertLabel.textColor = r.alertTextColor @@ -121,7 +121,7 @@ public final class BalanceTableViewCell: Cell, CellType { public final class BalanceRow: Row, RowType { var alertBackgroundColor: UIColor? var alertTextColor: UIColor? - + required public init(tag: String?) { super.init(tag: tag) // We set the cellProvider to load the .xib corresponding to our cell diff --git a/Adamant/Modules/Wallets/Bitcoin/BitcoinKitTransactionFactory.swift b/Adamant/Modules/Wallets/Bitcoin/BitcoinKitTransactionFactory.swift new file mode 100644 index 000000000..f4f95f89b --- /dev/null +++ b/Adamant/Modules/Wallets/Bitcoin/BitcoinKitTransactionFactory.swift @@ -0,0 +1,31 @@ +// +// Untitled.swift +// Adamant +// +// Created by Christian Benua on 17.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import BitcoinKit + +final class BitcoinKitTransactionFactory: BitcoinKitTransactionFactoryProtocol { + func createTransaction( + toAddress address: Address, + amount: UInt64, + fee: UInt64, + changeAddress: Address, + utxos: [UnspentTransaction], + lockTime: UInt32, + keys: [PrivateKey] + ) -> Transaction { + BitcoinKit.Transaction.createNewTransaction( + toAddress: address, + amount: amount, + fee: fee, + changeAddress: changeAddress, + utxos: utxos, + lockTime: lockTime, + keys: keys + ) + } +} diff --git a/Adamant/Modules/Wallets/Bitcoin/BitcoinKitTransactionFactoryProtocol.swift b/Adamant/Modules/Wallets/Bitcoin/BitcoinKitTransactionFactoryProtocol.swift new file mode 100644 index 000000000..44ea96c4f --- /dev/null +++ b/Adamant/Modules/Wallets/Bitcoin/BitcoinKitTransactionFactoryProtocol.swift @@ -0,0 +1,22 @@ +// +// BitcoinKitTransactionFactoryProtocol.swift +// Adamant +// +// Created by Christian Benua on 11.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import BitcoinKit + +// sourcery: AutoMockable +protocol BitcoinKitTransactionFactoryProtocol { + func createTransaction( + toAddress: Address, + amount: UInt64, + fee: UInt64, + changeAddress: Address, + utxos: [UnspentTransaction], + lockTime: UInt32, + keys: [PrivateKey] + ) -> BitcoinKit.Transaction +} diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcApiService.swift b/Adamant/Modules/Wallets/Bitcoin/BtcApiService.swift index 7c8e9b528..c5134a813 100644 --- a/Adamant/Modules/Wallets/Bitcoin/BtcApiService.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcApiService.swift @@ -15,7 +15,7 @@ final class BtcApiCore: BlockchainHealthCheckableService, Sendable { init(apiCore: APICoreProtocol) { self.apiCore = apiCore } - + func request( origin: NodeOrigin, _ request: @Sendable @escaping (APICoreProtocol, NodeOrigin) async -> ApiServiceResult @@ -25,7 +25,7 @@ final class BtcApiCore: BlockchainHealthCheckableService, Sendable { func getStatusInfo(origin: NodeOrigin) async -> WalletServiceResult { let startTimestamp = Date.now.timeIntervalSince1970 - + let response = await apiCore.sendRequestRPC( origin: origin, path: BtcApiCommands.getRPC(), @@ -38,47 +38,58 @@ final class BtcApiCore: BlockchainHealthCheckableService, Sendable { guard case let .success(data) = response else { return .failure(.internalError(.parsingFailed)) } - + let networkInfoModel = data.first( where: { $0.id == BtcApiCommands.networkInfoMethod } ) - + let blockchainInfoModel = data.first( where: { $0.id == BtcApiCommands.blockchainInfoMethod } ) - + guard let networkInfo: BtcNetworkInfoDTO = networkInfoModel?.serialize(), let blockchainInfo: BtcBlockchainInfoDTO = blockchainInfoModel?.serialize() else { return .failure(.internalError(.parsingFailed)) } - - return .success(.init( - ping: Date.now.timeIntervalSince1970 - startTimestamp, - height: blockchainInfo.blocks, - wsEnabled: false, - wsPort: nil, - version: .init([networkInfo.version]) - )) + + return .success( + .init( + ping: Date.now.timeIntervalSince1970 - startTimestamp, + height: blockchainInfo.blocks, + wsEnabled: false, + wsPort: nil, + version: .init([networkInfo.version]) + ) + ) } } -final class BtcApiService: ApiServiceProtocol { +protocol BtcApiServiceProtocol: ApiServiceProtocol { + func request( + waitsForConnectivity: Bool, + _ request: @Sendable @escaping (APICoreProtocol, NodeOrigin) async -> ApiServiceResult + ) async -> WalletServiceResult + + func getStatusInfo() async -> WalletServiceResult +} + +final class BtcApiService: BtcApiServiceProtocol { let api: BlockchainHealthCheckWrapper @MainActor var nodesInfoPublisher: AnyObservable { api.nodesInfoPublisher } - + @MainActor var nodesInfo: NodesListInfo { api.nodesInfo } - + func healthCheck() { api.healthCheck() } - + init(api: BlockchainHealthCheckWrapper) { self.api = api } - + func request( waitsForConnectivity: Bool, _ request: @Sendable @escaping (APICoreProtocol, NodeOrigin) async -> ApiServiceResult @@ -87,7 +98,7 @@ final class BtcApiService: ApiServiceProtocol { await core.request(origin: origin, request) } } - + func getStatusInfo() async -> WalletServiceResult { await api.request(waitsForConnectivity: false) { core, origin in await core.getStatusInfo(origin: origin) diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcTransactionDetailsViewController.swift b/Adamant/Modules/Wallets/Bitcoin/BtcTransactionDetailsViewController.swift index 12c171de0..12cda3dd9 100644 --- a/Adamant/Modules/Wallets/Bitcoin/BtcTransactionDetailsViewController.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcTransactionDetailsViewController.swift @@ -6,52 +6,52 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UIKit -import CommonKit import Combine +import CommonKit +import UIKit final class BtcTransactionDetailsViewController: TransactionDetailsViewControllerBase { // MARK: - Dependencies - + weak var service: BtcWalletService? { walletService?.core as? BtcWalletService } - + // MARK: - Properties - + private let autoupdateInterval: TimeInterval = 5.0 private var timerSubscription: AnyCancellable? - + private lazy var refreshControl: UIRefreshControl = { let control = UIRefreshControl() control.tintColor = .adamant.primary control.addTarget(self, action: #selector(refresh), for: UIControl.Event.valueChanged) return control }() - + // MARK: - Lifecycle - + override func viewDidLoad() { currencySymbol = BtcWalletService.currencySymbol - + super.viewDidLoad() - + if service != nil { tableView.refreshControl = refreshControl } - + if transaction != nil { startUpdate() } } - + // MARK: - Overrides - + override func explorerUrl(for transaction: TransactionDetails) -> URL? { let id = transaction.txId return URL(string: "\(BtcWalletService.explorerTx)\(id)") } - + @MainActor @objc func refresh(silent: Bool = false) { refreshTask = Task { @@ -59,29 +59,30 @@ final class BtcTransactionDetailsViewController: TransactionDetailsViewControlle refreshControl.endRefreshing() return } - + do { let trs = try await service.getTransaction(by: id, waitsForConnectivity: false) transaction = trs - + updateIncosinstentRowIfNeeded() tableView.reloadData() refreshControl.endRefreshing() } catch { refreshControl.endRefreshing() updateTransactionStatus() - + guard !silent else { return } dialogService.showRichError(error: error) } } } - + // MARK: - Autoupdate - + func startUpdate() { refresh(silent: true) - timerSubscription = Timer + timerSubscription = + Timer .publish(every: autoupdateInterval, on: .main, in: .common) .autoconnect() .sink { [weak self] _ in diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcTransactionsViewController.swift b/Adamant/Modules/Wallets/Bitcoin/BtcTransactionsViewController.swift index 50ec5ff4e..0884c81e3 100644 --- a/Adamant/Modules/Wallets/Bitcoin/BtcTransactionsViewController.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcTransactionsViewController.swift @@ -6,18 +6,18 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UIKit import BitcoinKit import CommonKit +import UIKit final class BtcTransactionsViewController: TransactionsListViewControllerBase { - + // MARK: - Dependencies - + private let addressBook: AddressBookService - + // MARK: - Init - + init( walletService: WalletService, dialogService: DialogService, @@ -26,7 +26,7 @@ final class BtcTransactionsViewController: TransactionsListViewControllerBase { addressBook: AddressBookService ) { self.addressBook = addressBook - + super.init( walletService: walletService, dialogService: dialogService, @@ -34,31 +34,31 @@ final class BtcTransactionsViewController: TransactionsListViewControllerBase { screensFactory: screensFactory ) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: - UITableView - + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) guard let address = walletService.core.wallet?.address, - let transaction = transactions[safe: indexPath.row] + let transaction = transactions[safe: indexPath.row] else { return } - + let controller = screensFactory.makeDetailsVC(service: walletService) - + controller.transaction = transaction if transaction.senderAddress.caseInsensitiveCompare(address) == .orderedSame { controller.senderName = String.adamant.transactionDetails.yourAddress } - + if transaction.recipientAddress.caseInsensitiveCompare(address) == .orderedSame { controller.recipientName = String.adamant.transactionDetails.yourAddress } - + navigationController?.pushViewController(controller, animated: true) } } diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcTransferViewController.swift b/Adamant/Modules/Wallets/Bitcoin/BtcTransferViewController.swift index 09b5dd10d..9f1e5fbe9 100644 --- a/Adamant/Modules/Wallets/Bitcoin/BtcTransferViewController.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcTransferViewController.swift @@ -6,18 +6,18 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UIKit -import Eureka import CommonKit +import Eureka +import UIKit final class BtcTransferViewController: TransferViewControllerBase { - + // MARK: Properties - + private var skipValueChange: Bool = false - + // MARK: Send - + @MainActor override func sendFunds() { let comments: String @@ -26,17 +26,17 @@ final class BtcTransferViewController: TransferViewControllerBase { } else { comments = "" } - + guard let service = walletCore as? BtcWalletService, - let recipient = recipientAddress, - let amount = amount, - let wallet = service.wallet + let recipient = recipientAddress, + let amount = amount, + let wallet = service.wallet else { return } - + dialogService.showProgress(withMessage: String.adamant.transfer.transferProcessingMessage, userInteractionEnable: false) - + Task { do { let transaction = try await service.createTransaction( @@ -45,15 +45,16 @@ final class BtcTransferViewController: TransferViewControllerBase { fee: transactionFee, comment: nil ) - + if await !doesNotContainSendingTx() { presentSendingError() return } - + // Send adm report if let reportRecipient = admReportRecipient, - let hash = transaction.txHash { + let hash = transaction.txHash + { try await reportTransferTo( admAddress: reportRecipient, amount: amount, @@ -61,7 +62,7 @@ final class BtcTransferViewController: TransferViewControllerBase { hash: hash ) } - + do { let simpleTransaction = SimpleTransactionDetails( txId: transaction.txID, @@ -72,10 +73,10 @@ final class BtcTransferViewController: TransferViewControllerBase { confirmationsValue: nil, blockValue: nil, isOutgoing: true, - transactionStatus: nil, + transactionStatus: nil, nonceRaw: nil ) - + service.coinStorage.append(simpleTransaction) try await service.sendTransaction(transaction) } catch { @@ -83,14 +84,14 @@ final class BtcTransferViewController: TransferViewControllerBase { for: transaction.txId, status: .failed ) - + throw error } - + Task { await service.update() } - + var detailTransaction: BtcTransaction? if let hash = transaction.txHash { detailTransaction = try? await service.getTransaction( @@ -98,7 +99,7 @@ final class BtcTransferViewController: TransferViewControllerBase { waitsForConnectivity: false ) } - + processTransaction( self, localTransaction: detailTransaction, @@ -106,7 +107,7 @@ final class BtcTransferViewController: TransferViewControllerBase { comments: comments, transaction: transaction ) - + dialogService.dismissProgress() dialogService.showSuccess(withMessage: String.adamant.transfer.transferSuccess) } catch { @@ -115,7 +116,7 @@ final class BtcTransferViewController: TransferViewControllerBase { } } } - + private func processTransaction( _ vc: BtcTransferViewController, localTransaction: BtcTransaction?, @@ -124,30 +125,30 @@ final class BtcTransferViewController: TransferViewControllerBase { transaction: TransactionDetails ) { vc.dialogService.showSuccess(withMessage: String.adamant.transfer.transferSuccess) - + let detailsVc = screensFactory.makeDetailsVC(service: service) detailsVc.transaction = localTransaction ?? transaction detailsVc.senderName = String.adamant.transactionDetails.yourAddress - + if recipientAddress == service.core.wallet?.address { detailsVc.recipientName = String.adamant.transactionDetails.yourAddress } else { detailsVc.recipientName = self.recipientName } - + if comments.count > 0 { detailsVc.comment = comments } - + vc.delegate?.transferViewController( vc, didFinishWithTransfer: transaction, detailsViewController: detailsVc ) } - + // MARK: Overrides - + override func recipientRow() -> BaseRow { let row = TextRow { $0.tag = BaseRows.address.tag @@ -155,11 +156,11 @@ final class BtcTransferViewController: TransferViewControllerBase { $0.cell.textField.setLineBreakMode() $0.cell.textField.keyboardType = .namePhonePad $0.cell.textField.autocorrectionType = .no - + $0.value = recipientAddress?.components( separatedBy: TransferViewControllerBase.invalidCharacters ).joined() - + if recipientIsReadonly { $0.disabled = true } @@ -172,19 +173,19 @@ final class BtcTransferViewController: TransferViewControllerBase { self?.skipValueChange = false return } - + row.cell.textField.text = row.value?.components( separatedBy: TransferViewControllerBase.invalidCharacters ).joined() - + self?.updateToolbar(for: row) }.onCellSelection { [weak self] (cell, _) in self?.shareValue(self?.recipientAddress, from: cell) } - + return row } - + override func defaultSceneTitle() -> String? { return String.adamant.sendBtc } diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcWallet.swift b/Adamant/Modules/Wallets/Bitcoin/BtcWallet.swift index d220e1835..2e69baf7e 100644 --- a/Adamant/Modules/Wallets/Bitcoin/BtcWallet.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcWallet.swift @@ -6,24 +6,24 @@ // Copyright © 2019 Adamant. All rights reserved. // -import Foundation @preconcurrency import BitcoinKit import CommonKit +import Foundation final class BtcWallet: WalletAccount, @unchecked Sendable { let unicId: String let addressEntity: Address let privateKey: PrivateKey let publicKey: PublicKey - + @Atomic var balance: Decimal = 0.0 @Atomic var notifications: Int = 0 @Atomic var minBalance: Decimal = 0.00001 @Atomic var minAmount: Decimal = 546e-8 @Atomic var isBalanceInitialized: Bool = false - + var address: String { addressEntity.stringValue } - + init( unicId: String, privateKey: PrivateKey, @@ -34,4 +34,18 @@ final class BtcWallet: WalletAccount, @unchecked Sendable { self.publicKey = privateKey.publicKey() self.addressEntity = try addressConverter.convert(publicKey: publicKey, type: .p2pkh) } + + #if DEBUG + @available(*, deprecated, message: "For testing purposes only") + init( + unicId: String, + privateKey: PrivateKey, + address: Address + ) { + self.unicId = unicId + self.privateKey = privateKey + self.publicKey = privateKey.publicKey() + self.addressEntity = address + } + #endif } diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcWalletFactory.swift b/Adamant/Modules/Wallets/Bitcoin/BtcWalletFactory.swift index a575d21f9..226fbee86 100644 --- a/Adamant/Modules/Wallets/Bitcoin/BtcWalletFactory.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcWalletFactory.swift @@ -6,16 +6,16 @@ // Copyright © 2023 Adamant. All rights reserved. // +import CommonKit import Swinject import UIKit -import CommonKit struct BtcWalletFactory: WalletFactory { typealias Service = WalletService - + let typeSymbol: String = BtcWalletService.richMessageType let assembler: Assembler - + func makeWalletVC(service: Service, screensFactory: ScreensFactory) -> WalletViewController { BtcWalletViewController( dialogService: assembler.resolve(DialogService.self)!, @@ -26,7 +26,7 @@ struct BtcWalletFactory: WalletFactory { service: service ) } - + func makeTransferListVC(service: Service, screensFactory: ScreensFactory) -> UIViewController { BtcTransactionsViewController( walletService: service, @@ -36,7 +36,7 @@ struct BtcWalletFactory: WalletFactory { addressBook: assembler.resolve(AddressBookService.self)! ) } - + func makeTransferVC(service: Service, screensFactory: ScreensFactory) -> TransferViewControllerBase { BtcTransferViewController( chatsProvider: assembler.resolve(ChatsProvider.self)!, @@ -52,18 +52,18 @@ struct BtcWalletFactory: WalletFactory { apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)! ) } - + func makeDetailsVC(service: Service, transaction: RichMessageTransaction) -> UIViewController? { guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) else { return nil } - + let comment: String? if let raw = transaction.getRichValue(for: RichContentKeys.transfer.comments), raw.count > 0 { comment = raw } else { comment = nil } - + return makeTransactionDetailsVC( hash: hash, senderId: transaction.senderId, @@ -76,14 +76,14 @@ struct BtcWalletFactory: WalletFactory { service: service ) } - + func makeDetailsVC(service: Service) -> TransactionDetailsViewControllerBase { makeTransactionDetailsVC(service: service) } } -private extension BtcWalletFactory { - func makeTransactionDetailsVC( +extension BtcWalletFactory { + fileprivate func makeTransactionDetailsVC( hash: String, senderId: String?, recipientId: String?, @@ -95,15 +95,16 @@ private extension BtcWalletFactory { service: Service ) -> UIViewController { let vc = makeTransactionDetailsVC(service: service) - + let amount: Decimal if let amountRaw = richTransaction.getRichValue(for: RichContentKeys.transfer.amount), - let decimal = Decimal(string: amountRaw) { + let decimal = Decimal(string: amountRaw) + { amount = decimal } else { amount = 0 } - + let failedTransaction = SimpleTransactionDetails( txId: hash, senderAddress: senderAddress, @@ -117,7 +118,7 @@ private extension BtcWalletFactory { transactionStatus: nil, nonceRaw: nil ) - + vc.senderId = senderId vc.recipientId = recipientId vc.comment = comment @@ -125,8 +126,8 @@ private extension BtcWalletFactory { vc.richTransaction = richTransaction return vc } - - func makeTransactionDetailsVC(service: Service) -> BtcTransactionDetailsViewController { + + fileprivate func makeTransactionDetailsVC(service: Service) -> BtcTransactionDetailsViewController { BtcTransactionDetailsViewController( dialogService: assembler.resolve(DialogService.self)!, currencyInfo: assembler.resolve(InfoServiceProtocol.self)!, diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+DynamicConstants.swift b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+DynamicConstants.swift deleted file mode 100644 index 652e18048..000000000 --- a/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+DynamicConstants.swift +++ /dev/null @@ -1,89 +0,0 @@ -import Foundation -import BigInt -import CommonKit - -extension BtcWalletService { - // MARK: - Constants - static let fixedFee: Decimal = 3.153e-05 - static let currencySymbol = "BTC" - static let currencyExponent: Int = -8 - static let qqPrefix: String = "bitcoin" - - static let healthCheckParameters = CoinHealthCheckParameters( - normalUpdateInterval: 360, - crucialUpdateInterval: 30, - onScreenUpdateInterval: 10, - threshold: 2, - normalServiceUpdateInterval: 330, - crucialServiceUpdateInterval: 30, - onScreenServiceUpdateInterval: 10 - ) - - static var newPendingInterval: Int { - 10000 - } - - static var oldPendingInterval: Int { - 3000 - } - - static var registeredInterval: Int { - 40000 - } - - static var newPendingAttempts: Int { - 20 - } - - static var oldPendingAttempts: Int { - 4 - } - - var tokenName: String { - "Bitcoin" - } - - var consistencyMaxTime: Double { - 10800 - } - - var minBalance: Decimal { - 1.0e-05 - } - - var minAmount: Decimal { - 5.46e-06 - } - - var defaultVisibility: Bool { - true - } - - var defaultOrdinalLevel: Int? { - 10 - } - - static var minNodeVersion: String? { - nil - } - - var transferDecimals: Int { - 8 - } - - static let explorerTx = "https://bitcoinexplorer.org/tx/" - static let explorerAddress = "https://bitcoinexplorer.org/address/" - - static var nodes: [Node] { - [ - Node.makeDefaultNode(url: URL(string: "https://btcnode1.adamant.im/bitcoind")!, altUrl: URL(string: "http://176.9.38.204:44099/bitcoind")), -Node.makeDefaultNode(url: URL(string: "https://btcnode3.adamant.im/bitcoind")!, altUrl: URL(string: "http://195.201.242.108:44099/bitcoind")), - ] - } - - static var serviceNodes: [Node] { - [ - - ] - } -} diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+RichMessageProvider.swift b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+RichMessageProvider.swift index 7dfacd12f..4c9aac855 100644 --- a/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+RichMessageProvider.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+RichMessageProvider.swift @@ -6,59 +6,59 @@ // Copyright © 2019 Adamant. All rights reserved. // +import CommonKit import Foundation import MessageKit import UIKit -import CommonKit extension BtcWalletService { var newPendingInterval: TimeInterval { .init(milliseconds: type(of: self).newPendingInterval) } - + var oldPendingInterval: TimeInterval { .init(milliseconds: type(of: self).oldPendingInterval) } - + var registeredInterval: TimeInterval { .init(milliseconds: type(of: self).registeredInterval) } - + var newPendingAttempts: Int { type(of: self).newPendingAttempts } - + var oldPendingAttempts: Int { type(of: self).oldPendingAttempts } - + var dynamicRichMessageType: String { return type(of: self).richMessageType } // MARK: Short description - + func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { let amount: String - + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount) else { return NSAttributedString(string: "⬅️ \(BtcWalletService.currencySymbol)") } - + if let decimal = Decimal(string: raw) { amount = AdamantBalanceFormat.full.format(decimal) } else { amount = raw } - + let string: String if transaction.isOutgoing { string = "⬅️ \(amount) \(BtcWalletService.currencySymbol)" } else { string = "➡️ \(amount) \(BtcWalletService.currencySymbol)" } - + return NSAttributedString(string: string) } } diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+RichMessageProviderWithStatusCheck.swift b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+RichMessageProviderWithStatusCheck.swift index 28f38c6dc..ff73b6b0f 100644 --- a/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+RichMessageProviderWithStatusCheck.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+RichMessageProviderWithStatusCheck.swift @@ -6,29 +6,29 @@ // Copyright © 2019 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation extension BtcWalletService { func statusInfoFor(transaction: CoinTransaction) async -> TransactionStatusInfo { let hash: String? - + if let transaction = transaction as? RichMessageTransaction { hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) } else { hash = transaction.txId } - + guard let hash = hash else { return .init(sentDate: nil, status: .inconsistent(.wrongTxHash)) } - + do { let btcTransaction = try await getTransaction( by: hash, waitsForConnectivity: true ) - + return await .init( sentDate: btcTransaction.dateValue, status: getStatus(transaction: transaction, btcTransaction: btcTransaction) @@ -39,80 +39,80 @@ extension BtcWalletService { } } -private extension BtcWalletService { - func getStatus( +extension BtcWalletService { + fileprivate func getStatus( transaction: CoinTransaction, btcTransaction: BtcTransaction ) async -> TransactionStatus { guard let status = btcTransaction.transactionStatus else { return .inconsistent(.unknown) } - + guard status == .success else { return status } - + // MARK: Check address - + var realSenderAddress = btcTransaction.senderAddress var realRecipientAddress = btcTransaction.recipientAddress - + if transaction is RichMessageTransaction { guard let senderAddress = try? await getWalletAddress(byAdamantAddress: transaction.senderAddress) else { return .inconsistent(.senderCryptoAddressUnavailable(tokenSymbol)) } - + guard let recipientAddress = try? await getWalletAddress(byAdamantAddress: transaction.recipientAddress) else { return .inconsistent(.recipientCryptoAddressUnavailable(tokenSymbol)) } - + realSenderAddress = senderAddress realRecipientAddress = recipientAddress } - + guard btcTransaction.senderAddress.caseInsensitiveCompare(realSenderAddress) == .orderedSame else { return .inconsistent(.senderCryptoAddressMismatch(tokenSymbol)) } - + guard btcTransaction.recipientAddress.caseInsensitiveCompare(realRecipientAddress) == .orderedSame else { return .inconsistent(.recipientCryptoAddressMismatch(tokenSymbol)) } - + if transaction.isOutgoing { - guard btcWallet?.address.caseInsensitiveCompare(btcTransaction.senderAddress) == .orderedSame else { - return .inconsistent(.senderCryptoAddressMismatch(tokenSymbol)) - } - } else { - guard btcWallet?.address.caseInsensitiveCompare(btcTransaction.recipientAddress) == .orderedSame else { - return .inconsistent(.recipientCryptoAddressMismatch(tokenSymbol)) - } - } - + guard btcWallet?.address.caseInsensitiveCompare(btcTransaction.senderAddress) == .orderedSame else { + return .inconsistent(.senderCryptoAddressMismatch(tokenSymbol)) + } + } else { + guard btcWallet?.address.caseInsensitiveCompare(btcTransaction.recipientAddress) == .orderedSame else { + return .inconsistent(.recipientCryptoAddressMismatch(tokenSymbol)) + } + } + // MARK: Check amount if let reported = reportedValue(for: transaction) { guard reported == btcTransaction.amountValue else { return .inconsistent(.wrongAmount) } } - + return .success } - - func reportedValue(for transaction: CoinTransaction) -> Decimal? { + + fileprivate func reportedValue(for transaction: CoinTransaction) -> Decimal? { guard let transaction = transaction as? RichMessageTransaction else { return transaction.amountValue } - + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), let reportedValue = AdamantBalanceFormat.deserializeBalance(from: raw) else { return nil } - + return reportedValue } } diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+Send.swift b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+Send.swift index 4056fe11d..153d60cee 100644 --- a/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+Send.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService+Send.swift @@ -6,13 +6,13 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UIKit import Alamofire import BitcoinKit +import UIKit extension BtcWalletService: WalletServiceTwoStepSend { typealias T = BitcoinKit.Transaction - + // MARK: Create & Send func createTransaction( recipient: String, @@ -24,46 +24,47 @@ extension BtcWalletService: WalletServiceTwoStepSend { guard let wallet = self.btcWallet else { throw WalletServiceError.notLogged } - + let key = wallet.privateKey - + guard let toAddress = try? addressConverter.convert(address: recipient) else { throw WalletServiceError.accountNotFound } - + let rawAmount = NSDecimalNumber(decimal: amount * BtcWalletService.multiplier).uint64Value let fee = NSDecimalNumber(decimal: fee * BtcWalletService.multiplier).uint64Value - + // MARK: 2. Search for unspent transactions let utxos = try await getUnspentTransactions() - + // MARK: 3. Check if we have enought money - + let totalAmount: UInt64 = UInt64(utxos.reduce(0) { $0 + $1.output.value }) - guard totalAmount >= rawAmount + fee else { // This shit can crash BitcoinKit + guard totalAmount >= rawAmount + fee else { // This shit can crash BitcoinKit throw WalletServiceError.notEnoughMoney } - + // MARK: 4. Create local transaction - - let transaction = BitcoinKit.Transaction.createNewTransaction( + + let transaction = btcTransactionFactory.createTransaction( toAddress: toAddress, amount: rawAmount, fee: fee, changeAddress: wallet.addressEntity, utxos: utxos, + lockTime: 0, keys: [key] ) - + return transaction } - + func sendTransaction(_ transaction: BitcoinKit.Transaction) async throws { // MARK: Prepare params - + let txHex = transaction.serialized().hex - + // MARK: Sending request let responseData = try await btcApiService.request(waitsForConnectivity: false) { core, origin in await core.sendRequest( @@ -79,15 +80,15 @@ extension BtcWalletService: WalletServiceTwoStepSend { guard response != transaction.txId else { return } throw WalletServiceError.remoteServiceError(message: response) } - + func getUnspentTransactions() async throws -> [UnspentTransaction] { guard let wallet = self.btcWallet else { throw WalletServiceError.notLogged } - + let address = wallet.address let parameters = ["noCache": "1"] - + let responseData = try await btcApiService.request(waitsForConnectivity: false) { core, origin in await core.sendRequest( origin: origin, @@ -97,7 +98,7 @@ extension BtcWalletService: WalletServiceTwoStepSend { encoding: .url ) }.get() - + guard let items = try? Self.jsonDecoder.decode( [BtcUnspentTransactionResponse].self, @@ -106,26 +107,26 @@ extension BtcWalletService: WalletServiceTwoStepSend { else { throw WalletServiceError.internalError(message: "BTC Wallet: not valid response", error: nil) } - + var utxos = [UnspentTransaction]() for item in items { guard item.status.confirmed else { continue } - + let value = NSDecimalNumber(decimal: item.value).uint64Value - + let lockScript = wallet.addressEntity.lockingScript let txHash = Data(hex: item.txId).map { Data($0.reversed()) } ?? Data() let txIndex = item.vout - + let unspentOutput = TransactionOutput(value: value, lockingScript: lockScript) let unspentOutpoint = TransactionOutPoint(hash: txHash, index: txIndex) let utxo = UnspentTransaction(output: unspentOutput, outpoint: unspentOutpoint) - + utxos.append(utxo) } - + return utxos } } diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcWalletService.swift b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService.swift index 8863452ba..02c6e952e 100644 --- a/Adamant/Modules/Wallets/Bitcoin/BtcWalletService.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcWalletService.swift @@ -6,12 +6,13 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UIKit -import Swinject import Alamofire import BitcoinKit import Combine import CommonKit +import Swinject +import UIKit +import Web3Core enum DefaultBtcTransferFee: Decimal { case high = 24000 @@ -23,17 +24,17 @@ struct BtcApiCommands { static let blockchainInfoMethod: String = "getblockchaininfo" static let networkInfoMethod: String = "getnetworkinfo" - + static func getRPC() -> String { return "/bitcoind" } - + static func getHeight() -> String { return "/blocks/tip/height" } static func getFeeRate() -> String { - return "/fee-estimates" //this._get('').then(estimates => estimates['2']) + return "/fee-estimates" //this._get('').then(estimates => estimates['2']) } static func balance(for address: String) -> String { @@ -51,11 +52,11 @@ struct BtcApiCommands { static func getTransaction(by hash: String) -> String { return "/tx/\(hash)" } - + static func getUnspentTransactions(for address: String) -> String { return "/address/\(address)/utxo" } - + static func sendTransaction() -> String { return "/tx" } @@ -70,67 +71,69 @@ extension String.adamant { } } -final class BtcWalletService: WalletCoreProtocol, @unchecked Sendable { +final class BtcWalletService: WalletCoreProtocol, WalletStaticCoreProtocol, @unchecked Sendable { + static let currencySymbol = "BTC" var tokenSymbol: String { type(of: self).currencySymbol } - + var tokenLogo: UIImage { type(of: self).currencyLogo } - + static var tokenNetworkSymbol: String { "BTC" } - + var tokenContract: String { return "" } - - var tokenUnicID: String { + + var tokenUniqueID: String { Self.tokenNetworkSymbol + tokenSymbol } - + var richMessageType: String { return Self.richMessageType - } + } var qqPrefix: String { return Self.qqPrefix - } + } var isSupportIncreaseFee: Bool { return true } - + var isIncreaseFeeEnabled: Bool { - return increaseFeeService.isIncreaseFeeEnabled(for: tokenUnicID) + return increaseFeeService.isIncreaseFeeEnabled(for: tokenUniqueID) } - + var nodeGroups: [NodeGroup] { [.btc] } - + var explorerAddress: String { Self.explorerAddress } - + var wallet: WalletAccount? { return btcWallet } - + // MARK: RichMessageProvider properties static let richMessageType = "btc_transaction" - + // MARK: - Dependencies var apiService: AdamantApiServiceProtocol! - var btcApiService: BtcApiService! + var btcApiService: BtcApiServiceProtocol! + var btcTransactionFactory: BitcoinKitTransactionFactoryProtocol! var accountService: AccountService! var dialogService: DialogService! var increaseFeeService: IncreaseFeeService! var addressConverter: AddressConverter! var coreDataStack: CoreDataStack! var vibroService: VibroService! - + // MARK: - Constants static let currencyLogo = UIImage.asset(named: "bitcoin_wallet") ?? .init() static let multiplier = Decimal(sign: .plus, exponent: 8, significand: 1) @@ -141,86 +144,95 @@ final class BtcWalletService: WalletCoreProtocol, @unchecked Sendable { @Atomic private(set) var isWarningGasPrice = false @Atomic private var cachedWalletAddress: [String: String] = [:] @Atomic private var balanceInvalidationSubscription: AnyCancellable? - + static let kvsAddress = "btc:address" private let walletPath = "m/44'/0'/21'/0/0" - + // MARK: - Notifications let walletUpdatedNotification = Notification.Name("adamant.btcWallet.walletUpdated") let serviceEnabledChanged = Notification.Name("adamant.btcWallet.enabledChanged") let serviceStateChanged = Notification.Name("adamant.btcWallet.stateChanged") let transactionFeeUpdated = Notification.Name("adamant.btcWallet.feeUpdated") - + + @MainActor + private let walletUpdateSender = ObservableSender() + @MainActor + var walletUpdatePublisher: AnyObservable { + walletUpdateSender.eraseToAnyPublisher() + } + // MARK: - Delayed KVS save @Atomic private var balanceObserver: NSObjectProtocol? - + // MARK: - Properties @Atomic private(set) var btcWallet: BtcWallet? @Atomic private(set) var enabled = true @Atomic public var network: Network - + static let jsonDecoder = JSONDecoder() - + let defaultDispatchQueue = DispatchQueue( label: "im.adamant.btcWalletService", qos: .userInteractive, attributes: [.concurrent] ) - + @Atomic private var subscriptions = Set() - + @ObservableValue private(set) var transactions: [TransactionDetails] = [] @ObservableValue private(set) var hasMoreOldTransactions: Bool = true var transactionsPublisher: AnyObservable<[TransactionDetails]> { $transactions.eraseToAnyPublisher() } - + var hasMoreOldTransactionsPublisher: AnyObservable { $hasMoreOldTransactions.eraseToAnyPublisher() } - + @MainActor var hasEnabledNode: Bool { btcApiService.hasEnabledNode } - + @MainActor var hasEnabledNodePublisher: AnyObservable { btcApiService.hasEnabledNodePublisher } - + private(set) lazy var coinStorage: CoinStorageService = AdamantCoinStorageService( - coinId: tokenUnicID, + coinId: tokenUniqueID, coreDataStack: coreDataStack, blockchainType: richMessageType ) - + // MARK: - State @Atomic private(set) var state: WalletServiceState = .notInitiated - + private func setState(_ newState: WalletServiceState, silent: Bool = false) { guard newState != state else { return } - + state = newState - + if !silent { - NotificationCenter.default.post(name: serviceStateChanged, - object: self, - userInfo: [AdamantUserInfoKey.WalletService.walletState: state]) + NotificationCenter.default.post( + name: serviceStateChanged, + object: self, + userInfo: [AdamantUserInfoKey.WalletService.walletState: state] + ) } } - + init() { self.network = BTCMainnet() self.setState(.notInitiated) - + // Notifications addObservers() } - + func addObservers() { NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedIn, object: nil) @@ -228,14 +240,14 @@ final class BtcWalletService: WalletCoreProtocol, @unchecked Sendable { self?.update() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantAccountService.accountDataUpdated, object: nil) .sink { @MainActor [weak self] _ in self?.update() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedOut, object: nil) .sink { @MainActor [weak self] _ in @@ -251,7 +263,7 @@ final class BtcWalletService: WalletCoreProtocol, @unchecked Sendable { } .store(in: &subscriptions) } - + func addTransactionObserver() { coinStorage.transactionsPublisher .sink { [weak self] transactions in @@ -259,75 +271,80 @@ final class BtcWalletService: WalletCoreProtocol, @unchecked Sendable { } .store(in: &subscriptions) } - + func update() { Task { await update() } } - + @MainActor func update() async { guard let wallet = btcWallet else { return } - + switch state { case .notInitiated, .updating, .initiationFailed: return - + case .upToDate: break } - + setState(.updating) - + if let balance = try? await getBalance() { if wallet.balance < balance, wallet.isBalanceInitialized { vibroService.applyVibration(.success) } - + wallet.balance = balance markBalanceAsFresh(wallet) - - NotificationCenter.default.post( - name: walletUpdatedNotification, - object: self, - userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet] - ) + } else { + wallet.isBalanceInitialized = false } - + + NotificationCenter.default.post( + name: walletUpdatedNotification, + object: self, + userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet] + ) + + walletUpdateSender.send() + setState(.upToDate) - + if let rate = try? await getFeeRate() { feeRate = rate } - + if let height = try? await getCurrentHeight() { currentHeight = height } - + if let transactions = try? await getUnspentTransactions() { let feeRate = feeRate - + let fee = Decimal(transactions.count * 181 + 78) * feeRate var newTransactionFee = fee / BtcWalletService.multiplier - - newTransactionFee = isIncreaseFeeEnabled - ? newTransactionFee * defaultIncreaseFee - : newTransactionFee - + + newTransactionFee = + isIncreaseFeeEnabled + ? newTransactionFee * defaultIncreaseFee + : newTransactionFee + guard transactionFee != newTransactionFee else { return } - + transactionFee = newTransactionFee - + NotificationCenter.default.post(name: transactionFeeUpdated, object: self, userInfo: nil) } } - + func validate(address: String) -> AddressValidationResult { let address = try? addressConverter.convert(address: address) - + switch address?.scriptType { case .p2pk, .p2pkh, .p2sh, .p2multi, .p2wpkh, .p2wpkhSh, .p2wsh: return .valid @@ -362,41 +379,43 @@ final class BtcWalletService: WalletCoreProtocol, @unchecked Sendable { return output } - + private func markBalanceAsFresh(_ wallet: BtcWallet) { wallet.isBalanceInitialized = true - + balanceInvalidationSubscription = Task { [weak self] in try await Task.sleep(interval: Self.balanceLifetime, pauseInBackground: true) guard let self else { return } wallet.isBalanceInitialized = false - + NotificationCenter.default.post( name: walletUpdatedNotification, object: self, userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet] ) + + await walletUpdateSender.send() }.eraseToAnyCancellable() } public func isValid(bitcoinAddress address: String) -> Bool { (try? addressConverter.convert(address: address)) != nil } - + func getWalletAddress(byAdamantAddress address: String) async throws -> String { if let address = cachedWalletAddress[address], !address.isEmpty { return address } - + do { let result = try await apiService.get(key: BtcWalletService.kvsAddress, sender: address).get() - + guard let result = result else { throw WalletServiceError.walletNotInitiated } - + cachedWalletAddress[address] = result - + return result } catch _ as ApiServiceError { throw WalletServiceError.remoteServiceError( @@ -430,40 +449,45 @@ extension BtcWalletService { setState(.initiationFailed(reason: reason)) btcWallet = nil } - - func initWallet(withPassphrase passphrase: String) async throws -> WalletAccount { + + func initWallet(withPassphrase passphrase: String, withPassword password: String) async throws -> WalletAccount { guard let adamant = accountService.account else { throw WalletServiceError.notLogged } - + setState(.notInitiated) - + if enabled { enabled = false NotificationCenter.default.post(name: serviceEnabledChanged, object: self) } - - let privateKeyData = passphrase.data(using: .utf8)!.sha256() + + guard let privateKeyData = makeBinarySeed(withMnemonicSentence: passphrase, withSalt: password) else { + throw WalletServiceError.internalError(message: "BTC Wallet: failed to generate private key", error: nil) + } + let privateKey = PrivateKey(data: privateKeyData, network: self.network, isPublicKeyCompressed: true) let eWallet = try BtcWallet( - unicId: tokenUnicID, + unicId: tokenUniqueID, privateKey: privateKey, addressConverter: addressConverter ) self.btcWallet = eWallet let kvsAddressModel = makeKVSAddressModel(wallet: eWallet) - + NotificationCenter.default.post( name: walletUpdatedNotification, object: self, userInfo: [AdamantUserInfoKey.WalletService.wallet: eWallet] ) - + + await walletUpdateSender.send() + if !self.enabled { self.enabled = true NotificationCenter.default.post(name: self.serviceEnabledChanged, object: self) } - + // MARK: 4. Save address into KVS let service = self do { @@ -474,13 +498,13 @@ extension BtcWalletService { } throw WalletServiceError.accountNotFound } - + service.setState(.upToDate) - + Task { service.update() } - + return eWallet } catch let error as WalletServiceError { switch error { @@ -488,27 +512,33 @@ extension BtcWalletService { /// The ADM Wallet is not initialized. Check the balance of the current wallet /// and save the wallet address to kvs when dropshipping ADM service.setState(.upToDate) - + Task { await service.update() } - + if let kvsAddressModel { service.save(kvsAddressModel) { result in service.kvsSaveCompletionRecursion(kvsAddressModel, result: result) } } - + return eWallet - + default: service.setState(.upToDate) throw error } } - } + private func makeBinarySeed(withMnemonicSentence passphrase: String, withSalt salt: String) -> Data? { + guard !salt.isEmpty else { + return passphrase.data(using: .utf8)!.sha256() + } + + return BIP39.seedFromMmemonics(passphrase, password: salt, language: .english) + } } // MARK: - Dependencies @@ -521,9 +551,10 @@ extension BtcWalletService: SwinjectDependentService { increaseFeeService = container.resolve(IncreaseFeeService.self) addressConverter = container.resolve(AddressConverterFactory.self)?.make(network: network) btcApiService = container.resolve(BtcApiService.self) + btcTransactionFactory = container.resolve(BitcoinKitTransactionFactoryProtocol.self) vibroService = container.resolve(VibroService.self) coreDataStack = container.resolve(CoreDataStack.self) - + addTransactionObserver() } } @@ -534,15 +565,15 @@ extension BtcWalletService { guard let address = btcWallet?.address else { throw WalletServiceError.walletNotInitiated } - + return try await getBalance(address: address) } - + func getBalance(address: String) async throws -> Decimal { let response: BtcBalanceResponse = try await btcApiService.request(waitsForConnectivity: false) { api, origin in await api.sendRequestJsonResponse(origin: origin, path: BtcApiCommands.balance(for: address)) }.get() - + return response.value / BtcWalletService.multiplier } @@ -550,7 +581,7 @@ extension BtcWalletService { let response: [String: Decimal] = try await btcApiService.request(waitsForConnectivity: false) { api, origin in await api.sendRequestJsonResponse(origin: origin, path: BtcApiCommands.getFeeRate()) }.get() - + return response["2"] ?? 1 } @@ -571,19 +602,19 @@ extension BtcWalletService { completion(.failure(error: .notLogged)) return } - + guard adamant.balance >= AdamantApiService.KvsFee else { completion(.failure(error: .notEnoughMoney)) return } - + Task { - let result = await apiService.store(model) - + let result = await apiService.store(model, date: .now) + switch result { case .success: completion(.success) - + case .failure(let error): completion(.failure(error: .apiError(error))) } @@ -596,66 +627,74 @@ extension BtcWalletService { NotificationCenter.default.removeObserver(observer) balanceObserver = nil } - + switch result { case .success: break - + case .failure(let error): switch error { case .notEnoughMoney: // Possibly new account, we need to wait for dropship // Register observer - let observer = NotificationCenter.default.addObserver(forName: NSNotification.Name.AdamantAccountService.accountDataUpdated, object: nil, queue: nil) { [weak self] _ in + let observer = NotificationCenter.default.addObserver( + forName: NSNotification.Name.AdamantAccountService.accountDataUpdated, + object: nil, + queue: nil + ) { [weak self] _ in guard let balance = self?.accountService.account?.balance, balance > AdamantApiService.KvsFee else { return } - + self?.save(model) { [weak self] result in self?.kvsSaveCompletionRecursion(model, result: result) } } - + // Save referense to unregister it later balanceObserver = observer - + default: Task { @MainActor in dialogService.showRichError(error: error) } } } } - + private func kvsSaveCompletionRecursion(btcCheckpoint: Checkpoint, result: WalletServiceSimpleResult) { if let observer = balanceObserver { NotificationCenter.default.removeObserver(observer) balanceObserver = nil } - + switch result { case .success: break - + case .failure(let error): switch error { case .notEnoughMoney: // Possibly new account, we need to wait for dropship // Register observer - let observer = NotificationCenter.default.addObserver(forName: NSNotification.Name.AdamantAccountService.accountDataUpdated, object: nil, queue: nil) { [weak self] _ in + let observer = NotificationCenter.default.addObserver( + forName: NSNotification.Name.AdamantAccountService.accountDataUpdated, + object: nil, + queue: nil + ) { [weak self] _ in guard let balance = self?.accountService.account?.balance, balance > AdamantApiService.KvsFee else { return } } - + // Save referense to unregister it later balanceObserver = observer - + default: Task { @MainActor in dialogService.showRichError(error: error) } } } } - + private func makeKVSAddressModel(wallet: WalletAccount) -> KVSValueModel? { guard let keypair = accountService.keypair else { return nil } - + return .init( key: Self.kvsAddress, value: wallet.address, @@ -670,7 +709,7 @@ extension BtcWalletService { guard let address = self.wallet?.address else { throw WalletServiceError.notLogged } - + let items = try await getTransactions( for: address, fromTx: fromTx @@ -682,7 +721,7 @@ extension BtcWalletService { height: self.currentHeight ) } - + return transactions } @@ -705,7 +744,7 @@ extension BtcWalletService { guard let address = self.wallet?.address else { throw WalletServiceError.notLogged } - + let rawTransaction: RawBtcTransactionResponse = try await btcApiService.request( waitsForConnectivity: waitsForConnectivity ) { api, origin in @@ -714,7 +753,7 @@ extension BtcWalletService { path: BtcApiCommands.getTransaction(by: hash) ) }.get() - + return rawTransaction.asBtcTransaction( BtcTransaction.self, for: address, @@ -724,29 +763,30 @@ extension BtcWalletService { func loadTransactions(offset: Int, limit: Int) async throws -> Int { let trs = try await getTransactionsHistory(offset: offset, limit: limit) - + guard trs.count > 0 else { hasMoreOldTransactions = false return .zero } - + coinStorage.append(trs) - + return trs.count } - + func getTransactionsHistory(offset: Int, limit: Int) async throws -> [TransactionDetails] { - let txId = offset == .zero - ? transactions.first?.txId - : transactions.last?.txId - + let txId = + offset == .zero + ? transactions.first?.txId + : transactions.last?.txId + return try await getTransactions(fromTx: txId) } - + func getLocalTransactionHistory() -> [TransactionDetails] { transactions } - + func updateStatus(for id: String, status: TransactionStatus?) { coinStorage.updateStatus(for: id, status: status) } @@ -757,13 +797,13 @@ extension BtcWalletService: PrivateKeyGenerator { var rowTitle: String { return "Bitcoin" } - + var rowImage: UIImage? { return .asset(named: "bitcoin_wallet_row") } - + var keyFormat: KeyFormat { .WIF } - + func generatePrivateKeyFor(passphrase: String) -> String? { guard AdamantUtilities.validateAdamantPassphrase(passphrase: passphrase), @@ -772,13 +812,26 @@ extension BtcWalletService: PrivateKeyGenerator { return nil } - let privateKey = PrivateKey(data: privateKeyData, - network: self.network, - isPublicKeyCompressed: true) + let privateKey = PrivateKey( + data: privateKeyData, + network: self.network, + isPublicKeyCompressed: true + ) return privateKey.toWIF() } } +// MARK: test helpers + +#if DEBUG + extension BtcWalletService { + @available(*, deprecated, message: "For testing purposes only") + func setWalletForTests(_ wallet: BtcWallet?) { + self.btcWallet = wallet + } + } +#endif + final class BtcTransaction: BaseBtcTransaction { override var defaultCurrencySymbol: String? { BtcWalletService.currencySymbol } } diff --git a/Adamant/Modules/Wallets/Bitcoin/BtcWalletViewController.swift b/Adamant/Modules/Wallets/Bitcoin/BtcWalletViewController.swift index c74711b0f..af0cc024f 100644 --- a/Adamant/Modules/Wallets/Bitcoin/BtcWalletViewController.swift +++ b/Adamant/Modules/Wallets/Bitcoin/BtcWalletViewController.swift @@ -6,29 +6,29 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UIKit import CommonKit +import UIKit extension String.adamant { static var bitcoin: String { String.localized("AccountTab.Wallets.bitcoin_wallet", comment: "Account tab: Bitcoin wallet") } - + static var sendBtc: String { String.localized("AccountTab.Row.SendBtc", comment: "Account tab: 'Send BTC tokens' button") } } final class BtcWalletViewController: WalletViewControllerBase { - + override func sendRowLocalizedLabel() -> NSAttributedString { return NSAttributedString(string: String.adamant.sendBtc) } - + override func encodeForQr(address: String) -> String? { return "bitcoin:\(address)" } - + override func setTitle() { walletTitleLabel.text = String.adamant.bitcoin } diff --git a/Adamant/Modules/Wallets/Bitcoin/DTO/BtcTransactionResponse.swift b/Adamant/Modules/Wallets/Bitcoin/DTO/BtcTransactionResponse.swift index 05492ea11..911ca8fb5 100644 --- a/Adamant/Modules/Wallets/Bitcoin/DTO/BtcTransactionResponse.swift +++ b/Adamant/Modules/Wallets/Bitcoin/DTO/BtcTransactionResponse.swift @@ -16,7 +16,7 @@ struct RawBtcTransactionResponse: Decodable { case fee case status } - + let txId: String let inputs: [RawBtcInput] let outputs: [RawBtcOutput] @@ -29,7 +29,7 @@ struct RawBtcInput: Decodable { case txId = "txid" case prevout } - + let txId: String let prevout: RawBtcOutput } @@ -47,7 +47,7 @@ struct RawBtcOutput: Decodable { let container = try decoder.container(keyedBy: CodingKeys.self) self.address = try container.decode(String.self, forKey: .address) - + let raw = try container.decode(Decimal.self, forKey: .value) self.value = raw / BtcWalletService.multiplier } @@ -60,7 +60,7 @@ struct RawBtcStatus: Decodable { case hash = "block_hash" case time = "block_time" } - + let confirmed: Bool let height: Decimal? let hash: String? @@ -68,14 +68,14 @@ struct RawBtcStatus: Decodable { } extension RawBtcTransactionResponse { - func asBtcTransaction(_ as:T.Type, for address: String, height: Decimal? = nil) -> T { + func asBtcTransaction(_ as: T.Type, for address: String, height: Decimal? = nil) -> T { let transactionStatus: TransactionStatus = status.confirmed ? .success : .pending var date: Date? if let time = status.time { date = Date(timeIntervalSince1970: time.doubleValue) } - + let fee = fee / BtcWalletService.multiplier let confirmationsValue: String? @@ -88,56 +88,56 @@ extension RawBtcTransactionResponse { // Transfers var myInputs = inputs.filter { $0.prevout.address == address } var myOutputs = outputs.filter { $0.address == address } - + var totalInputsValue = myInputs.map { $0.prevout.value }.reduce(0, +) - fee var totalOutputsValue = myOutputs.map { $0.value }.reduce(0, +) - + if totalInputsValue == totalOutputsValue { totalInputsValue = 0 totalOutputsValue = 0 } - + if totalInputsValue > totalOutputsValue { while let out = myOutputs.first { totalInputsValue -= out.value totalOutputsValue -= out.value - + myOutputs.removeFirst() } } - + if totalInputsValue < totalOutputsValue { while let i = myInputs.first { totalInputsValue -= i.prevout.value totalOutputsValue -= i.prevout.value - + myInputs.removeFirst() } } - + let senders = Set(inputs.map { $0.prevout.address }) let recipients = Set(outputs.map { $0.address }) - + let sender: String let recipient: String - + if senders.count == 1 { sender = senders.first! } else { let filtered = senders.filter { $0 != address } - + if filtered.count == 1 { sender = filtered.first! } else { sender = String.adamant.dogeTransaction.senders(senders.count) } } - + if recipients.count == 1 { recipient = recipients.first! } else { let filtered = recipients.filter { $0 != address } - + if filtered.count == 1 { recipient = filtered.first! } else { @@ -162,38 +162,42 @@ extension RawBtcTransactionResponse { isOutgoing: isIncome, transactionStatus: transactionStatus ) - + return transaction } // MARK: Inputs if myInputs.count > 0 { - let inputTransaction = T(txId: txId, - dateValue: date, - blockValue: status.hash, - senderAddress: address, - recipientAddress: recipient, - amountValue: totalInputsValue, - feeValue: fee, - confirmationsValue: confirmationsValue, - isOutgoing: true, - transactionStatus: transactionStatus) - + let inputTransaction = T( + txId: txId, + dateValue: date, + blockValue: status.hash, + senderAddress: address, + recipientAddress: recipient, + amountValue: totalInputsValue, + feeValue: fee, + confirmationsValue: confirmationsValue, + isOutgoing: true, + transactionStatus: transactionStatus + ) + return inputTransaction } - + // MARK: Outputs - let outputTransaction = T(txId: txId, - dateValue: date, - blockValue: status.hash, - senderAddress: sender, - recipientAddress: address, - amountValue: totalOutputsValue, - feeValue: fee, - confirmationsValue: confirmationsValue, - isOutgoing: false, - transactionStatus: transactionStatus) - + let outputTransaction = T( + txId: txId, + dateValue: date, + blockValue: status.hash, + senderAddress: sender, + recipientAddress: address, + amountValue: totalOutputsValue, + feeValue: fee, + confirmationsValue: confirmationsValue, + isOutgoing: false, + transactionStatus: transactionStatus + ) + return outputTransaction } } diff --git a/Adamant/Modules/Wallets/Bitcoin/DTO/BtcUnspentTransactionResponse.swift b/Adamant/Modules/Wallets/Bitcoin/DTO/BtcUnspentTransactionResponse.swift index 32534c1b5..b6ec1ee76 100644 --- a/Adamant/Modules/Wallets/Bitcoin/DTO/BtcUnspentTransactionResponse.swift +++ b/Adamant/Modules/Wallets/Bitcoin/DTO/BtcUnspentTransactionResponse.swift @@ -15,7 +15,7 @@ struct BtcUnspentTransactionResponse: Decodable { case value case status } - + let txId: String let vout: UInt32 let value: Decimal diff --git a/Adamant/Modules/Wallets/DI/AdamantWalletFactoryCompose.swift b/Adamant/Modules/Wallets/DI/AdamantWalletFactoryCompose.swift index e71b6b3f3..164755874 100644 --- a/Adamant/Modules/Wallets/DI/AdamantWalletFactoryCompose.swift +++ b/Adamant/Modules/Wallets/DI/AdamantWalletFactoryCompose.swift @@ -10,7 +10,7 @@ import UIKit struct AdamantWalletFactoryCompose: WalletFactoryCompose { private let factories: [any WalletFactory] - + init( klyWalletFactory: KlyWalletFactory, dogeWalletFactory: DogeWalletFactory, @@ -30,79 +30,89 @@ struct AdamantWalletFactoryCompose: WalletFactoryCompose { admWalletFactory ] } - + func makeWalletVC(service: WalletService, screensFactory: ScreensFactory) -> WalletViewController { for factory in factories { - guard let result = tryMakeWalletVC( - factory: factory, - service: service, - screensFactory: screensFactory - ) else { continue } - + guard + let result = tryMakeWalletVC( + factory: factory, + service: service, + screensFactory: screensFactory + ) + else { continue } + return result } - + fatalError("No suitable factory") } - + func makeTransferListVC(service: WalletService, screenFactory: ScreensFactory) -> UIViewController { for factory in factories { - guard let result = tryMakeTransferListVC( - factory: factory, - service: service, - screenFactory: screenFactory - ) else { continue } - + guard + let result = tryMakeTransferListVC( + factory: factory, + service: service, + screenFactory: screenFactory + ) + else { continue } + return result } - + fatalError("No suitable factory") } - + func makeTransferVC(service: WalletService, screenFactory: ScreensFactory) -> TransferViewControllerBase { for factory in factories { - guard let result = tryMakeTransferVC( - factory: factory, - service: service, - screenFactory: screenFactory - ) else { continue } - + guard + let result = tryMakeTransferVC( + factory: factory, + service: service, + screenFactory: screenFactory + ) + else { continue } + return result } - + fatalError("No suitable factory") } - + func makeDetailsVC(service: WalletService) -> TransactionDetailsViewControllerBase { for factory in factories { - guard let result = tryMakeDetailsVC( - factory: factory, - service: service - ) else { continue } - + guard + let result = tryMakeDetailsVC( + factory: factory, + service: service + ) + else { continue } + return result } - + fatalError("No suitable factory") } - + func makeDetailsVC(service: WalletService, transaction: RichMessageTransaction) -> UIViewController? { for factory in factories { - guard let result = tryMakeDetailsVC( - factory: factory, - service: service, - transaction: transaction - ) else { continue } - + guard + let result = tryMakeDetailsVC( + factory: factory, + service: service, + transaction: transaction + ) + else { continue } + return result } - + fatalError("No suitable factory") } } -private extension AdamantWalletFactoryCompose { - func tryMakeWalletVC( +extension AdamantWalletFactoryCompose { + fileprivate func tryMakeWalletVC( factory: Factory, service: WalletService, screensFactory: ScreensFactory @@ -111,8 +121,8 @@ private extension AdamantWalletFactoryCompose { factory.makeWalletVC(service: $0, screensFactory: screensFactory) } } - - func tryMakeTransferListVC( + + fileprivate func tryMakeTransferListVC( factory: Factory, service: WalletService, screenFactory: ScreensFactory @@ -121,8 +131,8 @@ private extension AdamantWalletFactoryCompose { factory.makeTransferListVC(service: $0, screensFactory: screenFactory) } } - - func tryMakeTransferVC( + + fileprivate func tryMakeTransferVC( factory: Factory, service: WalletService, screenFactory: ScreensFactory @@ -131,8 +141,8 @@ private extension AdamantWalletFactoryCompose { factory.makeTransferVC(service: $0, screensFactory: screenFactory) } } - - func tryMakeDetailsVC( + + fileprivate func tryMakeDetailsVC( factory: Factory, service: WalletService, transaction: RichMessageTransaction @@ -141,8 +151,8 @@ private extension AdamantWalletFactoryCompose { factory.makeDetailsVC(service: $0, transaction: transaction) } } - - func tryMakeDetailsVC( + + fileprivate func tryMakeDetailsVC( factory: Factory, service: WalletService ) -> TransactionDetailsViewControllerBase? { @@ -150,8 +160,8 @@ private extension AdamantWalletFactoryCompose { factory.makeDetailsVC(service: $0) } } - - func tryExecuteFactoryMethod( + + fileprivate func tryExecuteFactoryMethod( factory: Factory, service: WalletService, method: (Factory.Service) -> Result diff --git a/Adamant/Modules/Wallets/DI/WalletFactory.swift b/Adamant/Modules/Wallets/DI/WalletFactory.swift index 1a86802b8..fb62e18b5 100644 --- a/Adamant/Modules/Wallets/DI/WalletFactory.swift +++ b/Adamant/Modules/Wallets/DI/WalletFactory.swift @@ -11,9 +11,9 @@ import UIKit @MainActor protocol WalletFactory { associatedtype Service = WalletService - + var typeSymbol: String { get } - + func makeWalletVC(service: Service, screensFactory: ScreensFactory) -> WalletViewController func makeTransferListVC(service: Service, screensFactory: ScreensFactory) -> UIViewController func makeTransferVC(service: Service, screensFactory: ScreensFactory) -> TransferViewControllerBase diff --git a/Adamant/Modules/Wallets/DI/WalletFactoryCompose.swift b/Adamant/Modules/Wallets/DI/WalletFactoryCompose.swift index b22b1b40b..973150774 100644 --- a/Adamant/Modules/Wallets/DI/WalletFactoryCompose.swift +++ b/Adamant/Modules/Wallets/DI/WalletFactoryCompose.swift @@ -14,7 +14,7 @@ protocol WalletFactoryCompose { func makeTransferListVC(service: WalletService, screenFactory: ScreensFactory) -> UIViewController func makeTransferVC(service: WalletService, screenFactory: ScreensFactory) -> TransferViewControllerBase func makeDetailsVC(service: WalletService) -> TransactionDetailsViewControllerBase - + func makeDetailsVC( service: WalletService, transaction: RichMessageTransaction diff --git a/Adamant/Modules/Wallets/Dash/DTO/DashErrorDTO.swift b/Adamant/Modules/Wallets/Dash/DTO/DashErrorDTO.swift index b2ed0e85c..ea152d5e1 100644 --- a/Adamant/Modules/Wallets/Dash/DTO/DashErrorDTO.swift +++ b/Adamant/Modules/Wallets/Dash/DTO/DashErrorDTO.swift @@ -11,7 +11,7 @@ import Foundation struct DashErrorDTO: Codable, LocalizedError { let code: Int let message: String - + var errorDescription: String? { message } diff --git a/Adamant/Modules/Wallets/Dash/DTO/DashGetAddressBalanceDTO.swift b/Adamant/Modules/Wallets/Dash/DTO/DashGetAddressBalanceDTO.swift index 4be8440c5..1d341f9ee 100644 --- a/Adamant/Modules/Wallets/Dash/DTO/DashGetAddressBalanceDTO.swift +++ b/Adamant/Modules/Wallets/Dash/DTO/DashGetAddressBalanceDTO.swift @@ -11,7 +11,7 @@ import Foundation struct DashGetAddressBalanceDTO: Codable { let method: String let params: [String] - + init(address: String) { self.method = "getaddressbalance" self.params = [address] diff --git a/Adamant/Modules/Wallets/Dash/DTO/DashGetAddressTransactionIds.swift b/Adamant/Modules/Wallets/Dash/DTO/DashGetAddressTransactionIds.swift index 304c48837..f97ed970d 100644 --- a/Adamant/Modules/Wallets/Dash/DTO/DashGetAddressTransactionIds.swift +++ b/Adamant/Modules/Wallets/Dash/DTO/DashGetAddressTransactionIds.swift @@ -11,7 +11,7 @@ import Foundation struct DashGetAddressTransactionIds: Codable { let method: String let params: [String] - + init(address: String) { self.method = "getaddresstxids" self.params = [address] diff --git a/Adamant/Modules/Wallets/Dash/DTO/DashGetBlockDTO.swift b/Adamant/Modules/Wallets/Dash/DTO/DashGetBlockDTO.swift index a5855c58a..4af04d268 100644 --- a/Adamant/Modules/Wallets/Dash/DTO/DashGetBlockDTO.swift +++ b/Adamant/Modules/Wallets/Dash/DTO/DashGetBlockDTO.swift @@ -11,7 +11,7 @@ import Foundation struct DashGetBlockDTO: Codable { let method: String let params: [String] - + init(hash: String) { self.method = "getblock" self.params = [hash] diff --git a/Adamant/Modules/Wallets/Dash/DTO/DashGetUnspentTransactionsDTO.swift b/Adamant/Modules/Wallets/Dash/DTO/DashGetUnspentTransactionsDTO.swift index e00e09cf1..0f52493c0 100644 --- a/Adamant/Modules/Wallets/Dash/DTO/DashGetUnspentTransactionsDTO.swift +++ b/Adamant/Modules/Wallets/Dash/DTO/DashGetUnspentTransactionsDTO.swift @@ -11,7 +11,7 @@ import Foundation struct DashGetUnspentTransactionDTO: Codable { let method: String let params: [String] - + init(address: String) { self.method = "getaddressutxos" self.params = [address] diff --git a/Adamant/Modules/Wallets/Dash/DTO/DashSendRawTransactionDTO.swift b/Adamant/Modules/Wallets/Dash/DTO/DashSendRawTransactionDTO.swift index cc80e2970..a84fbb92c 100644 --- a/Adamant/Modules/Wallets/Dash/DTO/DashSendRawTransactionDTO.swift +++ b/Adamant/Modules/Wallets/Dash/DTO/DashSendRawTransactionDTO.swift @@ -11,7 +11,7 @@ import Foundation struct DashSendRawTransactionDTO: Codable { let method: String let params: [String] - + init(txHex: String) { self.method = "sendrawtransaction" self.params = [txHex] diff --git a/Adamant/Modules/Wallets/Dash/DashApiService.swift b/Adamant/Modules/Wallets/Dash/DashApiService.swift index 61a03e81b..ed2c066f1 100644 --- a/Adamant/Modules/Wallets/Dash/DashApiService.swift +++ b/Adamant/Modules/Wallets/Dash/DashApiService.swift @@ -15,7 +15,7 @@ final class DashApiCore: BlockchainHealthCheckableService, Sendable { init(apiCore: APICoreProtocol) { self.apiCore = apiCore } - + func request( origin: NodeOrigin, _ request: @Sendable @escaping (APICoreProtocol, NodeOrigin) async -> ApiServiceResult @@ -25,7 +25,7 @@ final class DashApiCore: BlockchainHealthCheckableService, Sendable { func getStatusInfo(origin: NodeOrigin) async -> WalletServiceResult { let startTimestamp = Date.now.timeIntervalSince1970 - + let response = await apiCore.sendRequestRPC( origin: origin, path: .empty, @@ -34,51 +34,53 @@ final class DashApiCore: BlockchainHealthCheckableService, Sendable { .init(method: DashApiComand.blockchainInfoMethod) ] ) - + guard case let .success(data) = response else { return .failure(.internalError(.parsingFailed)) } - + let networkInfoModel = data.first( where: { $0.id == DashApiComand.networkInfoMethod } ) - + let blockchainInfoModel = data.first( where: { $0.id == DashApiComand.blockchainInfoMethod } ) - + guard let networkInfo: DashNetworkInfoDTO = networkInfoModel?.serialize(), let blockchainInfo: DashBlockchainInfoDTO = blockchainInfoModel?.serialize() else { return .failure(.internalError(.parsingFailed)) } - - return .success(.init( - ping: Date.now.timeIntervalSince1970 - startTimestamp, - height: blockchainInfo.blocks, - wsEnabled: false, - wsPort: nil, - version: .init(networkInfo.buildversion) - )) + + return .success( + .init( + ping: Date.now.timeIntervalSince1970 - startTimestamp, + height: blockchainInfo.blocks, + wsEnabled: false, + wsPort: nil, + version: .init(networkInfo.buildversion) + ) + ) } } -final class DashApiService: ApiServiceProtocol { +final class DashApiService: DashApiServiceProtocol { let api: BlockchainHealthCheckWrapper - + @MainActor var nodesInfoPublisher: AnyObservable { api.nodesInfoPublisher } - + @MainActor var nodesInfo: NodesListInfo { api.nodesInfo } - + func healthCheck() { api.healthCheck() } - + init(api: BlockchainHealthCheckWrapper) { self.api = api } - + func request( waitsForConnectivity: Bool, _ request: @Sendable @escaping (APICoreProtocol, NodeOrigin) async -> ApiServiceResult @@ -87,7 +89,7 @@ final class DashApiService: ApiServiceProtocol { await core.request(origin: origin, request) } } - + func getStatusInfo() async -> WalletServiceResult { await api.request(waitsForConnectivity: false) { core, origin in await core.getStatusInfo(origin: origin) diff --git a/Adamant/Modules/Wallets/Dash/DashApiServiceProtocol.swift b/Adamant/Modules/Wallets/Dash/DashApiServiceProtocol.swift new file mode 100644 index 000000000..d72b2cae4 --- /dev/null +++ b/Adamant/Modules/Wallets/Dash/DashApiServiceProtocol.swift @@ -0,0 +1,17 @@ +// +// DashApiServiceProtocol.swift +// Adamant +// +// Created by Christian Benua on 23.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import CommonKit +import Foundation + +protocol DashApiServiceProtocol: ApiServiceProtocol { + func request( + waitsForConnectivity: Bool, + _ request: @Sendable @escaping (APICoreProtocol, NodeOrigin) async -> ApiServiceResult + ) async -> WalletServiceResult +} diff --git a/Adamant/Modules/Wallets/Dash/DashLastTransactionStorage.swift b/Adamant/Modules/Wallets/Dash/DashLastTransactionStorage.swift new file mode 100644 index 000000000..39372f282 --- /dev/null +++ b/Adamant/Modules/Wallets/Dash/DashLastTransactionStorage.swift @@ -0,0 +1,56 @@ +// +// DashLastTransactionStorage.swift +// Adamant +// +// Created by Christian Benua on 22.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import CommonKit +import Foundation + +final class DashLastTransactionStorage: DashLastTransactionStorageProtocol { + + private let SecureStore: SecureStore + + init(SecureStore: SecureStore) { + self.SecureStore = SecureStore + } + + func getLastTransactionId() -> String? { + guard + let hash: String = self.SecureStore.get(Constants.transactionIdKey), + let timestampString: String = self.SecureStore.get(Constants.transactionTimeKey), + let timestamp = Double(string: timestampString) + else { return nil } + + let date = Date(timeIntervalSince1970: TimeInterval(timestamp)) + let timeAgo = -1 * date.timeIntervalSinceNow + + if timeAgo > Constants.tenMinutes { // 10m waiting for transaction complete + self.SecureStore.remove(Constants.transactionTimeKey) + self.SecureStore.remove(Constants.transactionIdKey) + return nil + } else { + return hash + } + } + + func setLastTransactionId(_ id: String?) { + if let value = id { + let timestamp = Date().timeIntervalSince1970 + self.SecureStore.set("\(timestamp)", for: Constants.transactionTimeKey) + self.SecureStore.set(value, for: Constants.transactionIdKey) + } else { + self.SecureStore.remove(Constants.transactionTimeKey) + self.SecureStore.remove(Constants.transactionIdKey) + } + } +} + +private enum Constants { + static let transactionTimeKey = "lastDashTransactionTime" + static let transactionIdKey = "lastDashTransactionId" + + static let tenMinutes: TimeInterval = 10 * 60 +} diff --git a/Adamant/Modules/Wallets/Dash/DashLastTransactionStorageProtocol.swift b/Adamant/Modules/Wallets/Dash/DashLastTransactionStorageProtocol.swift new file mode 100644 index 000000000..a10046004 --- /dev/null +++ b/Adamant/Modules/Wallets/Dash/DashLastTransactionStorageProtocol.swift @@ -0,0 +1,13 @@ +// +// DashLastTransactionStorageProtocol.swift +// Adamant +// +// Created by Christian Benua on 22.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +// sourcery: AutoMockable +protocol DashLastTransactionStorageProtocol: AnyObject { + func getLastTransactionId() -> String? + func setLastTransactionId(_ id: String?) +} diff --git a/Adamant/Modules/Wallets/Dash/DashMainnet.swift b/Adamant/Modules/Wallets/Dash/DashMainnet.swift index a95f117ea..2bbfdddc2 100644 --- a/Adamant/Modules/Wallets/Dash/DashMainnet.swift +++ b/Adamant/Modules/Wallets/Dash/DashMainnet.swift @@ -6,50 +6,50 @@ // Copyright © 2019 Adamant. All rights reserved. // -import Foundation import BitcoinKit +import Foundation final class DashMainnet: Network { override var name: String { return "livenet" } - + override var alias: String { return "mainnet" } - + override var scheme: String { return "dash" } - + override var magic: UInt32 { - return 0xbd6b0cbf + return 0xbd6b_0cbf } - + override var pubkeyhash: UInt8 { return 0x4c } - + override var privatekey: UInt8 { return 0xcc } - + override var scripthash: UInt8 { return 0x10 } - + override var xpubkey: UInt32 { - return 0x0488b21e + return 0x0488_b21e } - + override var xprivkey: UInt32 { - return 0x0488ade4 + return 0x0488_ade4 } - + override var port: UInt32 { return 9999 } - + override var dnsSeeds: [String] { return [ "dashnode1.adamant.im" diff --git a/Adamant/Modules/Wallets/Dash/DashTransactionDetailsViewController.swift b/Adamant/Modules/Wallets/Dash/DashTransactionDetailsViewController.swift index 21f94e302..851d3ec4c 100644 --- a/Adamant/Modules/Wallets/Dash/DashTransactionDetailsViewController.swift +++ b/Adamant/Modules/Wallets/Dash/DashTransactionDetailsViewController.swift @@ -6,56 +6,56 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UIKit -import Eureka -import CommonKit import Combine +import CommonKit +import Eureka +import UIKit final class DashTransactionDetailsViewController: TransactionDetailsViewControllerBase { // MARK: - Dependencies - + weak var service: DashWalletService? { walletService?.core as? DashWalletService } - + // MARK: - Properties - + private var cachedBlockInfo: (hash: String, height: String)? - + private let autoupdateInterval: TimeInterval = 5.0 private var timerSubscription: AnyCancellable? - + private lazy var refreshControl: UIRefreshControl = { let control = UIRefreshControl() control.tintColor = .adamant.primary control.addTarget(self, action: #selector(refresh), for: UIControl.Event.valueChanged) return control }() - + // MARK: - Lifecycle - + override func viewDidLoad() { currencySymbol = DashWalletService.currencySymbol - + super.viewDidLoad() if service != nil { tableView.refreshControl = refreshControl } - + refresh(silent: true) - + // MARK: Start update if transaction != nil { startUpdate() } } - + // MARK: - Overrides - + override func explorerUrl(for transaction: TransactionDetails) -> URL? { let id = transaction.txId - + return URL(string: "\(DashWalletService.explorerTx)\(id)") } - + @MainActor @objc func refresh(silent: Bool = false) { refreshTask = Task { [weak self] in @@ -66,11 +66,12 @@ final class DashTransactionDetailsViewController: TransactionDetailsViewControll else { return } - + do { let trs = try await service.getTransaction(by: id, waitsForConnectivity: false) if let blockInfo = self?.cachedBlockInfo, - blockInfo.hash == trs.blockHash { + blockInfo.hash == trs.blockHash + { self?.transaction = trs.asBtcTransaction(DashTransaction.self, for: address, blockId: blockInfo.height) self?.tableView.reloadData() @@ -97,24 +98,25 @@ final class DashTransactionDetailsViewController: TransactionDetailsViewControll ) self?.tableView.reloadData() } - + self?.updateIncosinstentRowIfNeeded() self?.refreshControl.endRefreshing() } catch { self?.refreshControl.endRefreshing() self?.updateTransactionStatus() - + guard !silent else { return } self?.dialogService.showRichError(error: error) } } } - + // MARK: Autoupdate - + func startUpdate() { refresh(silent: true) - timerSubscription = Timer + timerSubscription = + Timer .publish(every: autoupdateInterval, on: .main, in: .common) .autoconnect() .sink { [weak self] _ in diff --git a/Adamant/Modules/Wallets/Dash/DashTransactionsViewController.swift b/Adamant/Modules/Wallets/Dash/DashTransactionsViewController.swift index 6136ea9a0..324985813 100644 --- a/Adamant/Modules/Wallets/Dash/DashTransactionsViewController.swift +++ b/Adamant/Modules/Wallets/Dash/DashTransactionsViewController.swift @@ -6,53 +6,53 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UIKit import ProcedureKit +import UIKit final class DashTransactionsViewController: TransactionsListViewControllerBase { - + // MARK: - UITableView - + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let address = walletService.core.wallet?.address, - let transaction = transactions[safe: indexPath.row] + let transaction = transactions[safe: indexPath.row] else { return } - let controller = screensFactory.makeDetailsVC(service: walletService) + let controller = screensFactory.makeDetailsVC(service: walletService) controller.transaction = transaction - + if transaction.senderAddress.caseInsensitiveCompare(address) == .orderedSame { controller.senderName = String.adamant.transactionDetails.yourAddress } - + if transaction.recipientAddress.caseInsensitiveCompare(address) == .orderedSame { controller.recipientName = String.adamant.transactionDetails.yourAddress } - + navigationController?.pushViewController(controller, animated: true) } } private class LoadMoreDashTransactionsProcedure: Procedure { let service: DashWalletService - + private(set) var result: DashTransactionsPointer? - + init(service: DashWalletService) { self.service = service - + super.init() - + log.severity = .warning } - + override func execute() { service.getNextTransaction { result in switch result { case .success(let result): self.result = result self.finish() - + case .failure(let error): self.result = nil self.finish(with: error) diff --git a/Adamant/Modules/Wallets/Dash/DashTransferViewController.swift b/Adamant/Modules/Wallets/Dash/DashTransferViewController.swift index 5a1d1e717..140d58a32 100644 --- a/Adamant/Modules/Wallets/Dash/DashTransferViewController.swift +++ b/Adamant/Modules/Wallets/Dash/DashTransferViewController.swift @@ -6,17 +6,18 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UIKit -import Eureka import BitcoinKit import CommonKit +import Eureka +import UIKit extension String.adamant.transfer { - static var minAmountError: String { String.localized("TransferScene.Error.MinAmount", comment: "Transfer: Minimal transaction amount is 0.00001") + static var minAmountError: String { + String.localized("TransferScene.Error.MinAmount", comment: "Transfer: Minimal transaction amount is 0.00001") } static func pendingTxError(coin: String) -> String { let localizedString = String( - format: .localized("TransferScene.Error.Pending.Tx", comment: "Have a pending coin tx"), + format: .localized("TransferScene.Error.Pending.Tx", comment: "Have a pending coin tx"), coin ) return localizedString @@ -24,9 +25,9 @@ extension String.adamant.transfer { } final class DashTransferViewController: TransferViewControllerBase { - + // MARK: Send - + @MainActor override func sendFunds() { let comments: String @@ -35,14 +36,14 @@ final class DashTransferViewController: TransferViewControllerBase { } else { comments = "" } - + guard let service = walletCore as? DashWalletService, - let recipient = recipientAddress, - let amount = amount + let recipient = recipientAddress, + let amount = amount else { return } - + guard amount >= 0.00001 else { dialogService.showAlert( title: nil, @@ -53,13 +54,13 @@ final class DashTransferViewController: TransferViewControllerBase { ) return } - + guard let wallet = service.wallet else { return } - + dialogService.showProgress(withMessage: String.adamant.transfer.transferProcessingMessage, userInteractionEnable: false) - + Task { do { // Create transaction @@ -69,15 +70,16 @@ final class DashTransferViewController: TransferViewControllerBase { fee: transactionFee, comment: nil ) - + if await !doesNotContainSendingTx() { presentSendingError() return } - + // Send adm report if let reportRecipient = admReportRecipient, - let hash = transaction.txHash { + let hash = transaction.txHash + { try await reportTransferTo( admAddress: reportRecipient, amount: amount, @@ -85,7 +87,7 @@ final class DashTransferViewController: TransferViewControllerBase { hash: hash ) } - + do { let simpleTransaction = SimpleTransactionDetails( txId: transaction.txID, @@ -99,7 +101,7 @@ final class DashTransferViewController: TransferViewControllerBase { transactionStatus: nil, nonceRaw: nil ) - + service.coinStorage.append(simpleTransaction) try await service.sendTransaction(transaction) } catch { @@ -107,17 +109,17 @@ final class DashTransferViewController: TransferViewControllerBase { for: transaction.txId, status: .failed ) - + throw error } - + Task { await service.update() } - + dialogService.dismissProgress() dialogService.showSuccess(withMessage: String.adamant.transfer.transferSuccess) - + // Present detail VC presentDetailTransactionVC( transaction: transaction, @@ -130,7 +132,7 @@ final class DashTransferViewController: TransferViewControllerBase { } } } - + private func presentDetailTransactionVC( transaction: BitcoinKit.Transaction, comments: String, @@ -140,20 +142,20 @@ final class DashTransferViewController: TransferViewControllerBase { detailsVc.transaction = transaction detailsVc.senderName = String.adamant.transactionDetails.yourAddress detailsVc.recipientName = recipientName - + if comments.count > 0 { detailsVc.comment = comments } - + delegate?.transferViewController( self, didFinishWithTransfer: transaction, detailsViewController: detailsVc ) } - + // MARK: Overrides - + override func recipientRow() -> BaseRow { let row = TextRow { $0.tag = BaseRows.address.tag @@ -161,11 +163,11 @@ final class DashTransferViewController: TransferViewControllerBase { $0.cell.textField.keyboardType = .namePhonePad $0.cell.textField.autocorrectionType = .no $0.cell.textField.setLineBreakMode() - + $0.value = recipientAddress?.components( separatedBy: TransferViewControllerBase.invalidCharacters ).joined() - + if recipientIsReadonly { $0.disabled = true $0.cell.textField.isEnabled = false @@ -178,15 +180,15 @@ final class DashTransferViewController: TransferViewControllerBase { row.cell.textField.text = row.value?.components( separatedBy: TransferViewControllerBase.invalidCharacters ).joined() - + self?.updateToolbar(for: row) }.onCellSelection { [weak self] (cell, _) in self?.shareValue(self?.recipientAddress, from: cell) } - + return row } - + override func defaultSceneTitle() -> String? { return String.adamant.sendDash } diff --git a/Adamant/Modules/Wallets/Dash/DashWallet.swift b/Adamant/Modules/Wallets/Dash/DashWallet.swift index bb96ebbe6..e3b9ee8d1 100644 --- a/Adamant/Modules/Wallets/Dash/DashWallet.swift +++ b/Adamant/Modules/Wallets/Dash/DashWallet.swift @@ -6,9 +6,9 @@ // Copyright © 2019 Adamant. All rights reserved. // -import Foundation @preconcurrency import BitcoinKit import CommonKit +import Foundation final class DashWallet: WalletAccount, @unchecked Sendable { let unicId: String @@ -20,9 +20,9 @@ final class DashWallet: WalletAccount, @unchecked Sendable { @Atomic var minBalance: Decimal = 0.0001 @Atomic var minAmount: Decimal = 0.00002 @Atomic var isBalanceInitialized: Bool = false - + var address: String { addressEntity.stringValue } - + init( unicId: String, privateKey: PrivateKey, @@ -31,7 +31,7 @@ final class DashWallet: WalletAccount, @unchecked Sendable { self.unicId = unicId self.privateKey = privateKey self.publicKey = privateKey.publicKey() - + self.addressEntity = try addressConverter.convert( publicKey: publicKey, type: .p2pkh diff --git a/Adamant/Modules/Wallets/Dash/DashWalletFactory.swift b/Adamant/Modules/Wallets/Dash/DashWalletFactory.swift index 254644865..0506e4b40 100644 --- a/Adamant/Modules/Wallets/Dash/DashWalletFactory.swift +++ b/Adamant/Modules/Wallets/Dash/DashWalletFactory.swift @@ -6,16 +6,16 @@ // Copyright © 2019 Adamant. All rights reserved. // -import Swinject import CommonKit +import Swinject import UIKit struct DashWalletFactory: WalletFactory { typealias Service = WalletService - + let typeSymbol: String = DashWalletService.richMessageType let assembler: Assembler - + func makeWalletVC(service: Service, screensFactory: ScreensFactory) -> WalletViewController { DashWalletViewController( dialogService: assembler.resolve(DialogService.self)!, @@ -26,7 +26,7 @@ struct DashWalletFactory: WalletFactory { service: service ) } - + func makeTransferListVC(service: Service, screensFactory: ScreensFactory) -> UIViewController { DashTransactionsViewController( walletService: service, @@ -35,7 +35,7 @@ struct DashWalletFactory: WalletFactory { screensFactory: screensFactory ) } - + func makeTransferVC(service: Service, screensFactory: ScreensFactory) -> TransferViewControllerBase { DashTransferViewController( chatsProvider: assembler.resolve(ChatsProvider.self)!, @@ -45,26 +45,26 @@ struct DashWalletFactory: WalletFactory { screensFactory: screensFactory, currencyInfoService: assembler.resolve(InfoServiceProtocol.self)!, increaseFeeService: assembler.resolve(IncreaseFeeService.self)!, - vibroService: assembler.resolve(VibroService.self)!, + vibroService: assembler.resolve(VibroService.self)!, walletService: service, reachabilityMonitor: assembler.resolve(ReachabilityMonitor.self)!, apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)! ) } - + func makeDetailsVC(service: Service, transaction: RichMessageTransaction) -> UIViewController? { guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash), let address = assembler.resolve(AccountService.self)?.account?.address else { return nil } - + let comment: String? if let raw = transaction.getRichValue(for: RichContentKeys.transfer.comments), raw.count > 0 { comment = raw } else { comment = nil } - + return makeTransactionDetailsVC( hash: hash, senderId: transaction.senderId, @@ -79,14 +79,14 @@ struct DashWalletFactory: WalletFactory { service: service ) } - + func makeDetailsVC(service: Service) -> TransactionDetailsViewControllerBase { makeTransactionDetailsVC(service: service) } } -private extension DashWalletFactory { - func makeTransactionDetailsVC( +extension DashWalletFactory { + fileprivate func makeTransactionDetailsVC( hash: String, senderId: String?, recipientId: String?, @@ -100,15 +100,16 @@ private extension DashWalletFactory { service: Service ) -> UIViewController { let vc = makeTransactionDetailsVC(service: service) - + let amount: Decimal if let amountRaw = richTransaction.getRichValue(for: RichContentKeys.transfer.amount), - let decimal = Decimal(string: amountRaw) { + let decimal = Decimal(string: amountRaw) + { amount = decimal } else { amount = 0 } - + var dashTransaction = transaction?.asBtcTransaction(DashTransaction.self, for: address) if let blockId = blockId { dashTransaction = transaction?.asBtcTransaction(DashTransaction.self, for: address, blockId: blockId) @@ -126,7 +127,7 @@ private extension DashWalletFactory { transactionStatus: nil, nonceRaw: nil ) - + vc.senderId = senderId vc.recipientId = recipientId vc.comment = comment @@ -134,8 +135,8 @@ private extension DashWalletFactory { vc.richTransaction = richTransaction return vc } - - func makeTransactionDetailsVC(service: Service) -> DashTransactionDetailsViewController { + + fileprivate func makeTransactionDetailsVC(service: Service) -> DashTransactionDetailsViewController { DashTransactionDetailsViewController( dialogService: assembler.resolve(DialogService.self)!, currencyInfo: assembler.resolve(InfoServiceProtocol.self)!, diff --git a/Adamant/Modules/Wallets/Dash/DashWalletService+DynamicConstants.swift b/Adamant/Modules/Wallets/Dash/DashWalletService+DynamicConstants.swift deleted file mode 100644 index 7bc6b8f5d..000000000 --- a/Adamant/Modules/Wallets/Dash/DashWalletService+DynamicConstants.swift +++ /dev/null @@ -1,88 +0,0 @@ -import Foundation -import BigInt -import CommonKit - -extension DashWalletService { - // MARK: - Constants - static let fixedFee: Decimal = 0.0001 - static let currencySymbol = "DASH" - static let currencyExponent: Int = -8 - static let qqPrefix: String = "dash" - - static let healthCheckParameters = CoinHealthCheckParameters( - normalUpdateInterval: 210, - crucialUpdateInterval: 30, - onScreenUpdateInterval: 10, - threshold: 3, - normalServiceUpdateInterval: 210, - crucialServiceUpdateInterval: 30, - onScreenServiceUpdateInterval: 10 - ) - - static var newPendingInterval: Int { - 5000 - } - - static var oldPendingInterval: Int { - 3000 - } - - static var registeredInterval: Int { - 30000 - } - - static var newPendingAttempts: Int { - 20 - } - - static var oldPendingAttempts: Int { - 4 - } - - var tokenName: String { - "Dash" - } - - var consistencyMaxTime: Double { - 800 - } - - var minBalance: Decimal { - 0.0001 - } - - var minAmount: Decimal { - 2.0e-05 - } - - var defaultVisibility: Bool { - true - } - - var defaultOrdinalLevel: Int? { - 70 - } - - static var minNodeVersion: String? { - nil - } - - var transferDecimals: Int { - 8 - } - - static let explorerTx = "https://dashblockexplorer.com/tx/" - static let explorerAddress = "https://dashblockexplorer.com/address/" - static var nodes: [Node] { - [ - Node.makeDefaultNode(url: URL(string: "https://dashnode1.adamant.im")!, altUrl: URL(string: "http://45.85.147.224:44099")), -Node.makeDefaultNode(url: URL(string: "https://dashnode2.adamant.im")!, altUrl: URL(string: "http://207.180.210.95:44099")), - ] - } - - static var serviceNodes: [Node] { - [ - - ] - } -} diff --git a/Adamant/Modules/Wallets/Dash/DashWalletService+RichMessageProvider.swift b/Adamant/Modules/Wallets/Dash/DashWalletService+RichMessageProvider.swift index 49a8d399d..8365a5468 100644 --- a/Adamant/Modules/Wallets/Dash/DashWalletService+RichMessageProvider.swift +++ b/Adamant/Modules/Wallets/Dash/DashWalletService+RichMessageProvider.swift @@ -6,59 +6,59 @@ // Copyright © 2019 Adamant. All rights reserved. // +import CommonKit import Foundation import MessageKit import UIKit -import CommonKit extension DashWalletService { var newPendingInterval: TimeInterval { .init(milliseconds: type(of: self).newPendingInterval) } - + var oldPendingInterval: TimeInterval { .init(milliseconds: type(of: self).oldPendingInterval) } - + var registeredInterval: TimeInterval { .init(milliseconds: type(of: self).registeredInterval) } - + var newPendingAttempts: Int { type(of: self).newPendingAttempts } - + var oldPendingAttempts: Int { type(of: self).oldPendingAttempts } - + var dynamicRichMessageType: String { return type(of: self).richMessageType } - + // MARK: Short description - + func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { let amount: String - + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount) else { return NSAttributedString(string: "⬅️ \(DashWalletService.currencySymbol)") } - + if let decimal = Decimal(string: raw) { amount = AdamantBalanceFormat.full.format(decimal) } else { amount = raw } - + let string: String if transaction.isOutgoing { string = "⬅️ \(amount) \(DashWalletService.currencySymbol)" } else { string = "➡️ \(amount) \(DashWalletService.currencySymbol)" } - + return NSAttributedString(string: string) } } diff --git a/Adamant/Modules/Wallets/Dash/DashWalletService+RichMessageProviderWithStatusCheck.swift b/Adamant/Modules/Wallets/Dash/DashWalletService+RichMessageProviderWithStatusCheck.swift index c4e056235..75b1e6b17 100644 --- a/Adamant/Modules/Wallets/Dash/DashWalletService+RichMessageProviderWithStatusCheck.swift +++ b/Adamant/Modules/Wallets/Dash/DashWalletService+RichMessageProviderWithStatusCheck.swift @@ -6,31 +6,31 @@ // Copyright © 2019 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation extension DashWalletService { func statusInfoFor(transaction: CoinTransaction) async -> TransactionStatusInfo { let hash: String? - + if let transaction = transaction as? RichMessageTransaction { hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) } else { hash = transaction.txId } - + guard let hash = hash else { return .init(sentDate: nil, status: .inconsistent(.wrongTxHash)) } - + let dashTransaction: BTCRawTransaction - + do { dashTransaction = try await getTransaction(by: hash, waitsForConnectivity: true) } catch { return .init(error: error) } - + return await .init( sentDate: dashTransaction.date, status: getStatus(dashTransaction: dashTransaction, transaction: transaction) @@ -38,72 +38,74 @@ extension DashWalletService { } } -private extension DashWalletService { - func getStatus( +extension DashWalletService { + fileprivate func getStatus( dashTransaction: BTCRawTransaction, transaction: CoinTransaction ) async -> TransactionStatus { // MARK: Check confirmations - - guard let confirmations = dashTransaction.confirmations, let dashDate = dashTransaction.date, (confirmations > 0 || dashDate.timeIntervalSinceNow > -60 * 15) else { + + guard let confirmations = dashTransaction.confirmations, let dashDate = dashTransaction.date, + confirmations > 0 || dashDate.timeIntervalSinceNow > -60 * 15 + else { return .registered } - + // MARK: Check amount & address guard let reportedValue = reportedValue(for: transaction) else { return .inconsistent(.wrongAmount) } - - let min = reportedValue - reportedValue*0.005 - let max = reportedValue + reportedValue*0.005 - + + let min = reportedValue - reportedValue * 0.005 + let max = reportedValue + reportedValue * 0.005 + guard let walletAddress = dashWallet?.address else { return .inconsistent(.unknown) } - + let readableTransaction = dashTransaction.asBtcTransaction(DashTransaction.self, for: walletAddress) - + var realSenderAddress = readableTransaction.senderAddress var realRecipientAddress = readableTransaction.recipientAddress - + if transaction is RichMessageTransaction { guard let senderAddress = try? await getWalletAddress(byAdamantAddress: transaction.senderAddress) else { return .inconsistent(.senderCryptoAddressUnavailable(tokenSymbol)) } - + guard let recipientAddress = try? await getWalletAddress(byAdamantAddress: transaction.recipientAddress) else { return .inconsistent(.recipientCryptoAddressUnavailable(tokenSymbol)) } - + realSenderAddress = senderAddress realRecipientAddress = recipientAddress } - + guard readableTransaction.senderAddress.caseInsensitiveCompare(realSenderAddress) == .orderedSame else { return .inconsistent(.senderCryptoAddressMismatch(tokenSymbol)) } - + guard readableTransaction.recipientAddress.caseInsensitiveCompare(realRecipientAddress) == .orderedSame else { return .inconsistent(.recipientCryptoAddressMismatch(tokenSymbol)) } - + var result: TransactionStatus = .inconsistent(.wrongAmount) if transaction.isOutgoing { guard readableTransaction.senderAddress.caseInsensitiveCompare(walletAddress) == .orderedSame else { return .inconsistent(.senderCryptoAddressMismatch(tokenSymbol)) } - + var totalIncome: Decimal = 0 for output in dashTransaction.outputs { guard !output.addresses.contains(walletAddress) else { continue } - + totalIncome += output.value } - + if (min...max).contains(totalIncome) { result = .success } @@ -111,37 +113,37 @@ private extension DashWalletService { guard readableTransaction.recipientAddress.caseInsensitiveCompare(walletAddress) == .orderedSame else { return .inconsistent(.recipientCryptoAddressMismatch(tokenSymbol)) } - + var totalOutcome: Decimal = 0 for output in dashTransaction.outputs { guard output.addresses.contains(walletAddress) else { continue } - + totalOutcome += output.value } - + if (min...max).contains(totalOutcome) { result = .success } } - + return result } - - func reportedValue(for transaction: CoinTransaction) -> Decimal? { + + fileprivate func reportedValue(for transaction: CoinTransaction) -> Decimal? { guard let transaction = transaction as? RichMessageTransaction else { return transaction.amountValue } - + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), let reportedValue = AdamantBalanceFormat.deserializeBalance(from: raw) else { return nil } - + return reportedValue } } diff --git a/Adamant/Modules/Wallets/Dash/DashWalletService+Send.swift b/Adamant/Modules/Wallets/Dash/DashWalletService+Send.swift index 9d59fe872..3277a6749 100644 --- a/Adamant/Modules/Wallets/Dash/DashWalletService+Send.swift +++ b/Adamant/Modules/Wallets/Dash/DashWalletService+Send.swift @@ -6,13 +6,13 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UIKit -import BitcoinKit import Alamofire +import BitcoinKit +import UIKit extension DashWalletService: WalletServiceTwoStepSend { typealias T = BitcoinKit.Transaction - + // MARK: Create & Send func create(recipient: String, amount: Decimal) async throws -> BitcoinKit.Transaction { guard let lastTransaction = self.lastTransactionId else { @@ -23,15 +23,15 @@ extension DashWalletService: WalletServiceTwoStepSend { comment: nil ) } - + let transaction = try await getTransaction(by: lastTransaction, waitsForConnectivity: false) - + guard let confirmations = transaction.confirmations, - confirmations >= 1 + confirmations >= 1 else { throw WalletServiceError.remoteServiceError(message: "WAIT_FOR_COMPLETION", error: nil) } - + return try await createTransaction( recipient: recipient, amount: amount, @@ -39,7 +39,7 @@ extension DashWalletService: WalletServiceTwoStepSend { comment: nil ) } - + func createTransaction( recipient: String, amount: Decimal, @@ -50,41 +50,42 @@ extension DashWalletService: WalletServiceTwoStepSend { guard let wallet = self.dashWallet else { throw WalletServiceError.notLogged } - + let key = wallet.privateKey - + guard let toAddress = try? addressConverter.convert(address: recipient) else { throw WalletServiceError.accountNotFound } - + let rawAmount = NSDecimalNumber(decimal: amount * DashWalletService.multiplier).uint64Value let fee = NSDecimalNumber(decimal: fee * DashWalletService.multiplier).uint64Value - + // MARK: 2. Search for unspent transactions let utxos = try await getUnspentTransactions() - + // MARK: 3. Check if we have enought money let totalAmount: UInt64 = UInt64(utxos.reduce(0) { $0 + $1.output.value }) - guard totalAmount >= rawAmount + fee else { // This shit can crash BitcoinKit + guard totalAmount >= rawAmount + fee else { // This shit can crash BitcoinKit throw WalletServiceError.notEnoughMoney } - + // MARK: 4. Create local transaction - let transaction = BitcoinKit.Transaction.createNewTransaction( + let transaction = transactionFactory.createTransaction( toAddress: toAddress, amount: rawAmount, fee: fee, changeAddress: wallet.addressEntity, utxos: utxos, + lockTime: 0, keys: [key] ) return transaction } - + func sendTransaction(_ transaction: BitcoinKit.Transaction) async throws { let txHex = transaction.serialized().hex - + let response: BTCRPCServerResponce = try await dashApiService.request( waitsForConnectivity: false ) { core, origin in @@ -96,7 +97,7 @@ extension DashWalletService: WalletServiceTwoStepSend { encoding: .json ) }.get() - + if response.result != nil { lastTransactionId = transaction.txID } else if let error = response.error?.message { diff --git a/Adamant/Modules/Wallets/Dash/DashWalletService+Transactions.swift b/Adamant/Modules/Wallets/Dash/DashWalletService+Transactions.swift index d92d6dbe1..d28bcb901 100644 --- a/Adamant/Modules/Wallets/Dash/DashWalletService+Transactions.swift +++ b/Adamant/Modules/Wallets/Dash/DashWalletService+Transactions.swift @@ -6,9 +6,9 @@ // Copyright © 2021 Adamant. All rights reserved. // -import Foundation -import CommonKit import BitcoinKit +import CommonKit +import Foundation struct DashTransactionsPointer { let total: Int @@ -47,15 +47,15 @@ extension DashWalletService { params: [.string(hash), .bool(true)] ) ) - + guard case let .success(data) = response else { return .failure(.accountNotFound) } - + let tx: BTCRawTransaction? = data.serialize() return .success(tx) }.get() - + if let transaction = result { return transaction } else { @@ -67,43 +67,43 @@ extension DashWalletService { guard let address = wallet?.address else { throw ApiServiceError.notLogged } - + let params: [RpcRequest] = hashes.compactMap { .init( method: DashApiComand.rawTransactionMethod, params: [.string($0), .bool(true)] ) } - + let result: [BTCRawTransaction] = try await dashApiService.request(waitsForConnectivity: false) { core, origin in let response = await core.sendRequestRPC( origin: origin, path: .empty, requests: params ) - + guard case let .success(data) = response else { return .failure(.accountNotFound) } - + let res: [BTCRawTransaction] = data.compactMap { let tx: BTCRawTransaction? = $0.serialize() return tx } - + return .success(res) }.get() - + return result.compactMap { $0.asBtcTransaction(DashTransaction.self, for: address) } } - + func getBlockId(by hash: String?) async throws -> String { guard let hash = hash else { throw WalletServiceError.internalError(message: "Hash is empty", error: nil) } - + let result: BTCRPCServerResponce = try await dashApiService.request(waitsForConnectivity: false) { core, origin in await core.sendRequestJsonResponse( origin: origin, @@ -113,7 +113,7 @@ extension DashWalletService { encoding: .json ) }.get() - + if let block = result.result { return String(block.height) } else { @@ -125,9 +125,10 @@ extension DashWalletService { guard let wallet = dashWallet else { throw WalletServiceError.internalError(message: "DASH Wallet not found", error: nil) } - + let response: BTCRPCServerResponce<[DashUnspentTransaction]> = try await dashApiService.request(waitsForConnectivity: false) { - core, origin in + core, + origin in await core.sendRequestJsonResponse( origin: origin, path: .empty, @@ -136,7 +137,7 @@ extension DashWalletService { encoding: .json ) }.get() - + if let result = response.result { return result.map { $0.asUnspentTransaction(lockScript: wallet.addressEntity.lockingScript) @@ -155,9 +156,9 @@ extension DashWalletService { // MARK: - Handlers -private extension DashWalletService { +extension DashWalletService { - func handleTransactionsResponse( + fileprivate func handleTransactionsResponse( _ response: ApiServiceResult<[String]>, _ completion: @escaping @Sendable (ApiServiceResult) -> Void ) { @@ -170,7 +171,11 @@ private extension DashWalletService { } } - func handleTransactionResponse(id: String, _ response: ApiServiceResult, _ completion: @escaping (ApiServiceResult) -> Void) { + fileprivate func handleTransactionResponse( + id: String, + _ response: ApiServiceResult, + _ completion: @escaping (ApiServiceResult) -> Void + ) { guard let address = wallet?.address else { completion(.failure(.notLogged)) return @@ -195,7 +200,8 @@ private extension DashWalletService { extension DashWalletService { func requestTransactionsIds(for address: String) async throws -> [String] { let response: BTCRPCServerResponce<[String]> = try await dashApiService.request(waitsForConnectivity: false) { - core, origin in + core, + origin in await core.sendRequestJsonResponse( origin: origin, path: .empty, @@ -204,11 +210,11 @@ extension DashWalletService { encoding: .json ) }.get() - + if let result = response.result { return result } - + throw WalletServiceError.internalError(message: "DASH Wallet: not a valid response", error: nil) } diff --git a/Adamant/Modules/Wallets/Dash/DashWalletService.swift b/Adamant/Modules/Wallets/Dash/DashWalletService.swift index 560992f08..9bd8a7084 100644 --- a/Adamant/Modules/Wallets/Dash/DashWalletService.swift +++ b/Adamant/Modules/Wallets/Dash/DashWalletService.swift @@ -6,12 +6,13 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UIKit -import Swinject import Alamofire import BitcoinKit import Combine import CommonKit +import Swinject +import UIKit +import Web3Core struct DashApiComand { static let networkInfoMethod: String = "getnetworkinfo" @@ -19,133 +20,119 @@ struct DashApiComand { static let rawTransactionMethod: String = "getrawtransaction" } -final class DashWalletService: WalletCoreProtocol, @unchecked Sendable { - +final class DashWalletService: WalletCoreProtocol, WalletStaticCoreProtocol, @unchecked Sendable { + static let currencySymbol = "DASH" var tokenSymbol: String { return type(of: self).currencySymbol } - + var tokenLogo: UIImage { return type(of: self).currencyLogo } - + static var tokenNetworkSymbol: String { return "DASH" } - + var tokenContract: String { return "" } - - var tokenUnicID: String { + + var tokenUniqueID: String { Self.tokenNetworkSymbol + tokenSymbol } - + var richMessageType: String { return Self.richMessageType - } + } var qqPrefix: String { return Self.qqPrefix } - + var nodeGroups: [NodeGroup] { [.dash] } - + var explorerAddress: String { Self.explorerAddress } - + var wallet: WalletAccount? { return dashWallet } - + // MARK: RichMessageProvider properties static let richMessageType = "dash_transaction" - + // MARK: - Dependencies var apiService: AdamantApiServiceProtocol! - var dashApiService: DashApiService! + var dashApiService: DashApiServiceProtocol! var accountService: AccountService! - var securedStore: SecuredStore! + var lastTransactionStorage: DashLastTransactionStorageProtocol! + var transactionFactory: BitcoinKitTransactionFactoryProtocol! var dialogService: DialogService! var addressConverter: AddressConverter! var coreDataStack: CoreDataStack! var vibroService: VibroService! - + // MARK: - Constants static let currencyLogo = UIImage.asset(named: "dash_wallet") ?? .init() static let multiplier = Decimal(sign: .plus, exponent: 8, significand: 1) static let chunkSize = 20 - + var transactionFee: Decimal { return DashWalletService.fixedFee } - + @Atomic private(set) var isWarningGasPrice = false - + static let kvsAddress = "dash:address" - + @Atomic var transatrionsIds = [String]() - + var lastTransactionId: String? { get { - guard - let hash: String = self.securedStore.get("lastDashTransactionId"), - let timestampString: String = self.securedStore.get("lastDashTransactionTime"), - let timestamp = Double(string: timestampString) - else { return nil } - - let date = Date(timeIntervalSince1970: TimeInterval(timestamp)) - let timeAgo = -1 * date.timeIntervalSinceNow - - if timeAgo > 10 * 60 { // 10m waiting for transaction complete - self.securedStore.remove("lastDashTransactionTime") - self.securedStore.remove("lastDashTransactionId") - return nil - } else { - return hash - } + lastTransactionStorage.getLastTransactionId() } set { - if let value = newValue { - let timestamp = Date().timeIntervalSince1970 - self.securedStore.set("\(timestamp)", for: "lastDashTransactionTime") - self.securedStore.set(value, for: "lastDashTransactionId") - } else { - self.securedStore.remove("lastDashTransactionTime") - self.securedStore.remove("lastDashTransactionId") - } + lastTransactionStorage.setLastTransactionId(newValue) } } - + @MainActor var hasEnabledNode: Bool { dashApiService.hasEnabledNode } - + @MainActor var hasEnabledNodePublisher: AnyObservable { dashApiService.hasEnabledNodePublisher } - + // MARK: - Notifications let walletUpdatedNotification = Notification.Name("adamant.dashWallet.walletUpdated") let serviceEnabledChanged = Notification.Name("adamant.dashWallet.enabledChanged") let serviceStateChanged = Notification.Name("adamant.dashWallet.stateChanged") let transactionFeeUpdated = Notification.Name("adamant.dashWallet.feeUpdated") - + + @MainActor + private let walletUpdateSender = ObservableSender() + @MainActor + var walletUpdatePublisher: AnyObservable { + walletUpdateSender.eraseToAnyPublisher() + } + // MARK: - Delayed KVS save @Atomic private var balanceObserver: NSObjectProtocol? - + // MARK: - Properties @Atomic private(set) var dashWallet: DashWallet? @Atomic private(set) var enabled = true @Atomic public var network: Network @Atomic private var cachedWalletAddress: [String: String] = [:] @Atomic private var balanceInvalidationSubscription: AnyCancellable? - + let defaultDispatchQueue = DispatchQueue(label: "im.adamant.dashWalletService", qos: .userInteractive, attributes: [.concurrent]) - + static let jsonDecoder = JSONDecoder() @Atomic private var subscriptions = Set() @@ -155,27 +142,27 @@ final class DashWalletService: WalletCoreProtocol, @unchecked Sendable { var transactionsPublisher: AnyObservable<[TransactionDetails]> { $historyTransactions.eraseToAnyPublisher() } - + var hasMoreOldTransactionsPublisher: AnyObservable { $hasMoreOldTransactions.eraseToAnyPublisher() } - + private(set) lazy var coinStorage: CoinStorageService = AdamantCoinStorageService( - coinId: tokenUnicID, + coinId: tokenUniqueID, coreDataStack: coreDataStack, blockchainType: richMessageType ) - + // MARK: - State @Atomic private(set) var state: WalletServiceState = .notInitiated - + private func setState(_ newState: WalletServiceState, silent: Bool = false) { guard newState != state else { return } - + state = newState - + if !silent { NotificationCenter.default.post( name: serviceStateChanged, @@ -184,16 +171,16 @@ final class DashWalletService: WalletCoreProtocol, @unchecked Sendable { ) } } - + init() { self.network = DashMainnet() - + self.setState(.notInitiated) - + // Notifications addObservers() } - + func addObservers() { NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedIn, object: nil) @@ -201,14 +188,14 @@ final class DashWalletService: WalletCoreProtocol, @unchecked Sendable { self?.update() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantAccountService.accountDataUpdated, object: nil) .sink { @MainActor [weak self] _ in self?.update() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedOut, object: nil) .sink { @MainActor [weak self] _ in @@ -224,7 +211,7 @@ final class DashWalletService: WalletCoreProtocol, @unchecked Sendable { } .store(in: &subscriptions) } - + func addTransactionObserver() { coinStorage.transactionsPublisher .sink { [weak self] transactions in @@ -232,50 +219,54 @@ final class DashWalletService: WalletCoreProtocol, @unchecked Sendable { } .store(in: &subscriptions) } - + func update() { Task { await update() } } - + @MainActor func update() async { guard let wallet = dashWallet else { return } - + switch state { case .notInitiated, .updating, .initiationFailed: return - + case .upToDate: break } - + setState(.updating) - + if let balance = try? await getBalance() { if wallet.balance < balance, wallet.isBalanceInitialized { vibroService.applyVibration(.success) } - + wallet.balance = balance markBalanceAsFresh(wallet) - - NotificationCenter.default.post( - name: walletUpdatedNotification, - object: self, - userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet] - ) + + walletUpdateSender.send() + } else { + wallet.isBalanceInitialized = false } - + + NotificationCenter.default.post( + name: walletUpdatedNotification, + object: self, + userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet] + ) + setState(.upToDate) } - + func validate(address: String) -> AddressValidationResult { let address = try? addressConverter.convert(address: address) - + switch address?.scriptType { case .p2pk, .p2pkh, .p2sh: return .valid @@ -283,20 +274,22 @@ final class DashWalletService: WalletCoreProtocol, @unchecked Sendable { return .invalid(description: nil) } } - + private func markBalanceAsFresh(_ wallet: DashWallet) { wallet.isBalanceInitialized = true - + balanceInvalidationSubscription = Task { [weak self] in try await Task.sleep(interval: Self.balanceLifetime, pauseInBackground: true) guard let self else { return } wallet.isBalanceInitialized = false - + NotificationCenter.default.post( name: walletUpdatedNotification, object: self, userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet] ) + + await self.walletUpdateSender.send() }.eraseToAnyCancellable() } } @@ -307,43 +300,48 @@ extension DashWalletService { setState(.initiationFailed(reason: reason)) dashWallet = nil } - + @MainActor - func initWallet(withPassphrase passphrase: String) async throws -> WalletAccount { + func initWallet(withPassphrase passphrase: String, withPassword password: String) async throws -> WalletAccount { guard let adamant = accountService.account else { throw WalletServiceError.notLogged } - + setState(.notInitiated) - + if enabled { enabled = false NotificationCenter.default.post(name: serviceEnabledChanged, object: self) } - - let privateKeyData = passphrase.data(using: .utf8)!.sha256() + + guard let privateKeyData = makeBinarySeed(withMnemonicSentence: passphrase, withSalt: password) else { + throw WalletServiceError.internalError(message: "DASH Wallet: failed to generate private key", error: nil) + } + let privateKey = PrivateKey(data: privateKeyData, network: self.network, isPublicKeyCompressed: true) - + let eWallet = try DashWallet( - unicId: tokenUnicID, + unicId: tokenUniqueID, privateKey: privateKey, addressConverter: addressConverter ) - + self.dashWallet = eWallet let kvsAddressModel = makeKVSAddressModel(wallet: eWallet) - + NotificationCenter.default.post( name: walletUpdatedNotification, object: self, userInfo: [AdamantUserInfoKey.WalletService.wallet: eWallet] ) - + + self.walletUpdateSender.send() + if !self.enabled { self.enabled = true NotificationCenter.default.post(name: self.serviceEnabledChanged, object: self) } - + // MARK: 4. Save address into KVS do { let address = try await getWalletAddress(byAdamantAddress: adamant.address) @@ -353,9 +351,9 @@ extension DashWalletService { service.kvsSaveCompletionRecursion(kvsAddressModel, result: result) } } - + service.setState(.upToDate) - + Task { service.update() } @@ -367,26 +365,34 @@ extension DashWalletService { /// The ADM Wallet is not initialized. Check the balance of the current wallet /// and save the wallet address to kvs when dropshipping ADM service.setState(.upToDate) - + Task { await service.update() } - + if let kvsAddressModel { service.save(kvsAddressModel) { result in service.kvsSaveCompletionRecursion(kvsAddressModel, result: result) } } - + service.setState(.upToDate) return eWallet - + default: service.setState(.upToDate) throw error } } } + + private func makeBinarySeed(withMnemonicSentence passphrase: String, withSalt salt: String) -> Data? { + guard !salt.isEmpty else { + return passphrase.data(using: .utf8)!.sha256() + } + + return BIP39.seedFromMmemonics(passphrase, password: salt, language: .english) + } } // MARK: - Dependencies @@ -395,14 +401,15 @@ extension DashWalletService: SwinjectDependentService { func injectDependencies(from container: Container) { accountService = container.resolve(AccountService.self) apiService = container.resolve(AdamantApiServiceProtocol.self) - securedStore = container.resolve(SecuredStore.self) + lastTransactionStorage = container.resolve(DashLastTransactionStorageProtocol.self) + transactionFactory = container.resolve(BitcoinKitTransactionFactoryProtocol.self) dialogService = container.resolve(DialogService.self) addressConverter = container.resolve(AddressConverterFactory.self)? .make(network: network) dashApiService = container.resolve(DashApiService.self) vibroService = container.resolve(VibroService.self) coreDataStack = container.resolve(CoreDataStack.self) - + addTransactionObserver() } } @@ -413,10 +420,10 @@ extension DashWalletService { guard let address = dashWallet?.address else { throw WalletServiceError.walletNotInitiated } - + return try await getBalance(address: address) } - + func getBalance(address: String) async throws -> Decimal { let data: Data = try await dashApiService.request(waitsForConnectivity: false) { core, origin in await core.sendRequest( @@ -427,21 +434,22 @@ extension DashWalletService { encoding: .json ) }.get() - - let object = try? JSONSerialization.jsonObject( - with: data, - options: [] - ) as? [String: Any] + + let object = + try? JSONSerialization.jsonObject( + with: data, + options: [] + ) as? [String: Any] guard let object = object else { throw WalletServiceError.remoteServiceError( message: "DASH Wallet: not valid response" ) } - + let result = object["result"] as? [String: Any] let error = object["error"] - + if error is NSNull, let result = result, let raw = result["balance"] as? Int64 { let balance = Decimal(raw) / DashWalletService.multiplier return balance @@ -454,18 +462,18 @@ extension DashWalletService { if let address = cachedWalletAddress[address], !address.isEmpty { return address } - + if let address = cachedWalletAddress[address], !address.isEmpty { return address } - + do { let result = try await apiService.get(key: DashWalletService.kvsAddress, sender: address).get() - + guard let result = result else { throw WalletServiceError.walletNotInitiated } - + cachedWalletAddress[address] = result return result } catch _ as ApiServiceError { @@ -474,46 +482,47 @@ extension DashWalletService { ) } } - + func loadTransactions(offset: Int, limit: Int) async throws -> Int { let trs = try await getTransactionsHistory(offset: offset, limit: limit) - + guard trs.count > 0 else { hasMoreOldTransactions = false return .zero } - + coinStorage.append(trs) - + return trs.count } - + func getTransactionsHistory(offset: Int, limit: Int) async throws -> [TransactionDetails] { guard let address = wallet?.address else { throw WalletServiceError.accountNotFound } - + let allTransactionsIds = try await requestTransactionsIds(for: address).reversed() - + let availableToLoad = allTransactionsIds.count - offset - - let maxPerRequest = availableToLoad > limit - ? limit - : availableToLoad - + + let maxPerRequest = + availableToLoad > limit + ? limit + : availableToLoad + let startIndex = allTransactionsIds.index(allTransactionsIds.startIndex, offsetBy: offset) let endIndex = allTransactionsIds.index(startIndex, offsetBy: maxPerRequest) let ids = Array(allTransactionsIds[startIndex.. [TransactionDetails] { historyTransactions } - + func updateStatus(for id: String, status: TransactionStatus?) { coinStorage.updateStatus(for: id, status: status) } @@ -537,8 +546,7 @@ extension DashWalletService { } Task { @Sendable in - let result = await apiService.store(model) - + let result = await apiService.store(model, date: .now) switch result { case .success: completion(.success) @@ -548,7 +556,7 @@ extension DashWalletService { } } } - + /// New accounts doesn't have enought money to save KVS. We need to wait for balance update, and then - retry save private func kvsSaveCompletionRecursion(_ model: KVSValueModel, result: WalletServiceSimpleResult) { if let observer = balanceObserver { @@ -564,7 +572,11 @@ extension DashWalletService { switch error { case .notEnoughMoney: // Possibly new account, we need to wait for dropship // Register observer - let observer = NotificationCenter.default.addObserver(forName: NSNotification.Name.AdamantAccountService.accountDataUpdated, object: nil, queue: nil) { [weak self] _ in + let observer = NotificationCenter.default.addObserver( + forName: NSNotification.Name.AdamantAccountService.accountDataUpdated, + object: nil, + queue: nil + ) { [weak self] _ in guard let balance = self?.accountService.account?.balance, balance > AdamantApiService.KvsFee else { return } @@ -582,10 +594,10 @@ extension DashWalletService { } } } - + private func makeKVSAddressModel(wallet: WalletAccount) -> KVSValueModel? { guard let keypair = accountService.keypair else { return nil } - + return .init( key: Self.kvsAddress, value: wallet.address, @@ -594,25 +606,34 @@ extension DashWalletService { } } +#if DEBUG + extension DashWalletService { + @available(*, deprecated, message: "For testing purposes only") + func setWalletForTests(_ wallet: DashWallet?) { + self.dashWallet = wallet + } + } +#endif + // MARK: - PrivateKey generator extension DashWalletService: PrivateKeyGenerator { var rowTitle: String { return "Dash" } - + var rowImage: UIImage? { return .asset(named: "dash_wallet_row") } - + var keyFormat: KeyFormat { .WIF } - + func generatePrivateKeyFor(passphrase: String) -> String? { guard AdamantUtilities.validateAdamantPassphrase(passphrase: passphrase), let privateKeyData = passphrase.data(using: .utf8)?.sha256() else { return nil } - + let privateKey = PrivateKey(data: privateKeyData, network: self.network, isPublicKeyCompressed: true) - + return privateKey.toWIF() } } diff --git a/Adamant/Modules/Wallets/Dash/DashWalletViewController.swift b/Adamant/Modules/Wallets/Dash/DashWalletViewController.swift index 552b5f216..bc898a821 100644 --- a/Adamant/Modules/Wallets/Dash/DashWalletViewController.swift +++ b/Adamant/Modules/Wallets/Dash/DashWalletViewController.swift @@ -6,15 +6,15 @@ // Copyright © 2019 Adamant. All rights reserved. // +import CommonKit import Foundation import UIKit -import CommonKit extension String.adamant { static var dash: String { String.localized("AccountTab.Wallets.dash_wallet", comment: "Account tab: Dash wallet") } - + static var sendDash: String { String.localized("AccountTab.Row.SendDash", comment: "Account tab: 'Send Dash tokens' button") } @@ -24,11 +24,11 @@ final class DashWalletViewController: WalletViewControllerBase { override func sendRowLocalizedLabel() -> NSAttributedString { return NSAttributedString(string: String.adamant.sendDash) } - + override func encodeForQr(address: String) -> String? { return "dash:\(address)" } - + override func setTitle() { walletTitleLabel.text = String.adamant.dash } diff --git a/Adamant/Modules/Wallets/Doge/DTO/DogeNodeInfo.swift b/Adamant/Modules/Wallets/Doge/DTO/DogeNodeInfo.swift index 9995df453..775f5c657 100644 --- a/Adamant/Modules/Wallets/Doge/DTO/DogeNodeInfo.swift +++ b/Adamant/Modules/Wallets/Doge/DTO/DogeNodeInfo.swift @@ -14,7 +14,7 @@ struct DogeNodeInfoDTO: Decodable { let protocolversion: Int let blocks: Int } - + let info: Info } diff --git a/Adamant/Modules/Wallets/Doge/DogeApiService.swift b/Adamant/Modules/Wallets/Doge/DogeApiService.swift index 0ad51e300..b6d81f011 100644 --- a/Adamant/Modules/Wallets/Doge/DogeApiService.swift +++ b/Adamant/Modules/Wallets/Doge/DogeApiService.swift @@ -15,7 +15,7 @@ final class DogeApiCore: BlockchainHealthCheckableService, Sendable { init(apiCore: APICoreProtocol) { self.apiCore = apiCore } - + func request( origin: NodeOrigin, _ request: @Sendable @escaping (APICoreProtocol, NodeOrigin) async -> ApiServiceResult @@ -25,14 +25,14 @@ final class DogeApiCore: BlockchainHealthCheckableService, Sendable { func getStatusInfo(origin: NodeOrigin) async -> WalletServiceResult { let startTimestamp = Date.now.timeIntervalSince1970 - + let response: WalletServiceResult = await request(origin: origin) { core, origin in await core.sendRequestJsonResponse( origin: origin, path: DogeApiCommands.getInfo() ) } - + return response.map { data in return .init( ping: Date.now.timeIntervalSince1970 - startTimestamp, @@ -45,21 +45,25 @@ final class DogeApiCore: BlockchainHealthCheckableService, Sendable { } } -final class DogeApiService: ApiServiceProtocol { - let api: BlockchainHealthCheckWrapper - +final class DogeApiService: DogeApiServiceProtocol { + var api: DogeInternalApiProtocol { + _api + } + + let _api: BlockchainHealthCheckWrapper + @MainActor - var nodesInfoPublisher: AnyObservable { api.nodesInfoPublisher } - + var nodesInfoPublisher: AnyObservable { _api.nodesInfoPublisher } + @MainActor - var nodesInfo: NodesListInfo { api.nodesInfo } - - func healthCheck() { api.healthCheck() } - + var nodesInfo: NodesListInfo { _api.nodesInfo } + + func healthCheck() { _api.healthCheck() } + init(api: BlockchainHealthCheckWrapper) { - self.api = api + self._api = api } - + func request( waitsForConnectivity: Bool, _ request: @Sendable @escaping (APICoreProtocol, NodeOrigin) async -> ApiServiceResult @@ -68,10 +72,12 @@ final class DogeApiService: ApiServiceProtocol { await core.request(origin: origin, request) } } - + func getStatusInfo() async -> WalletServiceResult { await api.request(waitsForConnectivity: false) { core, origin in await core.getStatusInfo(origin: origin) } } } + +extension BlockchainHealthCheckWrapper: DogeInternalApiProtocol where Service == DogeApiCore {} diff --git a/Adamant/Modules/Wallets/Doge/DogeApiServiceProtocol.swift b/Adamant/Modules/Wallets/Doge/DogeApiServiceProtocol.swift new file mode 100644 index 000000000..b075ee9f4 --- /dev/null +++ b/Adamant/Modules/Wallets/Doge/DogeApiServiceProtocol.swift @@ -0,0 +1,22 @@ +// +// DogeApiServiceProtocol.swift +// Adamant +// +// Created by Christian Benua on 17.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import CommonKit +import Foundation + +protocol DogeApiServiceProtocol: ApiServiceProtocol { + + var api: DogeInternalApiProtocol { get } + + func request( + waitsForConnectivity: Bool, + _ request: @Sendable @escaping (APICoreProtocol, NodeOrigin) async -> ApiServiceResult + ) async -> WalletServiceResult + + func getStatusInfo() async -> WalletServiceResult +} diff --git a/Adamant/Modules/Wallets/Doge/DogeInternalApiProtocol.swift b/Adamant/Modules/Wallets/Doge/DogeInternalApiProtocol.swift new file mode 100644 index 000000000..3e7096f9f --- /dev/null +++ b/Adamant/Modules/Wallets/Doge/DogeInternalApiProtocol.swift @@ -0,0 +1,16 @@ +// +// DogeInternalApiProtocol.swift +// Adamant +// +// Created by Christian Benua on 17.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import CommonKit + +protocol DogeInternalApiProtocol { + func request( + waitsForConnectivity: Bool, + _ requestAction: @Sendable (DogeApiCore, NodeOrigin) async -> WalletServiceResult + ) async -> WalletServiceResult +} diff --git a/Adamant/Modules/Wallets/Doge/DogeMainnet.swift b/Adamant/Modules/Wallets/Doge/DogeMainnet.swift index fbc2907d0..419151301 100644 --- a/Adamant/Modules/Wallets/Doge/DogeMainnet.swift +++ b/Adamant/Modules/Wallets/Doge/DogeMainnet.swift @@ -6,50 +6,50 @@ // Copyright © 2019 Adamant. All rights reserved. // -import Foundation import BitcoinKit +import Foundation final class DogeMainnet: Network { override var name: String { return "livenet" } - + override var alias: String { return "mainnet" } - + override var scheme: String { return "dogecoin" } - + override var magic: UInt32 { - return 0xc0c0c0c0 + return 0xc0c0_c0c0 } - + override var pubkeyhash: UInt8 { return 0x1e } - + override var privatekey: UInt8 { return 0x9e } - + override var scripthash: UInt8 { return 0x16 } - + override var xpubkey: UInt32 { - return 0x02facafd + return 0x02fa_cafd } - + override var xprivkey: UInt32 { - return 0x02fac398 + return 0x02fa_c398 } - + override var port: UInt32 { return 22556 } - + override var dnsSeeds: [String] { return [ "dogenode1.adamant.im" diff --git a/Adamant/Modules/Wallets/Doge/DogeTransactionDetailsViewController.swift b/Adamant/Modules/Wallets/Doge/DogeTransactionDetailsViewController.swift index 262f9743d..15c2564fd 100644 --- a/Adamant/Modules/Wallets/Doge/DogeTransactionDetailsViewController.swift +++ b/Adamant/Modules/Wallets/Doge/DogeTransactionDetailsViewController.swift @@ -6,56 +6,56 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UIKit -import Eureka -import CommonKit import Combine +import CommonKit +import Eureka +import UIKit final class DogeTransactionDetailsViewController: TransactionDetailsViewControllerBase { // MARK: - Dependencies - + weak var service: DogeWalletService? { walletService?.core as? DogeWalletService } - + // MARK: - Properties - + private var cachedBlockInfo: (hash: String, height: String)? - + private let autoupdateInterval: TimeInterval = 5.0 private var timerSubscription: AnyCancellable? - + private lazy var refreshControl: UIRefreshControl = { let control = UIRefreshControl() control.tintColor = .adamant.primary control.addTarget(self, action: #selector(refresh), for: UIControl.Event.valueChanged) return control }() - + // MARK: - Lifecycle - + override func viewDidLoad() { currencySymbol = DogeWalletService.currencySymbol - + super.viewDidLoad() if service != nil { tableView.refreshControl = refreshControl } - + refresh(silent: true) - + // MARK: Start update if transaction != nil { startUpdate() } } - + // MARK: - Overrides - + override func explorerUrl(for transaction: TransactionDetails) -> URL? { let id = transaction.txId - + return URL(string: "\(DogeWalletService.explorerTx)\(id)") } - + @MainActor @objc func refresh(silent: Bool = false) { refreshTask = Task { @@ -65,72 +65,74 @@ final class DogeTransactionDetailsViewController: TransactionDetailsViewControll } catch { refreshControl.endRefreshing() updateTransactionStatus() - + guard !silent else { return } dialogService.showRichError(error: error) } } } - + // MARK: Autoupdate - + func startUpdate() { refresh(silent: true) - timerSubscription = Timer + timerSubscription = + Timer .publish(every: autoupdateInterval, on: .main, in: .common) .autoconnect() .sink { [weak self] _ in self?.refresh(silent: true) } } - + // MARK: Updating methods - + @MainActor func updateTransaction() async throws { guard let service = service, - let address = service.wallet?.address, - let id = transaction?.txId + let address = service.wallet?.address, + let id = transaction?.txId else { return } let trs = try await service.getTransaction(by: id, waitsForConnectivity: false) - + if let blockInfo = cachedBlockInfo, - blockInfo.hash == trs.blockHash { + blockInfo.hash == trs.blockHash + { transaction = trs.asBtcTransaction( DogeTransaction.self, for: address, blockId: blockInfo.height ) - + tableView.reloadData() } else if let blockHash = trs.blockHash { let blockInfo: (hash: String, height: String)? - + do { let height = try await service.getBlockId(by: blockHash) blockInfo = (hash: blockHash, height: height) } catch { blockInfo = nil } - + transaction = trs.asBtcTransaction( DogeTransaction.self, for: address, blockId: blockInfo?.height ) - + cachedBlockInfo = blockInfo - + tableView.reloadData() } else { transaction = trs.asBtcTransaction(DogeTransaction.self, for: address) - + tableView.reloadData() } - + updateIncosinstentRowIfNeeded() } } diff --git a/Adamant/Modules/Wallets/Doge/DogeTransactionsViewController.swift b/Adamant/Modules/Wallets/Doge/DogeTransactionsViewController.swift index cd63a6828..562d3f9f9 100644 --- a/Adamant/Modules/Wallets/Doge/DogeTransactionsViewController.swift +++ b/Adamant/Modules/Wallets/Doge/DogeTransactionsViewController.swift @@ -6,30 +6,30 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UIKit -import ProcedureKit import CommonKit +import ProcedureKit +import UIKit final class DogeTransactionsViewController: TransactionsListViewControllerBase { - + // MARK: - UITableView - + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let address = walletService.core.wallet?.address, - let transaction = transactions[safe: indexPath.row] + let transaction = transactions[safe: indexPath.row] else { return } - + let controller = screensFactory.makeDetailsVC(service: walletService) controller.transaction = transaction - + if transaction.senderAddress.caseInsensitiveCompare(address) == .orderedSame { controller.senderName = String.adamant.transactionDetails.yourAddress } - + if transaction.recipientAddress.caseInsensitiveCompare(address) == .orderedSame { controller.recipientName = String.adamant.transactionDetails.yourAddress } - + navigationController?.pushViewController(controller, animated: true) } } @@ -37,17 +37,17 @@ final class DogeTransactionsViewController: TransactionsListViewControllerBase { private class LoadMoreDogeTransactionsProcedure: Procedure { let from: Int let service: DogeWalletService - + private(set) var result: (transactions: [DogeTransaction], hasMore: Bool)? - + init(service: DogeWalletService, from: Int) { self.from = from self.service = service - + super.init() log.severity = .warning } - + override func execute() { Task { do { diff --git a/Adamant/Modules/Wallets/Doge/DogeTransferViewController.swift b/Adamant/Modules/Wallets/Doge/DogeTransferViewController.swift index e4b4ec047..133a6d587 100644 --- a/Adamant/Modules/Wallets/Doge/DogeTransferViewController.swift +++ b/Adamant/Modules/Wallets/Doge/DogeTransferViewController.swift @@ -6,15 +6,15 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UIKit -import Eureka @preconcurrency import BitcoinKit import CommonKit +import Eureka +import UIKit final class DogeTransferViewController: TransferViewControllerBase { // MARK: Send - + @MainActor override func sendFunds() { let comments: String @@ -23,38 +23,39 @@ final class DogeTransferViewController: TransferViewControllerBase { } else { comments = "" } - + guard let service = walletCore as? DogeWalletService, - let recipient = recipientAddress, - let amount = amount + let recipient = recipientAddress, + let amount = amount else { return } - + guard let wallet = service.wallet else { return } - + dialogService.showProgress(withMessage: String.adamant.transfer.transferProcessingMessage, userInteractionEnable: false) - + Task { do { // Create transaction let transaction = try await service.createTransaction( - recipient: recipient, + recipient: recipient, amount: amount, fee: transactionFee, comment: nil ) - + if await !doesNotContainSendingTx() { presentSendingError() return } - + // Send adm report if let reportRecipient = admReportRecipient, - let hash = transaction.txHash { + let hash = transaction.txHash + { try await reportTransferTo( admAddress: reportRecipient, amount: amount, @@ -62,7 +63,7 @@ final class DogeTransferViewController: TransferViewControllerBase { hash: hash ) } - + do { let simpleTransaction = SimpleTransactionDetails( txId: transaction.txID, @@ -76,7 +77,7 @@ final class DogeTransferViewController: TransferViewControllerBase { transactionStatus: nil, nonceRaw: nil ) - + service.coinStorage.append(simpleTransaction) try await service.sendTransaction(transaction) } catch { @@ -84,17 +85,17 @@ final class DogeTransferViewController: TransferViewControllerBase { for: transaction.txId, status: .failed ) - + throw error } - + Task { await service.update() } - + dialogService.dismissProgress() dialogService.showSuccess(withMessage: String.adamant.transfer.transferSuccess) - + // Present detail VC presentDetailTransactionVC( transaction: transaction, @@ -107,7 +108,7 @@ final class DogeTransferViewController: TransferViewControllerBase { } } } - + private func presentDetailTransactionVC( transaction: BitcoinKit.Transaction, comments: String, @@ -117,20 +118,20 @@ final class DogeTransferViewController: TransferViewControllerBase { detailsVc.transaction = transaction detailsVc.senderName = String.adamant.transactionDetails.yourAddress detailsVc.recipientName = recipientName - + if comments.count > 0 { detailsVc.comment = comments } - + delegate?.transferViewController( self, didFinishWithTransfer: transaction, detailsViewController: detailsVc ) } - + // MARK: Overrides - + override func recipientRow() -> BaseRow { let row = TextRow { $0.tag = BaseRows.address.tag @@ -138,11 +139,11 @@ final class DogeTransferViewController: TransferViewControllerBase { $0.cell.textField.keyboardType = .namePhonePad $0.cell.textField.autocorrectionType = .no $0.cell.textField.setLineBreakMode() - + $0.value = recipientAddress?.components( separatedBy: TransferViewControllerBase.invalidCharacters ).joined() - + if recipientIsReadonly { $0.disabled = true $0.cell.textField.isEnabled = false @@ -155,15 +156,15 @@ final class DogeTransferViewController: TransferViewControllerBase { row.cell.textField.text = row.value?.components( separatedBy: TransferViewControllerBase.invalidCharacters ).joined() - + self?.updateToolbar(for: row) }.onCellSelection { [weak self] (cell, _) in self?.shareValue(self?.recipientAddress, from: cell) } - + return row } - + override func defaultSceneTitle() -> String? { return String.adamant.sendDoge } diff --git a/Adamant/Modules/Wallets/Doge/DogeWallet.swift b/Adamant/Modules/Wallets/Doge/DogeWallet.swift index d83597d9d..c53aa710d 100644 --- a/Adamant/Modules/Wallets/Doge/DogeWallet.swift +++ b/Adamant/Modules/Wallets/Doge/DogeWallet.swift @@ -6,24 +6,24 @@ // Copyright © 2019 Adamant. All rights reserved. // -import Foundation @preconcurrency import BitcoinKit import CommonKit +import Foundation final class DogeWallet: WalletAccount, @unchecked Sendable { let unicId: String let addressEntity: Address let privateKey: PrivateKey let publicKey: PublicKey - + @Atomic var balance: Decimal = 0.0 @Atomic var notifications: Int = 0 @Atomic var minBalance: Decimal = 0 @Atomic var minAmount: Decimal = 0 @Atomic var isBalanceInitialized: Bool = false - + var address: String { addressEntity.stringValue } - + init( unicId: String, privateKey: PrivateKey, @@ -34,7 +34,7 @@ final class DogeWallet: WalletAccount, @unchecked Sendable { self.publicKey = privateKey.publicKey() self.addressEntity = try addressConverter.convert(publicKey: publicKey, type: .p2pkh) } - + init( unicId: String, privateKey: PrivateKey, @@ -49,4 +49,18 @@ final class DogeWallet: WalletAccount, @unchecked Sendable { self.publicKey = privateKey.publicKey() self.addressEntity = try addressConverter.convert(publicKey: publicKey, type: .p2pkh) } + + #if DEBUG + @available(*, deprecated, message: "For testing purposes only") + init( + unicId: String, + privateKey: PrivateKey, + addressEntity: Address + ) { + self.unicId = unicId + self.privateKey = privateKey + self.publicKey = privateKey.publicKey() + self.addressEntity = addressEntity + } + #endif } diff --git a/Adamant/Modules/Wallets/Doge/DogeWalletFactory.swift b/Adamant/Modules/Wallets/Doge/DogeWalletFactory.swift index 0ece9a4e8..3effd2316 100644 --- a/Adamant/Modules/Wallets/Doge/DogeWalletFactory.swift +++ b/Adamant/Modules/Wallets/Doge/DogeWalletFactory.swift @@ -6,16 +6,16 @@ // Copyright © 2019 Adamant. All rights reserved. // -import Swinject import CommonKit +import Swinject import UIKit struct DogeWalletFactory: WalletFactory { typealias Service = WalletService - + let typeSymbol: String = DogeWalletService.richMessageType let assembler: Assembler - + func makeWalletVC(service: Service, screensFactory: ScreensFactory) -> WalletViewController { DogeWalletViewController( dialogService: assembler.resolve(DialogService.self)!, @@ -26,7 +26,7 @@ struct DogeWalletFactory: WalletFactory { service: service ) } - + func makeTransferListVC(service: Service, screensFactory: ScreensFactory) -> UIViewController { DogeTransactionsViewController( walletService: service, @@ -35,7 +35,7 @@ struct DogeWalletFactory: WalletFactory { screensFactory: screensFactory ) } - + func makeTransferVC(service: Service, screensFactory: ScreensFactory) -> TransferViewControllerBase { DogeTransferViewController( chatsProvider: assembler.resolve(ChatsProvider.self)!, @@ -51,18 +51,18 @@ struct DogeWalletFactory: WalletFactory { apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)! ) } - + func makeDetailsVC(service: Service, transaction: RichMessageTransaction) -> UIViewController? { guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) else { return nil } - + let comment: String? if let raw = transaction.getRichValue(for: RichContentKeys.transfer.comments), raw.count > 0 { comment = raw } else { comment = nil } - + return makeTransactionDetailsVC( hash: hash, senderId: transaction.senderId, @@ -75,14 +75,14 @@ struct DogeWalletFactory: WalletFactory { service: service ) } - + func makeDetailsVC(service: Service) -> TransactionDetailsViewControllerBase { makeTransactionDetailsVC(service: service) } } -private extension DogeWalletFactory { - func makeTransactionDetailsVC( +extension DogeWalletFactory { + fileprivate func makeTransactionDetailsVC( hash: String, senderId: String?, recipientId: String?, @@ -97,15 +97,16 @@ private extension DogeWalletFactory { vc.senderId = senderId vc.recipientId = recipientId vc.comment = comment - + let amount: Decimal if let amountRaw = richTransaction.getRichValue(for: RichContentKeys.transfer.amount), - let decimal = Decimal(string: amountRaw) { + let decimal = Decimal(string: amountRaw) + { amount = decimal } else { amount = 0 } - + let failedTransaction = SimpleTransactionDetails( txId: hash, senderAddress: senderAddress, @@ -116,7 +117,7 @@ private extension DogeWalletFactory { confirmationsValue: nil, blockValue: nil, isOutgoing: richTransaction.isOutgoing, - transactionStatus: nil, + transactionStatus: nil, nonceRaw: nil ) @@ -124,8 +125,8 @@ private extension DogeWalletFactory { vc.richTransaction = richTransaction return vc } - - func makeTransactionDetailsVC(service: Service) -> DogeTransactionDetailsViewController { + + fileprivate func makeTransactionDetailsVC(service: Service) -> DogeTransactionDetailsViewController { DogeTransactionDetailsViewController( dialogService: assembler.resolve(DialogService.self)!, currencyInfo: assembler.resolve(InfoServiceProtocol.self)!, diff --git a/Adamant/Modules/Wallets/Doge/DogeWalletService+DynamicConstants.swift b/Adamant/Modules/Wallets/Doge/DogeWalletService+DynamicConstants.swift deleted file mode 100644 index 5cc531c7f..000000000 --- a/Adamant/Modules/Wallets/Doge/DogeWalletService+DynamicConstants.swift +++ /dev/null @@ -1,89 +0,0 @@ -import Foundation -import BigInt -import CommonKit - -extension DogeWalletService { - // MARK: - Constants - static let fixedFee: Decimal = 1 - static let currencySymbol = "DOGE" - static let currencyExponent: Int = -8 - static let qqPrefix: String = "doge" - - static let healthCheckParameters = CoinHealthCheckParameters( - normalUpdateInterval: 390, - crucialUpdateInterval: 30, - onScreenUpdateInterval: 10, - threshold: 3, - normalServiceUpdateInterval: 390, - crucialServiceUpdateInterval: 30, - onScreenServiceUpdateInterval: 10 - ) - - static var newPendingInterval: Int { - 5000 - } - - static var oldPendingInterval: Int { - 3000 - } - - static var registeredInterval: Int { - 20000 - } - - static var newPendingAttempts: Int { - 20 - } - - static var oldPendingAttempts: Int { - 4 - } - - var tokenName: String { - "Dogecoin" - } - - var consistencyMaxTime: Double { - 900 - } - - var minBalance: Decimal { - 0 - } - - var minAmount: Decimal { - 1 - } - - var defaultVisibility: Bool { - true - } - - var defaultOrdinalLevel: Int? { - 70 - } - - static var minNodeVersion: String? { - nil - } - - var transferDecimals: Int { - 8 - } - - static let explorerTx = "https://dogechain.info/tx/" - static let explorerAddress = "https://dogechain.info/address/" - static var nodes: [Node] { - [ - Node.makeDefaultNode(url: URL(string: "https://dogenode1.adamant.im")!, altUrl: URL(string: "http://5.9.99.62:44099")), -Node.makeDefaultNode(url: URL(string: "https://dogenode2.adamant.im")!, altUrl: URL(string: "http://176.9.32.126:44098")), -Node.makeDefaultNode(url: URL(string: "https://dogenode3.adm.im")!, altUrl: URL(string: "http://95.216.45.88:44098")), - ] - } - - static var serviceNodes: [Node] { - [ - - ] - } -} diff --git a/Adamant/Modules/Wallets/Doge/DogeWalletService+RichMessageProvider.swift b/Adamant/Modules/Wallets/Doge/DogeWalletService+RichMessageProvider.swift index 87868730a..900e0dd25 100644 --- a/Adamant/Modules/Wallets/Doge/DogeWalletService+RichMessageProvider.swift +++ b/Adamant/Modules/Wallets/Doge/DogeWalletService+RichMessageProvider.swift @@ -6,28 +6,28 @@ // Copyright © 2019 Adamant. All rights reserved. // +import CommonKit import Foundation import MessageKit import UIKit -import CommonKit extension DogeWalletService { var newPendingInterval: TimeInterval { .init(milliseconds: type(of: self).newPendingInterval) } - + var oldPendingInterval: TimeInterval { .init(milliseconds: type(of: self).oldPendingInterval) } - + var registeredInterval: TimeInterval { .init(milliseconds: type(of: self).registeredInterval) } - + var newPendingAttempts: Int { type(of: self).newPendingAttempts } - + var oldPendingAttempts: Int { type(of: self).oldPendingAttempts } @@ -35,30 +35,30 @@ extension DogeWalletService { var dynamicRichMessageType: String { return type(of: self).richMessageType } - + // MARK: Short description - + func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { let amount: String - + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount) else { return NSAttributedString(string: "⬅️ \(DogeWalletService.currencySymbol)") } - + if let decimal = Decimal(string: raw) { amount = AdamantBalanceFormat.full.format(decimal) } else { amount = raw } - + let string: String if transaction.isOutgoing { string = "⬅️ \(amount) \(DogeWalletService.currencySymbol)" } else { string = "➡️ \(amount) \(DogeWalletService.currencySymbol)" } - + return NSAttributedString(string: string) } } diff --git a/Adamant/Modules/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift b/Adamant/Modules/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift index e01a28a5f..87f92df4f 100644 --- a/Adamant/Modules/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift +++ b/Adamant/Modules/Wallets/Doge/DogeWalletService+RichMessageProviderWithStatusCheck.swift @@ -6,31 +6,31 @@ // Copyright © 2019 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation extension DogeWalletService { func statusInfoFor(transaction: CoinTransaction) async -> TransactionStatusInfo { let hash: String? - + if let transaction = transaction as? RichMessageTransaction { hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) } else { hash = transaction.txId } - + guard let hash = hash else { return .init(sentDate: nil, status: .inconsistent(.wrongTxHash)) } - + let dogeTransaction: BTCRawTransaction - + do { dogeTransaction = try await getTransaction(by: hash, waitsForConnectivity: true) } catch { return .init(error: error) } - + return await .init( sentDate: dogeTransaction.date, status: getStatus( @@ -41,74 +41,74 @@ extension DogeWalletService { } } -private extension DogeWalletService { - func getStatus( +extension DogeWalletService { + fileprivate func getStatus( dogeTransaction: BTCRawTransaction, transaction: CoinTransaction ) async -> TransactionStatus { // MARK: Check confirmations guard let confirmations = dogeTransaction.confirmations, - let dogeDate = dogeTransaction.date, - (confirmations > 0 || dogeDate.timeIntervalSinceNow > -60 * 15) + let dogeDate = dogeTransaction.date, + confirmations > 0 || dogeDate.timeIntervalSinceNow > -60 * 15 else { return .pending } - + // MARK: Check amount & address guard let reportedValue = reportedValue(for: transaction) else { return .inconsistent(.wrongAmount) } - - let min = reportedValue - reportedValue*0.005 - let max = reportedValue + reportedValue*0.005 - + + let min = reportedValue - reportedValue * 0.005 + let max = reportedValue + reportedValue * 0.005 + guard let walletAddress = dogeWallet?.address else { return .inconsistent(.unknown) } - + let readableTransaction = dogeTransaction.asBtcTransaction(DogeTransaction.self, for: walletAddress) - + var realSenderAddress = readableTransaction.senderAddress var realRecipientAddress = readableTransaction.recipientAddress - + if transaction is RichMessageTransaction { guard let senderAddress = try? await getWalletAddress(byAdamantAddress: transaction.senderAddress) else { return .inconsistent(.senderCryptoAddressUnavailable(tokenSymbol)) } - + guard let recipientAddress = try? await getWalletAddress(byAdamantAddress: transaction.recipientAddress) else { return .inconsistent(.recipientCryptoAddressUnavailable(tokenSymbol)) } - + realSenderAddress = senderAddress realRecipientAddress = recipientAddress } - + guard readableTransaction.senderAddress.caseInsensitiveCompare(realSenderAddress) == .orderedSame else { return .inconsistent(.senderCryptoAddressMismatch(tokenSymbol)) } - + guard readableTransaction.recipientAddress.caseInsensitiveCompare(realRecipientAddress) == .orderedSame else { return .inconsistent(.recipientCryptoAddressMismatch(tokenSymbol)) } - + var result: TransactionStatus = .inconsistent(.wrongAmount) if transaction.isOutgoing { guard readableTransaction.senderAddress.caseInsensitiveCompare(walletAddress) == .orderedSame else { return .inconsistent(.senderCryptoAddressMismatch(tokenSymbol)) } - + var totalIncome: Decimal = 0 for output in dogeTransaction.outputs { guard !output.addresses.contains(walletAddress) else { continue } - + totalIncome += output.value } - + if (min...max).contains(totalIncome) { result = .success } @@ -116,37 +116,37 @@ private extension DogeWalletService { guard readableTransaction.recipientAddress.caseInsensitiveCompare(walletAddress) == .orderedSame else { return .inconsistent(.recipientCryptoAddressMismatch(tokenSymbol)) } - + var totalOutcome: Decimal = 0 for output in dogeTransaction.outputs { guard output.addresses.contains(walletAddress) else { continue } - + totalOutcome += output.value } - + if (min...max).contains(totalOutcome) { result = .success } } - + return result } - - func reportedValue(for transaction: CoinTransaction) -> Decimal? { + + fileprivate func reportedValue(for transaction: CoinTransaction) -> Decimal? { guard let transaction = transaction as? RichMessageTransaction else { return transaction.amountValue } - + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), let reportedValue = AdamantBalanceFormat.deserializeBalance(from: raw) else { return nil } - + return reportedValue } } diff --git a/Adamant/Modules/Wallets/Doge/DogeWalletService+Send.swift b/Adamant/Modules/Wallets/Doge/DogeWalletService+Send.swift index d81fa10c4..8561383e8 100644 --- a/Adamant/Modules/Wallets/Doge/DogeWalletService+Send.swift +++ b/Adamant/Modules/Wallets/Doge/DogeWalletService+Send.swift @@ -6,10 +6,10 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UIKit -import BitcoinKit import Alamofire +import BitcoinKit import CommonKit +import UIKit extension BitcoinKit.Transaction: RawTransaction { var txHash: String? { @@ -19,7 +19,7 @@ extension BitcoinKit.Transaction: RawTransaction { extension DogeWalletService: WalletServiceTwoStepSend { typealias T = BitcoinKit.Transaction - + // MARK: Create & Send func createTransaction( recipient: String, @@ -31,33 +31,34 @@ extension DogeWalletService: WalletServiceTwoStepSend { guard let wallet = self.dogeWallet else { throw WalletServiceError.notLogged } - + let key = wallet.privateKey - + guard let toAddress = try? addressConverter.convert(address: recipient) else { throw WalletServiceError.accountNotFound } - + let rawAmount = NSDecimalNumber(decimal: amount * DogeWalletService.multiplier).uint64Value let fee = NSDecimalNumber(decimal: fee * DogeWalletService.multiplier).uint64Value - + // Search for unspent transactions do { let utxos = try await getUnspentTransactions() - + // Check if we have enought money let totalAmount: UInt64 = UInt64(utxos.reduce(0) { $0 + $1.output.value }) - guard totalAmount >= rawAmount + fee else { // This shit can crash BitcoinKit + guard totalAmount >= rawAmount + fee else { // This shit can crash BitcoinKit throw WalletServiceError.notEnoughMoney } - + // Create local transaction - let transaction = BitcoinKit.Transaction.createNewTransaction( + let transaction = btcTransactionFactory.createTransaction( toAddress: toAddress, amount: rawAmount, fee: fee, changeAddress: wallet.addressEntity, utxos: utxos, + lockTime: 0, keys: [key] ) return transaction @@ -65,10 +66,10 @@ extension DogeWalletService: WalletServiceTwoStepSend { throw error } } - + func sendTransaction(_ transaction: BitcoinKit.Transaction) async throws { let txHex = transaction.serialized().hex - + _ = try await dogeApiService.api.request(waitsForConnectivity: false) { core, origin in let response: APIResponseModel = await core.apiCore.sendRequestBasic( origin: origin, @@ -79,14 +80,14 @@ extension DogeWalletService: WalletServiceTwoStepSend { timeout: .common, downloadProgress: { _ in } ) - + guard - !(200 ... 299).contains(response.code ?? .zero), + !(200...299).contains(response.code ?? .zero), let dataString = response.data.map({ String(decoding: $0, as: UTF8.self) }), dataString.contains("dust"), dataString.contains("-26") else { return response.result.mapError { $0.asWalletServiceError() } } - + return .failure(.dustAmountError) }.get() } @@ -96,58 +97,58 @@ extension BitcoinKit.Transaction: @retroactive @unchecked Sendable {} extension BitcoinKit.Transaction: TransactionDetails { var defaultCurrencySymbol: String? { DogeWalletService.currencySymbol } - + var txId: String { return txID } - + var dateValue: Date? { switch lockTime { - case 1..<500000000: + case 1..<500_000_000: return nil - case 500000000...: + case 500_000_000...: return Date(timeIntervalSince1970: TimeInterval(lockTime)) default: return nil } } - + var amountValue: Decimal? { - return Decimal(outputs[0].value) / Decimal(100000000) + return Decimal(outputs[0].value) / Decimal(100_000_000) } - + var feeValue: Decimal? { return nil } - + var confirmationsValue: String? { return "0" } - + var blockValue: String? { return nil } - + var isOutgoing: Bool { return true } - + var blockHeight: UInt64? { return nil } - + var transactionStatus: TransactionStatus? { - return .pending + return .notInitiated } - + var senderAddress: String { return "" } - + var recipientAddress: String { return "" } - + var nonceRaw: String? { nil } diff --git a/Adamant/Modules/Wallets/Doge/DogeWalletService.swift b/Adamant/Modules/Wallets/Doge/DogeWalletService.swift index 480dda84c..3e0706dff 100644 --- a/Adamant/Modules/Wallets/Doge/DogeWalletService.swift +++ b/Adamant/Modules/Wallets/Doge/DogeWalletService.swift @@ -6,134 +6,144 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UIKit -import Swinject import Alamofire import BitcoinKit import Combine import CommonKit +import Swinject +import UIKit +import Web3Core struct DogeApiCommands { static func balance(for address: String) -> String { return "/api/addr/\(address)/balance" } - + static func getTransactions(for address: String) -> String { return "/api/addrs/\(address)/txs" } - + static func getTransaction(by hash: String) -> String { return "/api/tx/\(hash)" } - + static func getBlock(by hash: String) -> String { return "/api/block/\(hash)" } - + static func getBlocks() -> String { return "/api/blocks" } - + static func getUnspentTransactions(for address: String) -> String { return "/api/addr/\(address)/utxo" } - + static func sendTransaction() -> String { return "/api/tx/send" } - + static func getInfo() -> String { return "/api/status" } } -final class DogeWalletService: WalletCoreProtocol, @unchecked Sendable { +final class DogeWalletService: WalletCoreProtocol, WalletStaticCoreProtocol, @unchecked Sendable { + static let currencySymbol = "DOGE" var wallet: WalletAccount? { return dogeWallet } - + // MARK: RichMessageProvider properties static let richMessageType = "doge_transaction" - + // MARK: - Dependencies var apiService: AdamantApiServiceProtocol! - var dogeApiService: DogeApiService! + var dogeApiService: DogeApiServiceProtocol! + var btcTransactionFactory: BitcoinKitTransactionFactoryProtocol! var accountService: AccountService! var dialogService: DialogService! var addressConverter: AddressConverter! var vibroService: VibroService! var coreDataStack: CoreDataStack! var chatsProvider: ChatsProvider! - + // MARK: - Constants static let currencyLogo = UIImage.asset(named: "doge_wallet") ?? .init() static let multiplier = Decimal(sign: .plus, exponent: 8, significand: 1) static let chunkSize = 20 - + var tokenSymbol: String { return type(of: self).currencySymbol } - + var tokenLogo: UIImage { return type(of: self).currencyLogo } - + static var tokenNetworkSymbol: String { return "DOGE" } - + var tokenContract: String { return "" } - - var tokenUnicID: String { + + var tokenUniqueID: String { Self.tokenNetworkSymbol + tokenSymbol } - + var transactionFee: Decimal { return DogeWalletService.fixedFee } - + var richMessageType: String { return Self.richMessageType - } - + } + var explorerAddress: String { Self.explorerAddress } - + var qqPrefix: String { return Self.qqPrefix } - + var nodeGroups: [NodeGroup] { [.doge] } - + static let kvsAddress = "doge:address" - + @Atomic private(set) var isWarningGasPrice = false - + // MARK: - Notifications let walletUpdatedNotification = Notification.Name("adamant.dogeWallet.walletUpdated") let serviceEnabledChanged = Notification.Name("adamant.dogeWallet.enabledChanged") let serviceStateChanged = Notification.Name("adamant.dogeWallet.stateChanged") let transactionFeeUpdated = Notification.Name("adamant.dogeWallet.feeUpdated") - + + @MainActor + private let walletUpdateSender = ObservableSender() + @MainActor + var walletUpdatePublisher: AnyObservable { + walletUpdateSender.eraseToAnyPublisher() + } + // MARK: - Delayed KVS save @Atomic private var balanceObserver: NSObjectProtocol? - + // MARK: - Properties @Atomic private(set) var dogeWallet: DogeWallet? @Atomic private(set) var enabled = true @Atomic public var network: Network @Atomic private var cachedWalletAddress: [String: String] = [:] @Atomic private var balanceInvalidationSubscription: AnyCancellable? - + let defaultDispatchQueue = DispatchQueue( label: "im.adamant.dogeWalletService", qos: .userInteractive, attributes: [.concurrent] ) - + private static let jsonDecoder = JSONDecoder() @Atomic private var subscriptions = Set() @@ -143,37 +153,37 @@ final class DogeWalletService: WalletCoreProtocol, @unchecked Sendable { var transactionsPublisher: AnyObservable<[TransactionDetails]> { $historyTransactions.eraseToAnyPublisher() } - + var hasMoreOldTransactionsPublisher: AnyObservable { $hasMoreOldTransactions.eraseToAnyPublisher() } - + @MainActor var hasEnabledNode: Bool { dogeApiService.hasEnabledNode } - + @MainActor var hasEnabledNodePublisher: AnyObservable { dogeApiService.hasEnabledNodePublisher } - + private(set) lazy var coinStorage: CoinStorageService = AdamantCoinStorageService( - coinId: tokenUnicID, + coinId: tokenUniqueID, coreDataStack: coreDataStack, blockchainType: richMessageType ) - + // MARK: - State @Atomic private(set) var state: WalletServiceState = .notInitiated - + private func setState(_ newState: WalletServiceState, silent: Bool = false) { guard newState != state else { return } - + state = newState - + if !silent { NotificationCenter.default.post( name: serviceStateChanged, @@ -182,15 +192,15 @@ final class DogeWalletService: WalletCoreProtocol, @unchecked Sendable { ) } } - + init() { self.network = DogeMainnet() self.setState(.notInitiated) - + // Notifications addObservers() } - + func addObservers() { NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedIn, object: nil) @@ -198,14 +208,14 @@ final class DogeWalletService: WalletCoreProtocol, @unchecked Sendable { self?.update() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantAccountService.accountDataUpdated, object: nil) .sink { @MainActor [weak self] _ in self?.update() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedOut, object: nil) .sink { @MainActor [weak self] _ in @@ -221,7 +231,7 @@ final class DogeWalletService: WalletCoreProtocol, @unchecked Sendable { } .store(in: &subscriptions) } - + func addTransactionObserver() { coinStorage.transactionsPublisher .sink { [weak self] transactions in @@ -229,50 +239,54 @@ final class DogeWalletService: WalletCoreProtocol, @unchecked Sendable { } .store(in: &subscriptions) } - + func update() { Task { await update() } } - + @MainActor func update() async { guard let wallet = dogeWallet else { return } - + switch state { case .notInitiated, .updating, .initiationFailed: return - + case .upToDate: break } - + setState(.updating) - + if let balance = try? await getBalance() { if wallet.balance < balance, wallet.isBalanceInitialized { vibroService.applyVibration(.success) } - + wallet.balance = balance markBalanceAsFresh(wallet) - - NotificationCenter.default.post( - name: walletUpdatedNotification, - object: self, - userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet] - ) + + walletUpdateSender.send() + } else { + wallet.isBalanceInitialized = false } - + + NotificationCenter.default.post( + name: walletUpdatedNotification, + object: self, + userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet] + ) + setState(.upToDate) } - + func validate(address: String) -> AddressValidationResult { let address = try? addressConverter.convert(address: address) - + switch address?.scriptType { case .p2pk, .p2pkh, .p2sh: return .valid @@ -280,20 +294,22 @@ final class DogeWalletService: WalletCoreProtocol, @unchecked Sendable { return .invalid(description: nil) } } - + private func markBalanceAsFresh(_ wallet: DogeWallet) { wallet.isBalanceInitialized = true - + balanceInvalidationSubscription = Task { [weak self] in try await Task.sleep(interval: Self.balanceLifetime, pauseInBackground: true) guard let self else { return } wallet.isBalanceInitialized = false - + NotificationCenter.default.post( name: walletUpdatedNotification, object: self, userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet] ) + + await self.walletUpdateSender.send() }.eraseToAnyCancellable() } } @@ -304,41 +320,46 @@ extension DogeWalletService { setState(.initiationFailed(reason: reason)) dogeWallet = nil } - - func initWallet(withPassphrase passphrase: String) async throws -> WalletAccount { + + func initWallet(withPassphrase passphrase: String, withPassword password: String) async throws -> WalletAccount { guard let adamant = accountService.account else { throw WalletServiceError.notLogged } - + setState(.notInitiated) - + if enabled { enabled = false NotificationCenter.default.post(name: serviceEnabledChanged, object: self) } - - let privateKeyData = passphrase.data(using: .utf8)!.sha256() + + guard let privateKeyData = makeBinarySeed(withMnemonicSentence: passphrase, withSalt: password) else { + throw WalletServiceError.internalError(message: "DOGE Wallet: failed to generate private key", error: nil) + } + let privateKey = PrivateKey(data: privateKeyData, network: self.network, isPublicKeyCompressed: true) - + let eWallet = try DogeWallet( - unicId: tokenUnicID, + unicId: tokenUniqueID, privateKey: privateKey, addressConverter: addressConverter ) self.dogeWallet = eWallet let kvsAddressModel = makeKVSAddressModel(wallet: eWallet) - + NotificationCenter.default.post( name: walletUpdatedNotification, object: self, userInfo: [AdamantUserInfoKey.WalletService.wallet: eWallet] ) - + + await walletUpdateSender.send() + if !self.enabled { self.enabled = true NotificationCenter.default.post(name: self.serviceEnabledChanged, object: self) } - + // MARK: 4. Save address into KVS let service = self do { @@ -348,13 +369,13 @@ extension DogeWalletService { service.kvsSaveCompletionRecursion(kvsAddressModel, result: result) } } - + service.setState(.upToDate) - + Task { await service.update() } - + return eWallet } catch let error as WalletServiceError { switch error { @@ -362,26 +383,34 @@ extension DogeWalletService { /// The ADM Wallet is not initialized. Check the balance of the current wallet /// and save the wallet address to kvs when dropshipping ADM service.setState(.upToDate) - + Task { await service.update() } - + if let kvsAddressModel { service.save(kvsAddressModel) { result in service.kvsSaveCompletionRecursion(kvsAddressModel, result: result) } } - + service.setState(.upToDate) return eWallet - + default: service.setState(.upToDate) throw error } } } + + private func makeBinarySeed(withMnemonicSentence passphrase: String, withSalt salt: String) -> Data? { + guard !salt.isEmpty else { + return passphrase.data(using: .utf8)!.sha256() + } + + return BIP39.seedFromMmemonics(passphrase, password: salt, language: .english) + } } // MARK: - Dependencies @@ -390,13 +419,14 @@ extension DogeWalletService: SwinjectDependentService { accountService = container.resolve(AccountService.self) apiService = container.resolve(AdamantApiServiceProtocol.self) dialogService = container.resolve(DialogService.self) + btcTransactionFactory = container.resolve(BitcoinKitTransactionFactoryProtocol.self) addressConverter = container.resolve(AddressConverterFactory.self)? .make(network: network) dogeApiService = container.resolve(DogeApiService.self) vibroService = container.resolve(VibroService.self) coreDataStack = container.resolve(CoreDataStack.self) chatsProvider = container.resolve(ChatsProvider.self) - + addTransactionObserver() } } @@ -407,10 +437,10 @@ extension DogeWalletService { guard let address = dogeWallet?.address else { throw WalletServiceError.walletNotInitiated } - + return try await getBalance(address: address) } - + func getBalance(address: String) async throws -> Decimal { let data: Data = try await dogeApiService.request(waitsForConnectivity: false) { core, origin in await core.sendRequest( @@ -418,9 +448,8 @@ extension DogeWalletService { path: DogeApiCommands.balance(for: address) ) }.get() - - if - let string = String(data: data, encoding: .utf8), + + if let string = String(data: data, encoding: .utf8), let raw = Decimal(string: string) { let balance = raw / DogeWalletService.multiplier @@ -429,21 +458,21 @@ extension DogeWalletService { throw WalletServiceError.internalError(InternalAPIError.parsingFailed) } } - + func getWalletAddress(byAdamantAddress address: String) async throws -> String { if let address = cachedWalletAddress[address], !address.isEmpty { return address } - + do { let result = try await apiService.get(key: DogeWalletService.kvsAddress, sender: address).get() - + guard let result = result else { throw WalletServiceError.walletNotInitiated } - + cachedWalletAddress[address] = result - + return result } catch _ as ApiServiceError { throw WalletServiceError.remoteServiceError( @@ -464,62 +493,65 @@ extension DogeWalletService { completion(.failure(error: .notLogged)) return } - + guard adamant.balance >= AdamantApiService.KvsFee else { completion(.failure(error: .notEnoughMoney)) return } - + Task { @Sendable in - let result = await apiService.store(model) - + let result = await apiService.store(model, date: .now) switch result { case .success: completion(.success) - + case .failure(let error): completion(.failure(error: .apiError(error))) } } } - + /// New accounts doesn't have enought money to save KVS. We need to wait for balance update, and then - retry save private func kvsSaveCompletionRecursion(_ model: KVSValueModel, result: WalletServiceSimpleResult) { if let observer = balanceObserver { NotificationCenter.default.removeObserver(observer) balanceObserver = nil } - + switch result { case .success: break - + case .failure(let error): switch error { case .notEnoughMoney: // Possibly new account, we need to wait for dropship // Register observer - let observer = NotificationCenter.default.addObserver(forName: NSNotification.Name.AdamantAccountService.accountDataUpdated, object: nil, queue: nil) { [weak self] _ in + let observer = NotificationCenter.default.addObserver( + forName: NSNotification.Name.AdamantAccountService.accountDataUpdated, + object: nil, + queue: nil + ) { [weak self] _ in guard let balance = self?.accountService.account?.balance, balance > AdamantApiService.KvsFee else { return } - + self?.save(model) { [weak self] result in self?.kvsSaveCompletionRecursion(model, result: result) } } - + // Save referense to unregister it later balanceObserver = observer - + default: print("\(error.localizedDescription)") } } } - + private func makeKVSAddressModel(wallet: WalletAccount) -> KVSValueModel? { guard let keypair = accountService.keypair else { return nil } - + return .init( key: Self.kvsAddress, value: wallet.address, @@ -534,20 +566,20 @@ extension DogeWalletService { guard let address = self.wallet?.address else { throw WalletServiceError.notLogged } - + let doge = try await getTransactions( for: address, from: from, to: from + DogeWalletService.chunkSize ) - + let hasMore = doge.to < doge.totalItems - + let transactions = doge.items.filter { !$0.isDoubleSpend }.map { $0.asBtcTransaction(DogeTransaction.self, for: address) } - + return (transactions: transactions, hasMore: hasMore) } - + private func getTransactions( for address: String, from: Int, @@ -557,7 +589,7 @@ extension DogeWalletService { "from": from, "to": to ] - + return try await dogeApiService.request(waitsForConnectivity: false) { core, origin in await core.sendRequestJsonResponse( origin: origin, @@ -568,18 +600,18 @@ extension DogeWalletService { ) }.get() } - + func getUnspentTransactions() async throws -> [UnspentTransaction] { guard let wallet = self.dogeWallet else { throw WalletServiceError.notLogged } - + let address = wallet.address - + let parameters = [ "noCache": "1" ] - + // MARK: Sending request let data = try await dogeApiService.request(waitsForConnectivity: false) { core, origin in await core.sendRequest( @@ -590,12 +622,13 @@ extension DogeWalletService { encoding: .url ) }.get() - - let items = try? JSONSerialization.jsonObject( - with: data, - options: [] - ) as? [[String: Any]] - + + let items = + try? JSONSerialization.jsonObject( + with: data, + options: [] + ) as? [[String: Any]] + guard let items = items else { throw WalletServiceError.remoteServiceError( message: "DOGE Wallet: not valid response" @@ -609,12 +642,13 @@ extension DogeWalletService { let confirmations = item["confirmations"] as? NSNumber, confirmations.intValue > 0, let vout = item["vout"] as? NSNumber, - let amount = item["amount"] as? NSNumber else { + let amount = item["amount"] as? NSNumber + else { continue } let value = NSDecimalNumber(decimal: (amount.decimalValue * DogeWalletService.multiplier)).uint64Value - + let lockScript = wallet.addressEntity.lockingScript let txHash = Data(hex: txid).map { Data($0.reversed()) } ?? Data() let txIndex = vout.uint32Value @@ -628,7 +662,7 @@ extension DogeWalletService { return utxos } - + func getTransaction(by hash: String, waitsForConnectivity: Bool) async throws -> BTCRawTransaction { try await dogeApiService.request(waitsForConnectivity: waitsForConnectivity) { core, origin in await core.sendRequestJsonResponse( @@ -637,78 +671,88 @@ extension DogeWalletService { ) }.get() } - + func getBlockId(by hash: String) async throws -> String { let data = try await dogeApiService.request(waitsForConnectivity: false) { core, origin in await core.sendRequest(origin: origin, path: DogeApiCommands.getBlock(by: hash)) }.get() - - let json = try? JSONSerialization.jsonObject( - with: data, - options: [] - ) as? [String: Any] - + + let json = + try? JSONSerialization.jsonObject( + with: data, + options: [] + ) as? [String: Any] + guard let json = json else { throw WalletServiceError.remoteServiceError( message: "DOGE Wallet: not valid response" ) } - + if let height = json["height"] as? NSNumber { return height.stringValue } else { throw WalletServiceError.remoteServiceError(message: "Failed to parse block") } } - + func loadTransactions(offset: Int, limit: Int) async throws -> Int { let tuple = try await getTransactions(from: offset) - + let trs = tuple.transactions hasMoreOldTransactions = tuple.hasMore - + guard trs.count > 0 else { hasMoreOldTransactions = false return .zero } - + coinStorage.append(trs) - + return trs.count } - + func getTransactionsHistory(offset: Int, limit: Int) async throws -> [TransactionDetails] { try await getTransactions(from: offset).transactions } - + func getLocalTransactionHistory() -> [TransactionDetails] { return historyTransactions } - + func updateStatus(for id: String, status: TransactionStatus?) { coinStorage.updateStatus(for: id, status: status) } } +#if DEBUG + extension DogeWalletService { + @available(*, deprecated, message: "For testing purposes only") + func setWalletForTests(_ wallet: DogeWallet?) { + self.dogeWallet = wallet + } + } +#endif + // MARK: - PrivateKey generator extension DogeWalletService: PrivateKeyGenerator { var rowTitle: String { return "Doge" } - + var rowImage: UIImage? { return .asset(named: "doge_wallet_row") } - + var keyFormat: KeyFormat { .WIF } - + func generatePrivateKeyFor(passphrase: String) -> String? { guard AdamantUtilities.validateAdamantPassphrase(passphrase: passphrase), let privateKeyData = passphrase.data(using: .utf8)?.sha256() else { return nil } - + let privateKey = PrivateKey(data: privateKeyData, network: self.network, isPublicKeyCompressed: true) - + return privateKey.toWIF() } } diff --git a/Adamant/Modules/Wallets/Doge/DogeWalletViewController.swift b/Adamant/Modules/Wallets/Doge/DogeWalletViewController.swift index fe411605e..244999e0e 100644 --- a/Adamant/Modules/Wallets/Doge/DogeWalletViewController.swift +++ b/Adamant/Modules/Wallets/Doge/DogeWalletViewController.swift @@ -6,14 +6,14 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UIKit import CommonKit +import UIKit extension String.adamant { static var doge: String { String.localized("AccountTab.Wallets.doge_wallet", comment: "Account tab: Doge wallet") } - + static var sendDoge: String { String.localized("AccountTab.Row.SendDoge", comment: "Account tab: 'Send DOGE tokens' button") } @@ -23,11 +23,11 @@ final class DogeWalletViewController: WalletViewControllerBase { override func sendRowLocalizedLabel() -> NSAttributedString { return NSAttributedString(string: String.adamant.sendDoge) } - + override func encodeForQr(address: String) -> String? { return "doge:\(address)" } - + override func setTitle() { walletTitleLabel.text = String.adamant.doge } diff --git a/Adamant/Modules/Wallets/ERC20/ERC20ApiService.swift b/Adamant/Modules/Wallets/ERC20/ERC20ApiService.swift index dce6ca79e..3edfb4670 100644 --- a/Adamant/Modules/Wallets/ERC20/ERC20ApiService.swift +++ b/Adamant/Modules/Wallets/ERC20/ERC20ApiService.swift @@ -6,17 +6,17 @@ // Copyright © 2023 Adamant. All rights reserved. // -import web3swift -@preconcurrency import Web3Core import CommonKit +@preconcurrency import Web3Core +import web3swift -final class ERC20ApiService: EthApiService, @unchecked Sendable { +final class ERC20ApiService: EthApiService, ERC20ApiServiceProtocol, @unchecked Sendable { func requestERC20( token: ERC20Token, _ body: @Sendable @escaping (ERC20) async throws -> Output ) async -> WalletServiceResult { let contractAddress = EthereumAddress(token.contractAddress) ?? .zero - + return await requestWeb3(waitsForConnectivity: false) { web3 in let erc20 = ERC20(web3: web3, provider: web3.provider, address: contractAddress) return try await body(erc20) diff --git a/Adamant/Modules/Wallets/ERC20/ERC20ApiServiceProtocol.swift b/Adamant/Modules/Wallets/ERC20/ERC20ApiServiceProtocol.swift new file mode 100644 index 000000000..93ce10f56 --- /dev/null +++ b/Adamant/Modules/Wallets/ERC20/ERC20ApiServiceProtocol.swift @@ -0,0 +1,21 @@ +// +// ERC20ApiServiceProtocol.swift +// Adamant +// +// Created by Christian Benua on 25.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import CommonKit +@preconcurrency import Web3Core +import web3swift + +protocol ERC20ApiServiceProtocol: EthApiServiceProtocol { + + var keystoreManager: KeystoreManager? { get async } + + func requestERC20( + token: ERC20Token, + _ body: @Sendable @escaping (ERC20) async throws -> Output + ) async -> WalletServiceResult +} diff --git a/Adamant/Modules/Wallets/ERC20/ERC20GasAlgorithm.swift b/Adamant/Modules/Wallets/ERC20/ERC20GasAlgorithm.swift new file mode 100644 index 000000000..49a9232d2 --- /dev/null +++ b/Adamant/Modules/Wallets/ERC20/ERC20GasAlgorithm.swift @@ -0,0 +1,40 @@ +// +// ERC20GasAlgorithm.swift +// Adamant +// +// Created by Sergei Veretennikov on 28.02.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import BigInt +import Foundation + +protocol ERC20GasAlgorithmComputable { + var reliabilityGasPricePercent: BigUInt { get } + var reliabilityGasLimitPercent: BigUInt { get } + var increasedGasPricePercent: Decimal { get } +} + +extension ERC20GasAlgorithmComputable { + func updateGasAndFee( + gasPrice: BigUInt, + gasLimit: BigUInt, + gasPriceCoeficient: Decimal, + completion: @escaping (_ gasPrice: BigUInt, _ gasLimit: BigUInt, _ newFee: Decimal) -> Void + ) { + let reliabilityGasPricePercent = gasPrice / reliabilityGasPricePercent + let reliabilityGasLimitPercent = gasLimit / reliabilityGasLimitPercent + + let reliableGasPrice = reliabilityGasPricePercent + gasPrice + let reliableGasLimit = reliabilityGasLimitPercent + gasLimit + + let finalGasPrice = BigUInt(reliableGasPrice.asDouble() * gasPriceCoeficient.doubleValue) + let newFee = (finalGasPrice * reliableGasLimit).asDecimal(exponent: EthWalletService.currencyExponent) + + completion( + finalGasPrice, + reliableGasLimit, + newFee + ) + } +} diff --git a/Adamant/Modules/Wallets/ERC20/ERC20TransactionDetailsViewController.swift b/Adamant/Modules/Wallets/ERC20/ERC20TransactionDetailsViewController.swift index 334d8dcee..879c2196e 100644 --- a/Adamant/Modules/Wallets/ERC20/ERC20TransactionDetailsViewController.swift +++ b/Adamant/Modules/Wallets/ERC20/ERC20TransactionDetailsViewController.swift @@ -6,59 +6,59 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UIKit -import CommonKit import Combine +import CommonKit +import UIKit @MainActor final class ERC20TransactionDetailsViewController: TransactionDetailsViewControllerBase { // MARK: - Dependencies - + weak var service: ERC20WalletService? { walletService?.core as? ERC20WalletService } - + // MARK: - Properties - + private let autoupdateInterval: TimeInterval = 5.0 private var timerSubscription: AnyCancellable? override var feeFormatter: NumberFormatter { return AdamantBalanceFormat.currencyFormatter(for: .full, currencySymbol: EthWalletService.currencySymbol) } - + override var feeCurrencySymbol: String? { EthWalletService.currencySymbol } - + private lazy var refreshControl: UIRefreshControl = { let control = UIRefreshControl() control.tintColor = .adamant.primary control.addTarget(self, action: #selector(refresh), for: UIControl.Event.valueChanged) return control }() - + // MARK: - Lifecycle - + override func viewDidLoad() { currencySymbol = service?.tokenSymbol - + super.viewDidLoad() - + if service != nil { tableView.refreshControl = refreshControl } - + startUpdate() } - + // MARK: - Overrides - + override func explorerUrl(for transaction: TransactionDetails) -> URL? { let id = transaction.txId - + return URL(string: "\(EthWalletService.explorerTx)\(id)") } - + @MainActor @objc func refresh(silent: Bool = false) { refreshTask = Task { @@ -66,7 +66,7 @@ final class ERC20TransactionDetailsViewController: TransactionDetailsViewControl refreshControl.endRefreshing() return } - + do { let trs = try await service.getTransaction(by: id, waitsForConnectivity: false) transaction = trs @@ -76,18 +76,19 @@ final class ERC20TransactionDetailsViewController: TransactionDetailsViewControl } catch { refreshControl.endRefreshing() updateTransactionStatus() - + guard !silent else { return } dialogService.showRichError(error: error) } } } - + // MARK: - Autoupdate - + func startUpdate() { refresh(silent: true) - timerSubscription = Timer + timerSubscription = + Timer .publish(every: autoupdateInterval, on: .main, in: .common) .autoconnect() .sink { [weak self] _ in diff --git a/Adamant/Modules/Wallets/ERC20/ERC20TransactionsViewController.swift b/Adamant/Modules/Wallets/ERC20/ERC20TransactionsViewController.swift index 1fc96b1d3..aa6561652 100644 --- a/Adamant/Modules/Wallets/ERC20/ERC20TransactionsViewController.swift +++ b/Adamant/Modules/Wallets/ERC20/ERC20TransactionsViewController.swift @@ -6,33 +6,33 @@ // Copyright © 2019 Adamant. All rights reserved. // +import CommonKit import UIKit import web3swift -import CommonKit final class ERC20TransactionsViewController: TransactionsListViewControllerBase { - + // MARK: - UITableView - + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let address = walletService.core.wallet?.address, - let transaction = transactions[safe: indexPath.row] + let transaction = transactions[safe: indexPath.row] else { return } - + tableView.deselectRow(at: indexPath, animated: true) - + let vc = screensFactory.makeDetailsVC(service: walletService) - + vc.transaction = transaction - + if transaction.senderAddress.caseInsensitiveCompare(address) == .orderedSame { vc.senderName = String.adamant.transactionDetails.yourAddress } - + if transaction.recipientAddress.caseInsensitiveCompare(address) == .orderedSame { vc.recipientName = String.adamant.transactionDetails.yourAddress } - + navigationController?.pushViewController(vc, animated: true) } } diff --git a/Adamant/Modules/Wallets/ERC20/ERC20TransferViewController.swift b/Adamant/Modules/Wallets/ERC20/ERC20TransferViewController.swift index 7fff25808..00571e7de 100644 --- a/Adamant/Modules/Wallets/ERC20/ERC20TransferViewController.swift +++ b/Adamant/Modules/Wallets/ERC20/ERC20TransferViewController.swift @@ -6,26 +6,26 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UIKit +import CommonKit import Eureka +import UIKit @preconcurrency import Web3Core -import CommonKit final class ERC20TransferViewController: TransferViewControllerBase { - + // MARK: Properties - + private var skipValueChange: Bool = false private let prefix = "0x" - + override var feeBalanceFormatter: NumberFormatter { return AdamantBalanceFormat.currencyFormatter(for: .full, currencySymbol: EthWalletService.currencySymbol) } - + override var isNeedAddFee: Bool { false } - + // MARK: Send - + @MainActor override func sendFunds() { let comments: String @@ -34,16 +34,16 @@ final class ERC20TransferViewController: TransferViewControllerBase { } else { comments = "" } - + guard let service = walletCore as? ERC20WalletService, - let recipient = recipientAddress, - let amount = amount + let recipient = recipientAddress, + let amount = amount else { return } - + dialogService.showProgress(withMessage: String.adamant.transfer.transferProcessingMessage, userInteractionEnable: false) - + Task { do { // Create transaction @@ -53,7 +53,7 @@ final class ERC20TransferViewController: TransferViewControllerBase { fee: transactionFee, comment: nil ) - + if await !doesNotContainSendingTx( with: Int(transaction.nonce), id: transaction.txHash, @@ -62,14 +62,14 @@ final class ERC20TransferViewController: TransferViewControllerBase { presentSendingError() return } - + guard let txHash = transaction.txHash else { throw WalletServiceError.internalError( message: "Transaction making failure", error: nil ) } - + // Send adm report if let reportRecipient = admReportRecipient { try await reportTransferTo( @@ -79,7 +79,7 @@ final class ERC20TransferViewController: TransferViewControllerBase { hash: txHash ) } - + do { try await service.sendTransaction(transaction) } catch { @@ -87,17 +87,17 @@ final class ERC20TransferViewController: TransferViewControllerBase { for: txHash, status: .failed ) - + throw error } - + Task { await service.update() } - + dialogService.dismissProgress() dialogService.showSuccess(withMessage: String.adamant.transfer.transferSuccess) - + // Present detail VC presentDetailTransactionVC( hash: txHash, @@ -113,7 +113,7 @@ final class ERC20TransferViewController: TransferViewControllerBase { } } } - + private func presentDetailTransactionVC( hash: String, transaction: CodableTransaction, @@ -131,63 +131,63 @@ final class ERC20TransferViewController: TransferViewControllerBase { confirmationsValue: nil, blockValue: nil, isOutgoing: true, - transactionStatus: nil, + transactionStatus: nil, nonceRaw: nil ) - + service.core.coinStorage.append(transaction) - let detailsVc = screensFactory.makeDetailsVC(service: service) + let detailsVc = screensFactory.makeDetailsVC(service: service) detailsVc.transaction = transaction detailsVc.senderName = String.adamant.transactionDetails.yourAddress detailsVc.recipientName = recipientName - + if comments.count > 0 { detailsVc.comment = comments } - + delegate?.transferViewController( self, didFinishWithTransfer: transaction, detailsViewController: detailsVc ) } - + private func doesNotContainSendingTx( with nonce: Int, id: String?, service: ERC20WalletService ) async -> Bool { var history = service.getLocalTransactionHistory() - + if history.isEmpty { history = (try? await service.getTransactionsHistory(offset: .zero, limit: 2)) ?? [] } - + let pendingTx = history.first(where: { $0.transactionStatus == .pending - || $0.transactionStatus == .registered - || $0.transactionStatus == .notInitiated - || $0.txId == id + || $0.transactionStatus == .registered + || $0.transactionStatus == .notInitiated + || $0.txId == id }) - + guard let pendingTx = pendingTx else { return true } - + guard let detailTx = try? await service.getTransaction(by: pendingTx.txId, waitsForConnectivity: false) else { return false } - + return detailTx.nonce != nonce } - + // MARK: Overrides - + override var recipientAddress: String? { set { let _recipient = newValue?.addPrefixIfNeeded(prefix: prefix) - + if let row: TextRow = form.rowBy(tag: BaseRows.address.tag) { row.value = _recipient row.updateCell() @@ -198,12 +198,12 @@ final class ERC20TransferViewController: TransferViewControllerBase { return row?.value?.addPrefixIfNeeded(prefix: prefix) } } - + override func validateRecipient(_ address: String) -> AddressValidationResult { let fixedAddress = address.addPrefixIfNeeded(prefix: prefix) return walletCore.validate(address: fixedAddress) } - + override func recipientRow() -> BaseRow { let row = TextRow { $0.tag = BaseRows.address.tag @@ -211,98 +211,99 @@ final class ERC20TransferViewController: TransferViewControllerBase { $0.cell.textField.keyboardType = .namePhonePad $0.cell.textField.autocorrectionType = .no $0.cell.textField.setLineBreakMode() - + if let recipient = recipientAddress { let trimmed = recipient.components(separatedBy: TransferViewControllerBase.invalidCharacters).joined() $0.value = trimmed } - + let prefixLabel = UILabel() prefixLabel.text = prefix prefixLabel.sizeToFit() - + let view = UIView() view.addSubview(prefixLabel) view.frame = prefixLabel.frame $0.cell.textField.leftView = view $0.cell.textField.leftViewMode = .always - + if recipientIsReadonly { $0.disabled = true prefixLabel.textColor = UIColor.lightGray } - }.cellUpdate { [weak self] (cell, _) in - if let text = cell.textField.text { - cell.textField.text = text.components(separatedBy: TransferViewControllerBase.invalidCharacters).joined() - - guard self?.recipientIsReadonly == false else { return } - - cell.textField.leftView?.subviews.forEach { view in - guard let label = view as? UILabel else { return } - label.textColor = UIColor.adamant.primary - } + }.cellUpdate { [weak self] (cell, _) in + if let text = cell.textField.text { + cell.textField.text = text.components(separatedBy: TransferViewControllerBase.invalidCharacters).joined() + + guard self?.recipientIsReadonly == false else { return } + + cell.textField.leftView?.subviews.forEach { view in + guard let label = view as? UILabel else { return } + label.textColor = UIColor.adamant.primary } - }.onChange { [weak self] row in - if let skip = self?.skipValueChange, skip { - self?.skipValueChange = false - return + } + }.onChange { [weak self] row in + if let skip = self?.skipValueChange, skip { + self?.skipValueChange = false + return + } + + if let text = row.value { + var trimmed = text.components(separatedBy: TransferViewControllerBase.invalidCharacters).joined() + if let prefix = self?.prefix, + trimmed.starts(with: prefix) + { + let i = trimmed.index(trimmed.startIndex, offsetBy: prefix.count) + trimmed = String(trimmed[i...]) } - - if let text = row.value { - var trimmed = text.components(separatedBy: TransferViewControllerBase.invalidCharacters).joined() - if let prefix = self?.prefix, - trimmed.starts(with: prefix) { - let i = trimmed.index(trimmed.startIndex, offsetBy: prefix.count) - trimmed = String(trimmed[i...]) - } - - if text != trimmed { - self?.skipValueChange = true - - DispatchQueue.main.async { - row.value = trimmed - row.updateCell() - } + + if text != trimmed { + self?.skipValueChange = true + + DispatchQueue.main.async { + row.value = trimmed + row.updateCell() } } - self?.updateToolbar(for: row) + } + self?.updateToolbar(for: row) }.onCellSelection { [weak self] (cell, _) in self?.shareValue(self?.recipientAddress, from: cell) } - + return row } - + override func defaultSceneTitle() -> String? { let networkSymbol = ERC20WalletService.tokenNetworkSymbol return String.adamant.wallets.erc20.sendToken(walletCore.tokenSymbol) + " (\(networkSymbol))" } - + override func validateAmount(_ amount: Decimal, withFee: Bool = true) -> Bool { guard amount > 0 else { return false } - + guard let balance = walletCore.wallet?.balance else { return false } - + let minAmount = walletCore.minAmount guard minAmount <= amount else { return false } - + let isEnoughBalance = balance >= amount let isEnoughFee = isEnoughFee() - + return isEnoughBalance && isEnoughFee } - + override func isEnoughFee() -> Bool { guard let rootCoinBalance = rootCoinBalance, - rootCoinBalance >= walletCore.diplayTransactionFee, - walletCore.isTransactionFeeValid + rootCoinBalance >= walletCore.diplayTransactionFee, + walletCore.isTransactionFeeValid else { return false } diff --git a/Adamant/Modules/Wallets/ERC20/ERC20Wallet.swift b/Adamant/Modules/Wallets/ERC20/ERC20Wallet.swift index 52b921281..9a96eba30 100644 --- a/Adamant/Modules/Wallets/ERC20/ERC20Wallet.swift +++ b/Adamant/Modules/Wallets/ERC20/ERC20Wallet.swift @@ -6,23 +6,23 @@ // Copyright © 2019 Adamant. All rights reserved. // +import CommonKit import Foundation -import web3swift import Web3Core -import CommonKit +import web3swift final class ERC20Wallet: WalletAccount, @unchecked Sendable { let unicId: String let address: String let ethAddress: EthereumAddress let keystore: BIP32Keystore - + @Atomic var balance: Decimal = 0 @Atomic var notifications: Int = 0 @Atomic var minBalance: Decimal = 0 @Atomic var minAmount: Decimal = 0 @Atomic var isBalanceInitialized: Bool = false - + init( unicId: String, address: String, diff --git a/Adamant/Modules/Wallets/ERC20/ERC20WalletFactory.swift b/Adamant/Modules/Wallets/ERC20/ERC20WalletFactory.swift index f77913123..a8ba4b4f7 100644 --- a/Adamant/Modules/Wallets/ERC20/ERC20WalletFactory.swift +++ b/Adamant/Modules/Wallets/ERC20/ERC20WalletFactory.swift @@ -6,16 +6,16 @@ // Copyright © 2019 Adamant. All rights reserved. // -import Swinject import CommonKit +import Swinject import UIKit struct ERC20WalletFactory: WalletFactory { typealias Service = WalletService - + let typeSymbol: String = ERC20WalletService.richMessageType let assembler: Assembler - + func makeWalletVC(service: Service, screensFactory: ScreensFactory) -> WalletViewController { ERC20WalletViewController( dialogService: assembler.resolve(DialogService.self)!, @@ -26,7 +26,7 @@ struct ERC20WalletFactory: WalletFactory { service: service ) } - + func makeTransferListVC(service: Service, screensFactory: ScreensFactory) -> UIViewController { ERC20TransactionsViewController( walletService: service, @@ -35,7 +35,7 @@ struct ERC20WalletFactory: WalletFactory { screensFactory: screensFactory ) } - + func makeTransferVC(service: Service, screensFactory: ScreensFactory) -> TransferViewControllerBase { ERC20TransferViewController( chatsProvider: assembler.resolve(ChatsProvider.self)!, @@ -45,26 +45,26 @@ struct ERC20WalletFactory: WalletFactory { screensFactory: screensFactory, currencyInfoService: assembler.resolve(InfoServiceProtocol.self)!, increaseFeeService: assembler.resolve(IncreaseFeeService.self)!, - vibroService: assembler.resolve(VibroService.self)!, + vibroService: assembler.resolve(VibroService.self)!, walletService: service, reachabilityMonitor: assembler.resolve(ReachabilityMonitor.self)!, apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)! ) } - + func makeDetailsVC(service: Service, transaction: RichMessageTransaction) -> UIViewController? { guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) else { return nil } - + let comment: String? if let raw = transaction.getRichValue(for: RichContentKeys.transfer.comments), raw.count > 0 { comment = raw } else { comment = nil } - + // MARK: Go to transaction - + return makeTransactionDetailsVC( hash: hash, senderId: transaction.senderId, @@ -77,14 +77,14 @@ struct ERC20WalletFactory: WalletFactory { service: service ) } - + func makeDetailsVC(service: Service) -> TransactionDetailsViewControllerBase { makeTransactionDetailsVC(service: service) } } -private extension ERC20WalletFactory { - func makeTransactionDetailsVC( +extension ERC20WalletFactory { + fileprivate func makeTransactionDetailsVC( hash: String, senderId: String?, recipientId: String?, @@ -96,15 +96,16 @@ private extension ERC20WalletFactory { service: Service ) -> UIViewController { let vc = makeTransactionDetailsVC(service: service) - + let amount: Decimal if let amountRaw = richTransaction.getRichValue(for: RichContentKeys.transfer.amount), - let decimal = Decimal(string: amountRaw) { + let decimal = Decimal(string: amountRaw) + { amount = decimal } else { amount = 0 } - + let failedTransaction = SimpleTransactionDetails( txId: hash, senderAddress: senderAddress, @@ -118,7 +119,7 @@ private extension ERC20WalletFactory { transactionStatus: nil, nonceRaw: nil ) - + vc.senderId = senderId vc.recipientId = recipientId vc.comment = comment @@ -126,13 +127,13 @@ private extension ERC20WalletFactory { vc.richTransaction = richTransaction return vc } - - func makeTransactionDetailsVC(service: Service) -> ERC20TransactionDetailsViewController { + + fileprivate func makeTransactionDetailsVC(service: Service) -> ERC20TransactionDetailsViewController { ERC20TransactionDetailsViewController( dialogService: assembler.resolve(DialogService.self)!, currencyInfo: assembler.resolve(InfoServiceProtocol.self)!, addressBookService: assembler.resolve(AddressBookService.self)!, - accountService: assembler.resolve(AccountService.self)!, + accountService: assembler.resolve(AccountService.self)!, walletService: service, languageService: assembler.resolve(LanguageStorageProtocol.self)! ) diff --git a/Adamant/Modules/Wallets/ERC20/ERC20WalletService+RichMessageProvider.swift b/Adamant/Modules/Wallets/ERC20/ERC20WalletService+RichMessageProvider.swift index 79c665000..e40596298 100644 --- a/Adamant/Modules/Wallets/ERC20/ERC20WalletService+RichMessageProvider.swift +++ b/Adamant/Modules/Wallets/ERC20/ERC20WalletService+RichMessageProvider.swift @@ -6,55 +6,55 @@ // Copyright © 2019 Adamant. All rights reserved. // +import CommonKit import Foundation import MessageKit import UIKit -import CommonKit extension ERC20WalletService { var newPendingInterval: TimeInterval { .init(milliseconds: EthWalletService.newPendingInterval) } - + var oldPendingInterval: TimeInterval { .init(milliseconds: EthWalletService.oldPendingInterval) } - + var registeredInterval: TimeInterval { .init(milliseconds: EthWalletService.registeredInterval) } - + var newPendingAttempts: Int { EthWalletService.newPendingAttempts } - + var oldPendingAttempts: Int { EthWalletService.oldPendingAttempts } - + // MARK: Short description func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { let amount: String - + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount) else { return NSAttributedString(string: "⬅️ \(self.tokenSymbol)") } - + if let decimal = Decimal(string: raw) { amount = AdamantBalanceFormat.full.format(decimal) } else { amount = raw } - + let string: String if transaction.isOutgoing { string = "⬅️ \(amount) \(self.tokenSymbol)" } else { string = "➡️ \(amount) \(self.tokenSymbol)" } - + return NSAttributedString(string: string) } } diff --git a/Adamant/Modules/Wallets/ERC20/ERC20WalletService+RichMessageProviderWithStatusCheck.swift b/Adamant/Modules/Wallets/ERC20/ERC20WalletService+RichMessageProviderWithStatusCheck.swift index 67f0f826d..75139bf72 100644 --- a/Adamant/Modules/Wallets/ERC20/ERC20WalletService+RichMessageProviderWithStatusCheck.swift +++ b/Adamant/Modules/Wallets/ERC20/ERC20WalletService+RichMessageProviderWithStatusCheck.swift @@ -6,33 +6,34 @@ // Copyright © 2019 Adamant. All rights reserved. // +import CommonKit import Foundation import web3swift + import struct BigInt.BigUInt -import CommonKit extension ERC20WalletService { func statusInfoFor(transaction: CoinTransaction) async -> TransactionStatusInfo { let hash: String? - + if let transaction = transaction as? RichMessageTransaction { hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) } else { hash = transaction.txId } - + guard let hash = hash else { return .init(sentDate: nil, status: .inconsistent(.wrongTxHash)) } - + let erc20Transaction: EthTransaction - + do { erc20Transaction = try await getTransaction(by: hash, waitsForConnectivity: true) } catch { return .init(error: error) } - + return await .init( sentDate: erc20Transaction.date, status: getStatus( @@ -43,42 +44,42 @@ extension ERC20WalletService { } } -private extension ERC20WalletService { - func getStatus( +extension ERC20WalletService { + fileprivate func getStatus( erc20Transaction: EthTransaction, transaction: CoinTransaction ) async -> TransactionStatus { let status = erc20Transaction.receiptStatus.asTransactionStatus() guard status == .success else { return status } - + // MARK: Check addresses - + var realSenderAddress = erc20Transaction.senderAddress var realRecipientAddress = erc20Transaction.recipientAddress - + if transaction is RichMessageTransaction { guard let senderAddress = try? await getWalletAddress(byAdamantAddress: transaction.senderAddress) else { return .inconsistent(.senderCryptoAddressUnavailable(tokenSymbol)) } - + guard let recipientAddress = try? await getWalletAddress(byAdamantAddress: transaction.recipientAddress) else { return .inconsistent(.recipientCryptoAddressUnavailable(tokenSymbol)) } - + realSenderAddress = senderAddress realRecipientAddress = recipientAddress } - + guard erc20Transaction.senderAddress.caseInsensitiveCompare(realSenderAddress) == .orderedSame else { return .inconsistent(.senderCryptoAddressMismatch(tokenSymbol)) } - + guard erc20Transaction.recipientAddress.caseInsensitiveCompare(realRecipientAddress) == .orderedSame else { return .inconsistent(.recipientCryptoAddressMismatch(tokenSymbol)) } - + if transaction.isOutgoing { guard ethWallet?.address.caseInsensitiveCompare(erc20Transaction.senderAddress) == .orderedSame else { return .inconsistent(.senderCryptoAddressMismatch(tokenSymbol)) @@ -88,35 +89,35 @@ private extension ERC20WalletService { return .inconsistent(.recipientCryptoAddressMismatch(tokenSymbol)) } } - + // MARK: Compare amounts guard let reportedValue = reportedValue(for: transaction) else { return .inconsistent(.wrongAmount) } - - let min = reportedValue - reportedValue*0.005 - let max = reportedValue + reportedValue*0.005 - + + let min = reportedValue - reportedValue * 0.005 + let max = reportedValue + reportedValue * 0.005 + guard (min...max).contains(erc20Transaction.value ?? 0) else { return .inconsistent(.wrongAmount) } - + return .success } - - func reportedValue(for transaction: CoinTransaction) -> Decimal? { + + fileprivate func reportedValue(for transaction: CoinTransaction) -> Decimal? { guard let transaction = transaction as? RichMessageTransaction else { return transaction.amountValue } - + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), let reportedValue = AdamantBalanceFormat.deserializeBalance(from: raw) else { return nil } - + return reportedValue } } diff --git a/Adamant/Modules/Wallets/ERC20/ERC20WalletService+Send.swift b/Adamant/Modules/Wallets/ERC20/ERC20WalletService+Send.swift index c97665c41..12702f22b 100644 --- a/Adamant/Modules/Wallets/ERC20/ERC20WalletService+Send.swift +++ b/Adamant/Modules/Wallets/ERC20/ERC20WalletService+Send.swift @@ -6,15 +6,16 @@ // Copyright © 2019 Adamant. All rights reserved. // +import CommonKit import UIKit +@preconcurrency import Web3Core @preconcurrency import web3swift + import struct BigInt.BigUInt -@preconcurrency import Web3Core -import CommonKit extension ERC20WalletService: WalletServiceTwoStepSend { typealias T = CodableTransaction - + // MARK: Create & Send func createTransaction( recipient: String, @@ -25,23 +26,23 @@ extension ERC20WalletService: WalletServiceTwoStepSend { guard let ethWallet = ethWallet else { throw WalletServiceError.notLogged } - + guard let ethRecipient = EthereumAddress(recipient) else { throw WalletServiceError.accountNotFound } - + guard let keystoreManager = await erc20ApiService.keystoreManager else { throw WalletServiceError.internalError(message: "Failed to get web3.provider.KeystoreManager", error: nil) } - + let provider = try await erc20ApiService.requestWeb3( waitsForConnectivity: false ) { web3 in web3.provider }.get() - + let resolver = PolicyResolver(provider: provider) - + // MARK: Create transaction - + var tx = try await erc20ApiService.requestERC20(token: token) { erc20 in try await erc20.transfer( from: ethWallet.ethAddress, @@ -49,31 +50,31 @@ extension ERC20WalletService: WalletServiceTwoStepSend { amount: "\(amount)" ).transaction }.get() - + await calculateFee(for: ethRecipient) - + let policies: Policies = Policies( gasLimitPolicy: .manual(gasLimit), gasPricePolicy: .manual(gasPrice) ) - + try await resolver.resolveAll(for: &tx, with: policies) - + try Web3Signer.signTX( transaction: &tx, keystore: keystoreManager, account: ethWallet.ethAddress, password: ERC20WalletService.walletPassword ) - + return tx } - + func sendTransaction(_ transaction: CodableTransaction) async throws { guard let txEncoded = transaction.encode() else { throw WalletServiceError.internalError(message: .adamant.sharedErrors.unknownError, error: nil) } - + _ = try await erc20ApiService.requestWeb3(waitsForConnectivity: false) { web3 in try await web3.eth.send(raw: txEncoded) }.get() diff --git a/Adamant/Modules/Wallets/ERC20/ERC20WalletService.swift b/Adamant/Modules/Wallets/ERC20/ERC20WalletService.swift index 7c4a86c1a..42e422204 100644 --- a/Adamant/Modules/Wallets/ERC20/ERC20WalletService.swift +++ b/Adamant/Modules/Wallets/ERC20/ERC20WalletService.swift @@ -6,79 +6,76 @@ // Copyright © 2019 Adamant. All rights reserved. // +import Alamofire +import Combine +import CommonKit import Foundation -import UIKit import Swinject +import UIKit +@preconcurrency import Web3Core import web3swift -import Alamofire + @preconcurrency import struct BigInt.BigUInt -@preconcurrency import Web3Core -import Combine -import CommonKit -final class ERC20WalletService: WalletCoreProtocol, @unchecked Sendable { +final class ERC20WalletService: WalletCoreProtocol, ERC20GasAlgorithmComputable, @unchecked Sendable { // MARK: - Constants let addressRegex = try! NSRegularExpression(pattern: "^0x[a-fA-F0-9]{40}$") - - static let currencySymbol: String = "" - static let currencyLogo: UIImage = UIImage() - static let qqPrefix: String = "" - + var minBalance: Decimal = 0 var minAmount: Decimal = 0 - + var tokenSymbol: String { - return token.symbol + token.symbol } - + var tokenName: String { - return token.name + token.name } - + var tokenLogo: UIImage { - return token.logo + token.logo } - + static var tokenNetworkSymbol: String { - return "ERC20" + "ERC20" } - + var consistencyMaxTime: Double { - return 1200 + 1200 } - + var tokenContract: String { - return token.contractAddress + token.contractAddress } - - var tokenUnicID: String { + + var tokenUniqueID: String { Self.tokenNetworkSymbol + tokenSymbol + tokenContract } - + var defaultVisibility: Bool { - return token.defaultVisibility + token.defaultVisibility } - + var defaultOrdinalLevel: Int? { - return token.defaultOrdinalLevel + token.defaultOrdinalLevel } - + var richMessageType: String { - return Self.richMessageType - } + Self.richMessageType + } var qqPrefix: String { - return EthWalletService.qqPrefix - } + EthWalletService.qqPrefix + } var isSupportIncreaseFee: Bool { - return true + true } - + var isIncreaseFeeEnabled: Bool { - return increaseFeeService.isIncreaseFeeEnabled(for: tokenUnicID) + increaseFeeService.isIncreaseFeeEnabled(for: tokenUniqueID) } - + var nodeGroups: [NodeGroup] { [.eth] } @@ -86,67 +83,87 @@ final class ERC20WalletService: WalletCoreProtocol, @unchecked Sendable { var transferDecimals: Int { token.transferDecimals } - + var explorerAddress: String { EthWalletService.explorerAddress } - + private(set) var blockchainSymbol: String = "ETH" private(set) var isDynamicFee: Bool = true private(set) var transactionFee: Decimal = 0.0 private(set) var gasPrice: BigUInt = 0 private(set) var gasLimit: BigUInt = 0 private(set) var isWarningGasPrice = false - + var isTransactionFeeValid: Bool { - return ethWallet?.balance ?? 0 > transactionFee + ethWallet?.balance ?? 0 > transactionFee + } + + var reliabilityGasPricePercent: BigUInt { + BigUInt(token.reliabilityGasPricePercent) + } + + var reliabilityGasLimitPercent: BigUInt { + BigUInt(token.reliabilityGasLimitPercent) + } + + var increasedGasPricePercent: Decimal { + token.increasedGasPricePercent } - + static let transferGas: Decimal = 21000 static let kvsAddress = "eth:address" - + static let walletPath = "m/44'/60'/3'/1" static let walletPassword = "" - + // MARK: - Dependencies weak var accountService: AccountService? var apiService: AdamantApiServiceProtocol! - var erc20ApiService: ERC20ApiService! + var erc20ApiService: ERC20ApiServiceProtocol! var dialogService: DialogService! var increaseFeeService: IncreaseFeeService! var vibroService: VibroService! var coreDataStack: CoreDataStack! - + var ethBIP32Service: EthBIP32ServiceProtocol! + // MARK: - Notifications let walletUpdatedNotification: Notification.Name let serviceEnabledChanged: Notification.Name let transactionFeeUpdated: Notification.Name let serviceStateChanged: Notification.Name - + + @MainActor + private let walletUpdateSender = ObservableSender() + @MainActor + var walletUpdatePublisher: AnyObservable { + walletUpdateSender.eraseToAnyPublisher() + } + // MARK: RichMessageProvider properties static let richMessageType = "erc20_transaction" var dynamicRichMessageType: String { return "\(self.token.symbol.lowercased())_transaction" } - + // MARK: - Properties - + let token: ERC20Token @Atomic private(set) var enabled = true @Atomic private var subscriptions = Set() @Atomic private var cachedWalletAddress: [String: String] = [:] @Atomic private var balanceInvalidationSubscription: AnyCancellable? - + // MARK: - State @Atomic private(set) var state: WalletServiceState = .notInitiated - + private func setState(_ newState: WalletServiceState, silent: Bool = false) { guard newState != state else { return } - + state = newState - + if !silent { NotificationCenter.default.post( name: serviceStateChanged, @@ -155,51 +172,51 @@ final class ERC20WalletService: WalletCoreProtocol, @unchecked Sendable { ) } } - + private(set) var ethWallet: EthWallet? var wallet: WalletAccount? { return ethWallet } private var balanceObserver: NSObjectProtocol? - + @ObservableValue private(set) var historyTransactions: [TransactionDetails] = [] @ObservableValue private(set) var hasMoreOldTransactions: Bool = true var transactionsPublisher: AnyObservable<[TransactionDetails]> { $historyTransactions.eraseToAnyPublisher() } - + var hasMoreOldTransactionsPublisher: AnyObservable { $hasMoreOldTransactions.eraseToAnyPublisher() } - + @MainActor var hasEnabledNode: Bool { erc20ApiService.hasEnabledNode } - + @MainActor var hasEnabledNodePublisher: AnyObservable { erc20ApiService.hasEnabledNodePublisher } - + private(set) lazy var coinStorage: CoinStorageService = AdamantCoinStorageService( - coinId: tokenUnicID, + coinId: tokenUniqueID, coreDataStack: coreDataStack, blockchainType: dynamicRichMessageType ) - + init(token: ERC20Token) { self.token = token walletUpdatedNotification = Notification.Name("adamant.erc20Wallet.\(token.symbol).walletUpdated") serviceEnabledChanged = Notification.Name("adamant.erc20Wallet.\(token.symbol).enabledChanged") transactionFeeUpdated = Notification.Name("adamant.erc20Wallet.\(token.symbol).feeUpdated") serviceStateChanged = Notification.Name("adamant.erc20Wallet.\(token.symbol).stateChanged") - + self.setState(.notInitiated) - + // Notifications addObservers() } - + func addObservers() { NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedIn, object: nil) @@ -207,7 +224,7 @@ final class ERC20WalletService: WalletCoreProtocol, @unchecked Sendable { self?.update() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedOut, object: nil) .sink { @MainActor [weak self] _ in @@ -223,7 +240,7 @@ final class ERC20WalletService: WalletCoreProtocol, @unchecked Sendable { } .store(in: &subscriptions) } - + func addTransactionObserver() { coinStorage.transactionsPublisher .sink { [weak self] transactions in @@ -231,103 +248,107 @@ final class ERC20WalletService: WalletCoreProtocol, @unchecked Sendable { } .store(in: &subscriptions) } - + func update() { Task { await update() } } - + @MainActor func update() async { guard let wallet = ethWallet else { return } - + switch state { case .notInitiated, .updating, .initiationFailed: return - + case .upToDate: break } - + setState(.updating) - + if let balance = try? await getBalance(forAddress: wallet.ethAddress) { if wallet.balance < balance, wallet.isBalanceInitialized { vibroService.applyVibration(.success) } - + wallet.balance = balance markBalanceAsFresh(wallet) - - NotificationCenter.default.post( - name: walletUpdatedNotification, - object: self, - userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet] - ) + + walletUpdateSender.send() + } else { + wallet.isBalanceInitialized = false } - + + NotificationCenter.default.post( + name: walletUpdatedNotification, + object: self, + userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet] + ) + setState(.upToDate) - + await calculateFee() } - + func calculateFee(for address: EthereumAddress? = nil) async { - let priceRaw = try? await getGasPrices() - let gasLimitRaw = try? await getGasLimit(to: address) - - var price = priceRaw ?? BigUInt(token.defaultGasPriceGwei).toWei() - var gasLimit = gasLimitRaw ?? BigUInt(token.defaultGasLimit) - - let pricePercent = price * BigUInt(token.reliabilityGasPricePercent) / 100 - let gasLimitPercent = gasLimit * BigUInt(token.reliabilityGasLimitPercent) / 100 - - price = priceRaw == nil - ? price - : price + pricePercent - - gasLimit = gasLimitRaw == nil - ? gasLimit - : gasLimit + gasLimitPercent - - var newFee = (price * gasLimit).asDecimal(exponent: EthWalletService.currencyExponent) - - newFee = isIncreaseFeeEnabled - ? newFee * defaultIncreaseFee - : newFee - - guard transactionFee != newFee else { return } - - transactionFee = newFee - let incGasPrice = UInt64(price.asDouble() * defaultIncreaseFee.doubleValue) - - gasPrice = isIncreaseFeeEnabled - ? BigUInt(integerLiteral: incGasPrice) - : price - - isWarningGasPrice = gasPrice >= BigUInt(token.warningGasPriceGwei).toWei() - self.gasLimit = gasLimit - - NotificationCenter.default.post(name: transactionFeeUpdated, object: self, userInfo: nil) - } - + // Setting initial + async let pricePriceAsync = getGasPrices() + async let gasLimitAsync = getGasLimit(to: address) + var gasPriceCoeficient: Decimal = 1 + if isIncreaseFeeEnabled { + gasPriceCoeficient += token.increasedGasPricePercent / 100 + } + + let gasPrice: BigUInt + let gasLimit: BigUInt + + // Getting gas data + do { + let (gasPriceFromChain, gasLimitFromChain) = try await (pricePriceAsync, gasLimitAsync) + try Task.checkCancellation() + gasPrice = gasPriceFromChain + gasLimit = gasLimitFromChain + } catch { + gasPrice = BigUInt(token.defaultGasPriceGwei).toWei() + gasLimit = BigUInt(token.defaultGasLimit) + } + + // Updating localy + updateGasAndFee( + gasPrice: gasPrice, + gasLimit: gasLimit, + gasPriceCoeficient: gasPriceCoeficient + ) { [weak self] gasPrice, gasLimit, newFee in + guard let self else { return } + self.gasPrice = gasPrice + self.gasLimit = gasLimit + guard transactionFee != newFee else { return } + transactionFee = newFee + isWarningGasPrice = gasPrice >= BigUInt(token.warningGasPriceGwei).toWei() + NotificationCenter.default.post(name: transactionFeeUpdated, object: self, userInfo: nil) + } + } + func validate(address: String) -> AddressValidationResult { return addressRegex.perfectMatch(with: address) ? .valid : .invalid(description: nil) } - + func getGasPrices() async throws -> BigUInt { try await erc20ApiService.requestWeb3(waitsForConnectivity: false) { web3 in try await web3.eth.gasPrice() }.get() } - + func getGasLimit(to address: EthereumAddress?) async throws -> BigUInt { guard let ethWallet = ethWallet else { throw WalletServiceError.internalError(message: "Can't get ethWallet service", error: nil) } - + let transaction = try await erc20ApiService.requestERC20(token: token) { erc20 in try await erc20.transfer( from: ethWallet.ethAddress, @@ -335,86 +356,78 @@ final class ERC20WalletService: WalletCoreProtocol, @unchecked Sendable { amount: "\(ethWallet.balance)" ).transaction }.get() - + return try await erc20ApiService.requestWeb3(waitsForConnectivity: false) { web3 in try await web3.eth.estimateGas(for: transaction) }.get() } - + private func markBalanceAsFresh(_ wallet: EthWallet) { wallet.isBalanceInitialized = true - + balanceInvalidationSubscription = Task { [weak self] in try await Task.sleep(interval: Self.balanceLifetime, pauseInBackground: true) guard let self else { return } wallet.isBalanceInitialized = false - + NotificationCenter.default.post( name: walletUpdatedNotification, object: self, userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet] ) + + await self.walletUpdateSender.send() }.eraseToAnyCancellable() } } // MARK: - WalletInitiatedWithPassphrase extension ERC20WalletService { - func initWallet(withPassphrase passphrase: String) async throws -> WalletAccount { - + func initWallet(withPassphrase passphrase: String, withPassword password: String) async throws -> WalletAccount { + // MARK: 1. Prepare setState(.notInitiated) - + if enabled { enabled = false NotificationCenter.default.post(name: serviceEnabledChanged, object: self) } - - // MARK: 2. Create keys and addresses - let keystore: BIP32Keystore - do { - guard let store = try BIP32Keystore(mnemonics: passphrase, password: EthWalletService.walletPassword, mnemonicsPassword: "", language: .english, prefixPath: EthWalletService.walletPath) else { - throw WalletServiceError.internalError(message: "ETH Wallet: failed to create Keystore", error: nil) - } - - keystore = store - } catch { - throw WalletServiceError.internalError(message: "ETH Wallet: failed to create Keystore", error: error) - } - - await erc20ApiService.setKeystoreManager(.init([keystore])) - + + let keystore = try await ethBIP32Service.keyStore(passphrase: passphrase) + guard let ethAddress = keystore.addresses?.first else { throw WalletServiceError.internalError(message: "ETH Wallet: failed to create Keystore", error: nil) } - + // MARK: 3. Update let eWallet = EthWallet( - unicId: tokenUnicID, + unicId: tokenUniqueID, address: ethAddress.address, ethAddress: ethAddress, keystore: keystore ) ethWallet = eWallet - + if !enabled { enabled = true NotificationCenter.default.post(name: serviceEnabledChanged, object: self) } - + NotificationCenter.default.post( name: walletUpdatedNotification, object: self, userInfo: [AdamantUserInfoKey.WalletService.wallet: eWallet] ) - + + await walletUpdateSender.send() + self.setState(.upToDate, silent: true) Task { await update() } return eWallet } - + func setInitiationFailed(reason: String) { setState(.initiationFailed(reason: reason)) ethWallet = nil @@ -432,7 +445,8 @@ extension ERC20WalletService: SwinjectDependentService { erc20ApiService = container.resolve(ERC20ApiService.self) vibroService = container.resolve(VibroService.self) coreDataStack = container.resolve(CoreDataStack.self) - + ethBIP32Service = container.resolve(EthBIP32ServiceProtocol.self) + addTransactionObserver() } } @@ -442,23 +456,23 @@ extension ERC20WalletService { func getTransaction(by hash: String, waitsForConnectivity: Bool) async throws -> EthTransaction { let sender = wallet?.address let isOutgoing: Bool - + // MARK: 1. Transaction details let details: Web3Core.TransactionDetails = try await erc20ApiService.requestWeb3( waitsForConnectivity: waitsForConnectivity ) { web3 in try await web3.eth.transactionDetails(hash) }.get() - + let receipt = try await erc20ApiService.requestWeb3( waitsForConnectivity: waitsForConnectivity ) { web3 in try await web3.eth.transactionReceipt(hash) }.get() - + // MARK: 3. Check if transaction is delivered guard receipt.status == .ok, - let blockNumber = details.blockNumber + let blockNumber = details.blockNumber else { let transaction = details.transaction.asEthTransaction( date: nil, @@ -471,36 +485,36 @@ extension ERC20WalletService { ) return transaction } - + // MARK: 4. Block timestamp & confirmations let currentBlock = try await erc20ApiService.requestWeb3( waitsForConnectivity: waitsForConnectivity ) { web3 in try await web3.eth.blockNumber() }.get() - + let block = try await erc20ApiService.requestWeb3( waitsForConnectivity: waitsForConnectivity ) { web3 in try await web3.eth.block(by: receipt.blockHash) }.get() - + guard currentBlock >= blockNumber else { throw WalletServiceError.remoteServiceError( message: "ERC20 confirmations calculating error" ) } - + let confirmations = currentBlock - blockNumber - + let transaction = details.transaction - + if let sender = sender { isOutgoing = transaction.sender?.address == sender } else { isOutgoing = false } - + let ethTransaction = transaction.asEthTransaction( date: block.timestamp, gasUsed: receipt.gasUsed, @@ -511,48 +525,57 @@ extension ERC20WalletService { isOutgoing: isOutgoing, for: self.token ) - + return ethTransaction } - + func getBalance(address: String) async throws -> Decimal { guard let address = EthereumAddress(address) else { throw WalletServiceError.internalError(message: "Incorrect address", error: nil) } - + return try await getBalance(forAddress: address) } - + func getBalance(forAddress address: EthereumAddress) async throws -> Decimal { let exponent = -token.naturalUnits - + let balance = try await erc20ApiService.requestERC20(token: token) { erc20 in try await erc20.getBalance(account: address) }.get() - + let value = balance.asDecimal(exponent: exponent) return value } - + func getWalletAddress(byAdamantAddress address: String) async throws -> String { if let address = cachedWalletAddress[address], !address.isEmpty { return address } - + let result = try await apiService.get(key: EthWalletService.kvsAddress, sender: address) .mapError { $0.asWalletServiceError() } .get() - + guard let result = result else { throw WalletServiceError.walletNotInitiated } - + cachedWalletAddress[address] = result - + return result } } +#if DEBUG + extension ERC20WalletService { + @available(*, deprecated, message: "For testing purposes only") + func setWalletForTests(_ wallet: EthWallet?) { + self.ethWallet = wallet + } + } +#endif + extension ERC20WalletService { func getTransactionsHistory( address: String, @@ -562,10 +585,11 @@ extension ERC20WalletService { guard let address = self.ethWallet?.address else { throw WalletServiceError.internalError(message: "Can't get address", error: nil) } - + // Request - let request = "(txto.eq.\(token.contractAddress),or(txfrom.eq.\(address.lowercased()),contract_to.eq.000000000000000000000000\(address.lowercased().replacingOccurrences(of: "0x", with: ""))))" - + let request = + "(txto.eq.\(token.contractAddress),or(txfrom.eq.\(address.lowercased()),contract_to.eq.000000000000000000000000\(address.lowercased().replacingOccurrences(of: "0x", with: ""))))" + // MARK: Request let txQueryParameters = [ "limit": String(limit), @@ -573,7 +597,7 @@ extension ERC20WalletService { "offset": String(offset), "order": "time.desc" ] - + var transactions: [EthTransactionShort] = try await erc20ApiService.requestApiCore(waitsForConnectivity: false) { core, origin in await core.sendRequestJsonResponse( origin: origin, @@ -583,44 +607,44 @@ extension ERC20WalletService { encoding: .url ) }.get() - + transactions.sort { $0.date.compare($1.date) == .orderedDescending } return transactions } - + func loadTransactions(offset: Int, limit: Int) async throws -> Int { let trs = try await getTransactionsHistory(offset: offset, limit: limit) - + guard trs.count > 0 else { hasMoreOldTransactions = false return .zero } - + coinStorage.append(trs) - + return trs.count } - - func getTransactionsHistory(offset: Int, limit: Int) async throws -> [TransactionDetails] { + + func getTransactionsHistory(offset: Int, limit: Int) async throws -> [TransactionDetails] { guard let address = wallet?.address else { throw WalletServiceError.accountNotFound } - + let trs = try await getTransactionsHistory( address: address, offset: offset, limit: limit ) - + guard trs.count > 0 else { hasMoreOldTransactions = false return [] } - + let newTrs = trs.map { transaction in let isOutgoing = transaction.from == address let exponent = -token.naturalUnits - + return SimpleTransactionDetails( txId: transaction.hash, senderAddress: transaction.from, @@ -635,14 +659,14 @@ extension ERC20WalletService { nonceRaw: nil ) } - + return newTrs } - + func getLocalTransactionHistory() -> [TransactionDetails] { historyTransactions } - + func updateStatus(for id: String, status: TransactionStatus?) { coinStorage.updateStatus(for: id, status: status) } diff --git a/Adamant/Modules/Wallets/ERC20/ERC20WalletViewController.swift b/Adamant/Modules/Wallets/ERC20/ERC20WalletViewController.swift index 050955703..af3546960 100644 --- a/Adamant/Modules/Wallets/ERC20/ERC20WalletViewController.swift +++ b/Adamant/Modules/Wallets/ERC20/ERC20WalletViewController.swift @@ -6,16 +6,16 @@ // Copyright © 2019 Adamant. All rights reserved. // +import CommonKit import Foundation import UIKit -import CommonKit extension String.adamant.wallets { enum erc20 { static func tokenWallet(_ token: String) -> String { return String(format: .localized("AccountTab.Wallets.erc20_wallet", comment: "Account tab: Ethereum wallet"), token) } - + static func sendToken(_ token: String) -> String { return String(format: .localized("AccountTab.Row.SendToken", comment: "Account tab: 'Send ERC20 tokens' button"), token) } @@ -30,7 +30,7 @@ final class ERC20WalletViewController: WalletViewControllerBase { let networkFont = currencyFont.withSize(8) let currencyAttributes: [NSAttributedString.Key: Any] = [.font: currencyFont] let networkAttributes: [NSAttributedString.Key: Any] = [.font: networkFont] - + let defaultString = NSMutableAttributedString( string: tokenSymbol, attributes: currencyAttributes @@ -39,16 +39,16 @@ final class ERC20WalletViewController: WalletViewControllerBase { string: " \(networkSymbol)", attributes: networkAttributes ) - + defaultString.append(underlineString) - + return defaultString } - + override func encodeForQr(address: String) -> String? { return "ethereum:\(address)" } - + override func setTitle() { walletTitleLabel.text = String.adamant.wallets.erc20.tokenWallet(service?.core.tokenName ?? "") } diff --git a/Adamant/Modules/Wallets/EthBIP32Service/EthBIP32Service.swift b/Adamant/Modules/Wallets/EthBIP32Service/EthBIP32Service.swift new file mode 100644 index 000000000..0973a6ad4 --- /dev/null +++ b/Adamant/Modules/Wallets/EthBIP32Service/EthBIP32Service.swift @@ -0,0 +1,45 @@ +// +// EthPIP32Service.swift +// Adamant +// +// Created by Владимир Клевцов on 30.1.25.. +// Copyright © 2025 Adamant. All rights reserved. +// +import Web3Core + +protocol EthBIP32ServiceProtocol { + func keyStore(passphrase: String) async throws -> BIP32Keystore +} + +actor EthBIP32Service: EthBIP32ServiceProtocol { + private var passphrase: String? + private var keystore: BIP32Keystore? + + private var ethApiService: EthApiServiceProtocol + + init(ethApiService: EthApiServiceProtocol) { + self.ethApiService = ethApiService + } + func keyStore(passphrase: String) async throws -> BIP32Keystore { + if let keystore = self.keystore, passphrase == self.passphrase { + return keystore + } + do { + guard + let store = try BIP32Keystore( + mnemonics: passphrase, + password: EthWalletService.walletPassword, + mnemonicsPassword: "", + language: .english, + prefixPath: EthWalletService.walletPath + ) + else { + throw WalletServiceError.internalError(message: "ETH Wallet: failed to create Keystore", error: nil) + } + self.passphrase = passphrase + self.keystore = store + await ethApiService.setKeystoreManager(.init([store])) + return store + } + } +} diff --git a/Adamant/Modules/Wallets/Ethereum/EthApiCore.swift b/Adamant/Modules/Wallets/Ethereum/EthApiCore.swift index ec3de54ab..5248e1e95 100644 --- a/Adamant/Modules/Wallets/Ethereum/EthApiCore.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthApiCore.swift @@ -8,14 +8,14 @@ import CommonKit import Foundation -@preconcurrency import web3swift import Web3Core +@preconcurrency import web3swift actor EthApiCore { let apiCore: APICoreProtocol private(set) var keystoreManager: KeystoreManager? private var web3Cache: [URL: Web3] = .init() - + func performRequest( origin: NodeOrigin, _ body: @escaping @Sendable (_ web3: Web3) async throws -> Success @@ -31,12 +31,12 @@ actor EthApiCore { return .failure(error) } } - + func setKeystoreManager(_ keystoreManager: KeystoreManager) { self.keystoreManager = keystoreManager web3Cache = .init() } - + init(apiCore: APICoreProtocol) { self.apiCore = apiCore } @@ -45,7 +45,7 @@ actor EthApiCore { extension EthApiCore: BlockchainHealthCheckableService { func getStatusInfo(origin: NodeOrigin) async -> WalletServiceResult { let startTimestamp = Date.now.timeIntervalSince1970 - + let response = await apiCore.sendRequestRPC( origin: origin, path: .empty, @@ -54,62 +54,66 @@ extension EthApiCore: BlockchainHealthCheckableService { .init(method: EthApiComand.clientVersionMethod) ] ) - + guard case let .success(data) = response else { return .failure(.internalError(.parsingFailed)) } - + let blockNumberData = data.first( where: { $0.id == EthApiComand.blockNumberMethod } ) let clientVersionData = data.first( where: { $0.id == EthApiComand.clientVersionMethod } ) - + guard let blockNumberData = blockNumberData, let clientVersionData = clientVersionData else { return .failure(.internalError(.parsingFailed)) } - + let blockNumber = String(decoding: blockNumberData.result, as: UTF8.self) let clientVersion = String(decoding: clientVersionData.result, as: UTF8.self) - + guard let height = hexStringToDouble(blockNumber) else { return .failure(.internalError(.parsingFailed)) } - - return .success(.init( - ping: Date.now.timeIntervalSince1970 - startTimestamp, - height: Int(height), - wsEnabled: false, - wsPort: nil, - version: extractVersion(from: clientVersion).flatMap { .init($0) } - )) + + return .success( + .init( + ping: Date.now.timeIntervalSince1970 - startTimestamp, + height: Int(height), + wsEnabled: false, + wsPort: nil, + version: extractVersion(from: clientVersion).flatMap { .init($0) } + ) + ) } } -private extension EthApiCore { - func getWeb3(origin: NodeOrigin) async -> WalletServiceResult { +extension EthApiCore { + fileprivate func getWeb3(origin: NodeOrigin) async -> WalletServiceResult { guard let url = origin.asURL() else { return .failure(.internalError(.endpointBuildFailed)) } - + if let web3 = web3Cache[url] { return .success(web3) } - + do { let web3 = try await Web3.new(url) web3.addKeystoreManager(keystoreManager) web3Cache[url] = web3 return .success(web3) } catch { - return .failure(.internalError( - message: error.localizedDescription, - error: error - )) + return .failure( + .internalError( + message: error.localizedDescription, + error: error + ) + ) } } } @@ -121,7 +125,7 @@ private func mapError(_ error: Error) -> WalletServiceError { return error.asWalletServiceError() } else if let error = error as? WalletServiceError { return error - } else if let _ = error as? URLError { + } else if error as? URLError != nil { return .networkError } else { return .remoteServiceError(message: error.localizedDescription) @@ -135,17 +139,17 @@ private struct EthApiComand { private func hexStringToDouble(_ hexString: String) -> UInt64? { let cleanString = hexString.replacingOccurrences(of: "0x", with: "") - + if let hexValue = UInt64(cleanString, radix: 16) { return hexValue } - + return nil } private func extractVersion(from input: String) -> String? { let pattern = #"^(.+?/v\d+\.\d+\.\d+).*?$"# - + let regex = try? NSRegularExpression(pattern: pattern, options: []) if let match = regex?.firstMatch(in: input, options: [], range: NSRange(location: 0, length: input.utf16.count)) { if let range = Range(match.range(at: 1), in: input) { diff --git a/Adamant/Modules/Wallets/Ethereum/EthApiService.swift b/Adamant/Modules/Wallets/Ethereum/EthApiService.swift index 4ff4671c0..a0feb90e8 100644 --- a/Adamant/Modules/Wallets/Ethereum/EthApiService.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthApiService.swift @@ -8,28 +8,28 @@ import CommonKit import Foundation -import web3swift @preconcurrency import Web3Core +import web3swift -class EthApiService: ApiServiceProtocol, @unchecked Sendable { +class EthApiService: EthApiServiceProtocol, @unchecked Sendable { let api: BlockchainHealthCheckWrapper - + var keystoreManager: KeystoreManager? { get async { await api.service.keystoreManager } } - + @MainActor var nodesInfoPublisher: AnyObservable { api.nodesInfoPublisher } - + @MainActor var nodesInfo: NodesListInfo { api.nodesInfo } - + func healthCheck() { api.healthCheck() } - + init(api: BlockchainHealthCheckWrapper) { self.api = api } - + func requestWeb3( waitsForConnectivity: Bool, _ request: @Sendable @escaping (Web3) async throws -> Output @@ -38,7 +38,7 @@ class EthApiService: ApiServiceProtocol, @unchecked Sendable { await core.performRequest(origin: origin, request) } } - + func requestApiCore( waitsForConnectivity: Bool, _ request: @Sendable @escaping (APICoreProtocol, NodeOrigin) async -> ApiServiceResult @@ -47,13 +47,13 @@ class EthApiService: ApiServiceProtocol, @unchecked Sendable { await request(core.apiCore, origin).mapError { $0.asWalletServiceError() } } } - + func getStatusInfo() async -> WalletServiceResult { await api.request(waitsForConnectivity: false) { core, origin in await core.getStatusInfo(origin: origin) } } - + func setKeystoreManager(_ keystoreManager: KeystoreManager) async { await api.service.setKeystoreManager(keystoreManager) } diff --git a/Adamant/Modules/Wallets/Ethereum/EthApiServiceProtocol.swift b/Adamant/Modules/Wallets/Ethereum/EthApiServiceProtocol.swift new file mode 100644 index 000000000..c6023d11f --- /dev/null +++ b/Adamant/Modules/Wallets/Ethereum/EthApiServiceProtocol.swift @@ -0,0 +1,25 @@ +// +// EthApiServiceProtocol.swift +// Adamant +// +// Created by Christian Benua on 16.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import CommonKit +@preconcurrency import Web3Core +import web3swift + +protocol EthApiServiceProtocol: ApiServiceProtocol { + func requestWeb3( + waitsForConnectivity: Bool, + _ request: @Sendable @escaping (Web3) async throws -> Output + ) async -> WalletServiceResult + + func requestApiCore( + waitsForConnectivity: Bool, + _ request: @Sendable @escaping (APICoreProtocol, NodeOrigin) async -> ApiServiceResult + ) async -> WalletServiceResult + + func setKeystoreManager(_ keystoreManager: KeystoreManager) async +} diff --git a/Adamant/Modules/Wallets/Ethereum/EthTransactionDetailsViewController.swift b/Adamant/Modules/Wallets/Ethereum/EthTransactionDetailsViewController.swift index de089bf48..0ba9b29ee 100644 --- a/Adamant/Modules/Wallets/Ethereum/EthTransactionDetailsViewController.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthTransactionDetailsViewController.swift @@ -6,61 +6,61 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import CommonKit import Combine +import CommonKit +import UIKit final class EthTransactionDetailsViewController: TransactionDetailsViewControllerBase { // MARK: - Dependencies - + weak var service: EthWalletService? { walletService?.core as? EthWalletService } - + // MARK: - Properties - + private let autoupdateInterval: TimeInterval = 5.0 private var timerSubscription: AnyCancellable? - + private lazy var refreshControl: UIRefreshControl = { let control = UIRefreshControl() control.tintColor = .adamant.primary control.addTarget(self, action: #selector(refresh), for: UIControl.Event.valueChanged) return control }() - + // MARK: - Lifecycle - + override func viewDidLoad() { currencySymbol = EthWalletService.currencySymbol - + super.viewDidLoad() - + if service != nil { tableView.refreshControl = refreshControl } - + startUpdate() } - + // MARK: - Overrides - + override func explorerUrl(for transaction: TransactionDetails) -> URL? { let id = transaction.txId - + return URL(string: "\(EthWalletService.explorerTx)\(id)") } - + @MainActor @objc func refresh(silent: Bool = false) { refreshTask = Task { guard let id = transaction?.txId, - let service = service + let service = service else { refreshControl.endRefreshing() return } - + do { let trs = try await service.getTransaction(by: id) transaction = trs @@ -76,12 +76,13 @@ final class EthTransactionDetailsViewController: TransactionDetailsViewControlle } } } - + // MARK: - Autoupdate - + func startUpdate() { refresh(silent: true) - timerSubscription = Timer + timerSubscription = + Timer .publish(every: autoupdateInterval, on: .main, in: .common) .autoconnect() .sink { [weak self] _ in diff --git a/Adamant/Modules/Wallets/Ethereum/EthTransactionsViewController.swift b/Adamant/Modules/Wallets/Ethereum/EthTransactionsViewController.swift index dbf9926ce..a56081d42 100644 --- a/Adamant/Modules/Wallets/Ethereum/EthTransactionsViewController.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthTransactionsViewController.swift @@ -6,32 +6,32 @@ // Copyright © 2018 Adamant. All rights reserved. // +import CommonKit import UIKit import web3swift -import CommonKit final class EthTransactionsViewController: TransactionsListViewControllerBase { - + // MARK: - UITableView - + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let address = walletService.core.wallet?.address, - let transaction = transactions[safe: indexPath.row] + let transaction = transactions[safe: indexPath.row] else { return } - + tableView.deselectRow(at: indexPath, animated: true) let vc = screensFactory.makeDetailsVC(service: walletService) - + vc.transaction = transaction - + if transaction.senderAddress.caseInsensitiveCompare(address) == .orderedSame { vc.senderName = String.adamant.transactionDetails.yourAddress } - + if transaction.recipientAddress.caseInsensitiveCompare(address) == .orderedSame { vc.recipientName = String.adamant.transactionDetails.yourAddress } - + navigationController?.pushViewController(vc, animated: true) } } diff --git a/Adamant/Modules/Wallets/Ethereum/EthTransferViewController.swift b/Adamant/Modules/Wallets/Ethereum/EthTransferViewController.swift index 1cd14b829..da556e2e7 100644 --- a/Adamant/Modules/Wallets/Ethereum/EthTransferViewController.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthTransferViewController.swift @@ -6,20 +6,20 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit +import CommonKit import Eureka +import UIKit @preconcurrency import Web3Core -import CommonKit final class EthTransferViewController: TransferViewControllerBase { - + // MARK: Properties - + private var skipValueChange: Bool = false private let prefix = "0x" - + // MARK: Send - + @MainActor override func sendFunds() { let comments: String @@ -28,16 +28,16 @@ final class EthTransferViewController: TransferViewControllerBase { } else { comments = "" } - + guard let service = walletCore as? EthWalletService, - let recipient = recipientAddress, - let amount = amount + let recipient = recipientAddress, + let amount = amount else { return } - + dialogService.showProgress(withMessage: String.adamant.transfer.transferProcessingMessage, userInteractionEnable: false) - + Task { do { // Create transaction @@ -47,7 +47,7 @@ final class EthTransferViewController: TransferViewControllerBase { fee: transactionFee, comment: nil ) - + if await !doesNotContainSendingTx( with: Int(transaction.nonce), id: transaction.txHash, @@ -56,14 +56,14 @@ final class EthTransferViewController: TransferViewControllerBase { presentSendingError() return } - + guard let txHash = transaction.txHash else { throw WalletServiceError.internalError( message: "Transaction making failure", error: nil ) } - + // Send adm report if let reportRecipient = admReportRecipient { try await reportTransferTo( @@ -73,7 +73,7 @@ final class EthTransferViewController: TransferViewControllerBase { hash: txHash ) } - + do { try await service.sendTransaction(transaction) } catch { @@ -81,17 +81,17 @@ final class EthTransferViewController: TransferViewControllerBase { for: txHash, status: .failed ) - + throw error } - + Task { await service.update() } - + dialogService.dismissProgress() dialogService.showSuccess(withMessage: String.adamant.transfer.transferSuccess) - + // Present detail VC presentDetailTransactionVC( hash: txHash, @@ -107,36 +107,36 @@ final class EthTransferViewController: TransferViewControllerBase { } } } - + private func doesNotContainSendingTx( with nonce: Int, id: String?, service: EthWalletService ) async -> Bool { var history = service.getLocalTransactionHistory() - + if history.isEmpty { history = (try? await service.getTransactionsHistory(offset: .zero, limit: 2)) ?? [] } - + let pendingTx = history.first(where: { $0.transactionStatus == .pending - || $0.transactionStatus == .registered - || $0.transactionStatus == .notInitiated - || $0.txId == id + || $0.transactionStatus == .registered + || $0.transactionStatus == .notInitiated + || $0.txId == id }) - + guard let pendingTx = pendingTx else { return true } - + guard let detailTx = try? await service.getTransaction(by: pendingTx.txId) else { return false } - + return detailTx.nonce != nonce } - + private func presentDetailTransactionVC( hash: String, transaction: CodableTransaction, @@ -154,29 +154,29 @@ final class EthTransferViewController: TransferViewControllerBase { confirmationsValue: nil, blockValue: nil, isOutgoing: true, - transactionStatus: nil, + transactionStatus: nil, nonceRaw: nil ) - + service.core.coinStorage.append(transaction) - let detailsVc = screensFactory.makeDetailsVC(service: service) + let detailsVc = screensFactory.makeDetailsVC(service: service) detailsVc.transaction = transaction detailsVc.senderName = String.adamant.transactionDetails.yourAddress detailsVc.recipientName = recipientName - + if comments.count > 0 { detailsVc.comment = comments } - + delegate?.transferViewController( self, didFinishWithTransfer: transaction, detailsViewController: detailsVc ) } - + // MARK: Overrides - + override var recipientAddress: String? { set { let _recipient = newValue?.addPrefixIfNeeded(prefix: prefix) @@ -190,34 +190,34 @@ final class EthTransferViewController: TransferViewControllerBase { return row?.value?.addPrefixIfNeeded(prefix: prefix) } } - + override func validateRecipient(_ address: String) -> AddressValidationResult { let fixedAddress = address.addPrefixIfNeeded(prefix: prefix) return walletCore.validate(address: fixedAddress) } - + override func recipientRow() -> BaseRow { let row = TextRow { $0.tag = BaseRows.address.tag $0.cell.textField.placeholder = String.adamant.newChat.addressPlaceholder $0.cell.textField.keyboardType = .namePhonePad $0.cell.textField.autocorrectionType = .no - + $0.value = recipientAddress?.components( separatedBy: TransferViewControllerBase.invalidCharacters ).joined() - + let prefixLabel = UILabel() prefixLabel.text = prefix prefixLabel.sizeToFit() - + let view = UIView() view.addSubview(prefixLabel) view.frame = prefixLabel.frame $0.cell.textField.leftView = view $0.cell.textField.leftViewMode = .always $0.cell.textField.setLineBreakMode() - + if recipientIsReadonly { $0.disabled = true prefixLabel.textColor = UIColor.lightGray @@ -227,7 +227,7 @@ final class EthTransferViewController: TransferViewControllerBase { cell.textField.text = text.components(separatedBy: TransferViewControllerBase.invalidCharacters).joined() guard self?.recipientIsReadonly == false else { return } - + cell.textField.leftView?.subviews.forEach { view in guard let label = view as? UILabel else { return } label.textColor = UIColor.adamant.primary @@ -238,21 +238,22 @@ final class EthTransferViewController: TransferViewControllerBase { self?.skipValueChange = false return } - + if let text = row.value { var trimmed = text.components( separatedBy: TransferViewControllerBase.invalidCharacters ).joined() - + if let prefix = self?.prefix, - trimmed.starts(with: prefix) { + trimmed.starts(with: prefix) + { let i = trimmed.index(trimmed.startIndex, offsetBy: prefix.count) trimmed = String(trimmed[i...]) } - + if text != trimmed { self?.skipValueChange = true - + DispatchQueue.main.async { row.value = trimmed row.updateCell() @@ -263,10 +264,10 @@ final class EthTransferViewController: TransferViewControllerBase { }.onCellSelection { [weak self] (cell, _) in self?.shareValue(self?.recipientAddress, from: cell) } - + return row } - + override func defaultSceneTitle() -> String? { return String.adamant.wallets.sendEth } diff --git a/Adamant/Modules/Wallets/Ethereum/EthWallet.swift b/Adamant/Modules/Wallets/Ethereum/EthWallet.swift index b87bd8c25..b84da24b9 100644 --- a/Adamant/Modules/Wallets/Ethereum/EthWallet.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthWallet.swift @@ -6,23 +6,23 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation -import web3swift import CommonKit +import Foundation @preconcurrency import Web3Core +import web3swift final class EthWallet: WalletAccount, @unchecked Sendable { let unicId: String let address: String let ethAddress: EthereumAddress let keystore: BIP32Keystore - + @Atomic var balance: Decimal = 0 @Atomic var notifications: Int = 0 @Atomic var minBalance: Decimal = 0 @Atomic var minAmount: Decimal = 0 @Atomic var isBalanceInitialized: Bool = false - + init( unicId: String, address: String, diff --git a/Adamant/Modules/Wallets/Ethereum/EthWalletFactory.swift b/Adamant/Modules/Wallets/Ethereum/EthWalletFactory.swift index 51b00bc72..d8381f1bd 100644 --- a/Adamant/Modules/Wallets/Ethereum/EthWalletFactory.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthWalletFactory.swift @@ -6,16 +6,16 @@ // Copyright © 2018 Adamant. All rights reserved. // +import CommonKit import Swinject import UIKit -import CommonKit struct EthWalletFactory: WalletFactory { typealias Service = WalletService - + let typeSymbol: String = EthWalletService.richMessageType let assembler: Assembler - + func makeWalletVC(service: Service, screensFactory: ScreensFactory) -> WalletViewController { EthWalletViewController( dialogService: assembler.resolve(DialogService.self)!, @@ -26,7 +26,7 @@ struct EthWalletFactory: WalletFactory { service: service ) } - + func makeTransferListVC(service: Service, screensFactory: ScreensFactory) -> UIViewController { EthTransactionsViewController( walletService: service, @@ -35,7 +35,7 @@ struct EthWalletFactory: WalletFactory { screensFactory: screensFactory ) } - + func makeTransferVC(service: Service, screensFactory: ScreensFactory) -> TransferViewControllerBase { EthTransferViewController( chatsProvider: assembler.resolve(ChatsProvider.self)!, @@ -45,24 +45,24 @@ struct EthWalletFactory: WalletFactory { screensFactory: screensFactory, currencyInfoService: assembler.resolve(InfoServiceProtocol.self)!, increaseFeeService: assembler.resolve(IncreaseFeeService.self)!, - vibroService: assembler.resolve(VibroService.self)!, + vibroService: assembler.resolve(VibroService.self)!, walletService: service, reachabilityMonitor: assembler.resolve(ReachabilityMonitor.self)!, apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)! ) } - + func makeDetailsVC(service: Service, transaction: RichMessageTransaction) -> UIViewController? { guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) else { return nil } - + let comment: String? if let raw = transaction.getRichValue(for: RichContentKeys.transfer.comments), raw.count > 0 { comment = raw } else { comment = nil } - + return makeTransactionDetailsVC( hash: hash, senderId: transaction.senderId, @@ -75,14 +75,14 @@ struct EthWalletFactory: WalletFactory { service: service ) } - + func makeDetailsVC(service: Service) -> TransactionDetailsViewControllerBase { makeTransactionDetailsVC(service: service) } } -private extension EthWalletFactory { - func makeTransactionDetailsVC( +extension EthWalletFactory { + fileprivate func makeTransactionDetailsVC( hash: String, senderId: String?, recipientId: String?, @@ -94,15 +94,16 @@ private extension EthWalletFactory { service: Service ) -> UIViewController { let vc = makeTransactionDetailsVC(service: service) - + let amount: Decimal if let amountRaw = richTransaction.getRichValue(for: RichContentKeys.transfer.amount), - let decimal = Decimal(string: amountRaw) { + let decimal = Decimal(string: amountRaw) + { amount = decimal } else { amount = 0 } - + let failedTransaction = SimpleTransactionDetails( txId: hash, senderAddress: senderAddress, @@ -113,10 +114,10 @@ private extension EthWalletFactory { confirmationsValue: nil, blockValue: nil, isOutgoing: richTransaction.isOutgoing, - transactionStatus: nil, + transactionStatus: nil, nonceRaw: nil ) - + vc.senderId = senderId vc.recipientId = recipientId vc.comment = comment @@ -124,13 +125,13 @@ private extension EthWalletFactory { vc.richTransaction = richTransaction return vc } - - func makeTransactionDetailsVC(service: Service) -> EthTransactionDetailsViewController { + + fileprivate func makeTransactionDetailsVC(service: Service) -> EthTransactionDetailsViewController { EthTransactionDetailsViewController( dialogService: assembler.resolve(DialogService.self)!, currencyInfo: assembler.resolve(InfoServiceProtocol.self)!, addressBookService: assembler.resolve(AddressBookService.self)!, - accountService: assembler.resolve(AccountService.self)!, + accountService: assembler.resolve(AccountService.self)!, walletService: service, languageService: assembler.resolve(LanguageStorageProtocol.self)! ) diff --git a/Adamant/Modules/Wallets/Ethereum/EthWalletService+CoinInfoExtension.swift b/Adamant/Modules/Wallets/Ethereum/EthWalletService+CoinInfoExtension.swift new file mode 100644 index 000000000..9cdd3d052 --- /dev/null +++ b/Adamant/Modules/Wallets/Ethereum/EthWalletService+CoinInfoExtension.swift @@ -0,0 +1,32 @@ +import AdamantWalletsKit +// +// SmartTokenInfoProtocol.swift +// Adamant +// +// Created by Владимир Клевцов on 17.1.25.. +// Copyright © 2025 Adamant. All rights reserved. +// +import BigInt +import CommonKit + +extension EthWalletService { + var reliabilityGasPricePercent: BigUInt { + BigUInt(Self.coinInfo?.reliabilityGasLimitPercent ?? 10) + } + + var reliabilityGasLimitPercent: BigUInt { + BigUInt(Self.coinInfo?.reliabilityGasLimitPercent ?? 10) + } + + var defaultGasPriceGwei: BigUInt { + BigUInt(Self.coinInfo?.defaultGasPriceGwei ?? 10) + } + + var defaultGasLimit: BigUInt { + BigUInt(Self.coinInfo?.defaultGasLimit ?? 22000) + } + + var warningGasPriceGwei: BigUInt { + BigUInt(Self.coinInfo?.warningGasPriceGwei ?? 25) + } +} diff --git a/Adamant/Modules/Wallets/Ethereum/EthWalletService+DynamicConstants.swift b/Adamant/Modules/Wallets/Ethereum/EthWalletService+DynamicConstants.swift deleted file mode 100644 index 54d359f9a..000000000 --- a/Adamant/Modules/Wallets/Ethereum/EthWalletService+DynamicConstants.swift +++ /dev/null @@ -1,108 +0,0 @@ -import Foundation -import BigInt -import CommonKit - -extension EthWalletService { - // MARK: - Constants - static let fixedFee: Decimal = 0.0 - static let currencySymbol = "ETH" - static let currencyExponent: Int = -18 - static let qqPrefix: String = "ethereum" - - static let healthCheckParameters = CoinHealthCheckParameters( - normalUpdateInterval: 300, - crucialUpdateInterval: 30, - onScreenUpdateInterval: 10, - threshold: 5, - normalServiceUpdateInterval: 330, - crucialServiceUpdateInterval: 30, - onScreenServiceUpdateInterval: 10 - ) - - static var newPendingInterval: Int { - 4000 - } - - static var oldPendingInterval: Int { - 3000 - } - - static var registeredInterval: Int { - 5000 - } - - static var newPendingAttempts: Int { - 20 - } - - static var oldPendingAttempts: Int { - 4 - } - - var reliabilityGasPricePercent: BigUInt { - 10 - } - - var reliabilityGasLimitPercent: BigUInt { - 10 - } - - var defaultGasPriceGwei: BigUInt { - 10 - } - - var defaultGasLimit: BigUInt { - 22000 - } - - var warningGasPriceGwei: BigUInt { - 25 - } - - var tokenName: String { - "Ethereum" - } - - var consistencyMaxTime: Double { - 1200 - } - - var minBalance: Decimal { - 0 - } - - var minAmount: Decimal { - 0 - } - - var defaultVisibility: Bool { - true - } - - var defaultOrdinalLevel: Int? { - 20 - } - - static var minNodeVersion: String? { - nil - } - - var transferDecimals: Int { - 6 - } - - static let explorerTx = "https://etherscan.io/tx/" - static let explorerAddress = "https://etherscan.io/address/" - static var nodes: [Node] { - [ - Node.makeDefaultNode(url: URL(string: "https://ethnode2.adamant.im")!, altUrl: URL(string: "http://95.216.114.252:44099")), -Node.makeDefaultNode(url: URL(string: "https://ethnode3.adamant.im")!, altUrl: URL(string: "http://46.4.37.157:44099")), - ] - } - - static var serviceNodes: [Node] { - [ - - ] - } -} diff --git a/Adamant/Modules/Wallets/Ethereum/EthWalletService+RichMessageProvider.swift b/Adamant/Modules/Wallets/Ethereum/EthWalletService+RichMessageProvider.swift index 50460628f..96fd3127c 100644 --- a/Adamant/Modules/Wallets/Ethereum/EthWalletService+RichMessageProvider.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthWalletService+RichMessageProvider.swift @@ -6,59 +6,59 @@ // Copyright © 2018 Adamant. All rights reserved. // +import CommonKit import Foundation import MessageKit import UIKit -import CommonKit extension EthWalletService { var newPendingInterval: TimeInterval { .init(milliseconds: type(of: self).newPendingInterval) } - + var oldPendingInterval: TimeInterval { .init(milliseconds: type(of: self).oldPendingInterval) } - + var registeredInterval: TimeInterval { .init(milliseconds: type(of: self).registeredInterval) } - + var newPendingAttempts: Int { type(of: self).newPendingAttempts } - + var oldPendingAttempts: Int { type(of: self).oldPendingAttempts } - + var dynamicRichMessageType: String { return type(of: self).richMessageType } - + // MARK: Short description - + func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { let amount: String - + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount) else { return NSAttributedString(string: "⬅️ \(EthWalletService.currencySymbol)") } - + if let decimal = Decimal(string: raw) { amount = AdamantBalanceFormat.full.format(decimal) } else { amount = raw } - + let string: String if transaction.isOutgoing { string = "⬅️ \(amount) \(EthWalletService.currencySymbol)" } else { string = "➡️ \(amount) \(EthWalletService.currencySymbol)" } - + return NSAttributedString(string: string) } } diff --git a/Adamant/Modules/Wallets/Ethereum/EthWalletService+RichMessageProviderWithStatusCheck.swift b/Adamant/Modules/Wallets/Ethereum/EthWalletService+RichMessageProviderWithStatusCheck.swift index a3283e322..8cd0d2158 100644 --- a/Adamant/Modules/Wallets/Ethereum/EthWalletService+RichMessageProviderWithStatusCheck.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthWalletService+RichMessageProviderWithStatusCheck.swift @@ -6,27 +6,27 @@ // Copyright © 2018 Adamant. All rights reserved. // +import CommonKit import Foundation -@preconcurrency import web3swift @preconcurrency import Web3Core -import CommonKit +@preconcurrency import web3swift extension EthWalletService { func statusInfoFor(transaction: CoinTransaction) async -> TransactionStatusInfo { let hash: String? - + if let transaction = transaction as? RichMessageTransaction { hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) } else { hash = transaction.txId } - + guard let hash = hash else { return .init(sentDate: nil, status: .inconsistent(.wrongTxHash)) } - + let transactionInfo: EthTransactionInfo - + do { transactionInfo = try await ethApiService.requestWeb3( waitsForConnectivity: true @@ -37,21 +37,21 @@ extension EthWalletService { } catch { return .init(error: error) } - + guard let details = transactionInfo.details, let receipt = transactionInfo.receipt else { return .init(sentDate: nil, status: .pending) } - + var sentDate: Date? if let blockHash = details.blockHash { sentDate = try? await ethApiService.requestWeb3(waitsForConnectivity: true) { web3 in try await web3.eth.block(by: blockHash).timestamp }.get() } - + return await .init( sentDate: sentDate, status: getStatus(details: details, transaction: transaction, receipt: receipt) @@ -59,18 +59,18 @@ extension EthWalletService { } } -private extension EthWalletService { - struct EthTransactionInfo: Sendable { +extension EthWalletService { + fileprivate struct EthTransactionInfo: Sendable { var details: Web3Core.TransactionDetails? var receipt: TransactionReceipt? } - enum EthTransactionInfoElement: Sendable { + fileprivate enum EthTransactionInfoElement: Sendable { case details(Web3Core.TransactionDetails) case receipt(TransactionReceipt) } - - func getTransactionInfo(hash: String, web3: Web3) async throws -> EthTransactionInfo { + + fileprivate func getTransactionInfo(hash: String, web3: Web3) async throws -> EthTransactionInfo { try await withThrowingTaskGroup( of: EthTransactionInfoElement.self, returning: Atomic.self @@ -78,11 +78,11 @@ private extension EthWalletService { group.addTask(priority: .userInitiated) { @Sendable in .details(try await web3.eth.transactionDetails(hash)) } - + group.addTask(priority: .userInitiated) { @Sendable in .receipt(try await web3.eth.transactionReceipt(hash)) } - + return try await group.reduce( into: .init(wrappedValue: .init()) ) { result, value in @@ -95,45 +95,45 @@ private extension EthWalletService { } }.wrappedValue } - - func getStatus( + + fileprivate func getStatus( details: Web3Core.TransactionDetails, transaction: CoinTransaction, receipt: TransactionReceipt ) async -> TransactionStatus { let status = receipt.status.asTransactionStatus() guard status == .success else { return status } - + let eth = details.transaction - + // MARK: Check addresses - + var realSenderAddress = eth.sender?.address ?? "" var realRecipientAddress = eth.to.address - + if transaction is RichMessageTransaction { guard let senderAddress = try? await getWalletAddress(byAdamantAddress: transaction.senderAddress) else { return .inconsistent(.senderCryptoAddressUnavailable(tokenSymbol)) } - + guard let recipientAddress = try? await getWalletAddress(byAdamantAddress: transaction.recipientAddress) else { return .inconsistent(.recipientCryptoAddressUnavailable(tokenSymbol)) } - + realSenderAddress = senderAddress realRecipientAddress = recipientAddress } - + guard eth.sender?.address.caseInsensitiveCompare(realSenderAddress) == .orderedSame else { return .inconsistent(.senderCryptoAddressMismatch(tokenSymbol)) } - + guard eth.to.address.caseInsensitiveCompare(realRecipientAddress) == .orderedSame else { return .inconsistent(.recipientCryptoAddressMismatch(tokenSymbol)) } - + if transaction.isOutgoing { guard ethWallet?.address.caseInsensitiveCompare(eth.sender?.address ?? "") == .orderedSame else { return .inconsistent(.senderCryptoAddressMismatch(tokenSymbol)) @@ -143,36 +143,36 @@ private extension EthWalletService { return .inconsistent(.recipientCryptoAddressMismatch(tokenSymbol)) } } - + // MARK: Compare amounts let realAmount = eth.value.asDecimal(exponent: EthWalletService.currencyExponent) - + guard let reported = reportedValue(for: transaction) else { return .inconsistent(.wrongAmount) } - let min = reported - reported*0.005 - let max = reported + reported*0.005 - + let min = reported - reported * 0.005 + let max = reported + reported * 0.005 + guard (min...max).contains(realAmount) else { return .inconsistent(.wrongAmount) } - + return .success } - - func reportedValue(for transaction: CoinTransaction) -> Decimal? { + + fileprivate func reportedValue(for transaction: CoinTransaction) -> Decimal? { guard let transaction = transaction as? RichMessageTransaction else { return transaction.amountValue } - + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), let reportedValue = AdamantBalanceFormat.deserializeBalance(from: raw) else { return nil } - + return reportedValue } } @@ -182,10 +182,10 @@ extension TransactionReceipt.TXStatus { switch self { case .ok: return .success - + case .failed: return .failed - + case .notYetProcessed: return .registered } diff --git a/Adamant/Modules/Wallets/Ethereum/EthWalletService+Send.swift b/Adamant/Modules/Wallets/Ethereum/EthWalletService+Send.swift index 253dd5611..748133f00 100644 --- a/Adamant/Modules/Wallets/Ethereum/EthWalletService+Send.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthWalletService+Send.swift @@ -6,11 +6,12 @@ // Copyright © 2018 Adamant. All rights reserved. // +import CommonKit import UIKit +@preconcurrency import Web3Core @preconcurrency import web3swift + import struct BigInt.BigUInt -@preconcurrency import Web3Core -import CommonKit extension CodableTransaction: RawTransaction { var txHash: String? { @@ -20,8 +21,8 @@ extension CodableTransaction: RawTransaction { } extension EthWalletService: WalletServiceTwoStepSend { - typealias T = CodableTransaction - + typealias T = CodableTransaction + func createTransaction( recipient: String, amount: Decimal, @@ -33,7 +34,7 @@ extension EthWalletService: WalletServiceTwoStepSend { return try await createTransaction(recipient: recipient, amount: amount, web3: web3) }.get() } - + // MARK: Create & Send private func createTransaction( recipient: String, @@ -43,61 +44,62 @@ extension EthWalletService: WalletServiceTwoStepSend { guard let ethWallet = ethWallet else { throw WalletServiceError.notLogged } - + guard let ethRecipient = EthereumAddress(recipient) else { throw WalletServiceError.accountNotFound } - + guard let bigUIntAmount = Utilities.parseToBigUInt(String(format: "%.18f", amount.doubleValue), units: .ether) else { throw WalletServiceError.invalidAmount(amount) } - + guard let keystoreManager = web3.provider.attachedKeystoreManager else { throw WalletServiceError.internalError(message: "Failed to get web3.provider.KeystoreManager", error: nil) } - + let provider = web3.provider - + // MARK: Create contract - + guard let contract = web3.contract(Web3.Utils.coldWalletABI, at: ethRecipient), - var tx = contract.createWriteOperation()?.transaction + var tx = contract.createWriteOperation()?.transaction else { throw WalletServiceError.internalError(message: "ETH Wallet: Send - contract loading error", error: nil) } - + tx.from = ethWallet.ethAddress tx.to = ethRecipient tx.value = bigUIntAmount - + await calculateFee(for: ethRecipient) - + let resolver = PolicyResolver(provider: provider) let policies: Policies = Policies( gasLimitPolicy: .manual(gasLimit), gasPricePolicy: .manual(gasPrice) ) - + do { try await resolver.resolveAll(for: &tx, with: policies) - - try Web3Signer.signTX(transaction: &tx, - keystore: keystoreManager, - account: ethWallet.ethAddress, - password: EthWalletService.walletPassword + + try Web3Signer.signTX( + transaction: &tx, + keystore: keystoreManager, + account: ethWallet.ethAddress, + password: EthWalletService.walletPassword ) - + return tx } catch { throw WalletServiceError.internalError(message: "Transaction sign error", error: error) } } - + func sendTransaction(_ transaction: CodableTransaction) async throws { guard let txEncoded = transaction.encode() else { throw WalletServiceError.internalError(message: .adamant.sharedErrors.unknownError, error: nil) } - + _ = try await ethApiService.requestWeb3(waitsForConnectivity: false) { web3 in try await web3.eth.send(raw: txEncoded) }.get() diff --git a/Adamant/Modules/Wallets/Ethereum/EthWalletService.swift b/Adamant/Modules/Wallets/Ethereum/EthWalletService.swift index aeda63dc5..d775fc583 100644 --- a/Adamant/Modules/Wallets/Ethereum/EthWalletService.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthWalletService.swift @@ -6,25 +6,26 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation -import UIKit -import web3swift -import Swinject +import AdamantWalletsKit import Alamofire @preconcurrency import BigInt -@preconcurrency import Web3Core import Combine import CommonKit +import Foundation +import Swinject +import UIKit +@preconcurrency import Web3Core +import web3swift struct EthWalletStorage { let keystore: BIP32Keystore let unicId: String - + func getWallet() -> EthWallet? { guard let ethAddress = keystore.addresses?.first else { return nil } - + return EthWallet( unicId: unicId, address: ethAddress.address, @@ -39,164 +40,181 @@ extension Web3Error { switch self { case .connectionError: return .networkError - + case .nodeError(let message): return .remoteServiceError(message: message) - + case .generalError(_ as URLError): return .networkError - + case .generalError(let error), - .keystoreError(let error as Error): + .keystoreError(let error as Error): return .internalError(message: error.localizedDescription, error: error) - + case .inputError(let message), .processingError(let message): return .internalError(message: message, error: nil) - + case .transactionSerializationError, - .dataError, - .walletError, - .unknownError, - .rpcError, - .revert, - .revertCustom, - .typeError: + .dataError, + .walletError, + .unknownError, + .rpcError, + .revert, + .revertCustom, + .typeError: return .internalError(message: "Unknown error", error: nil) - case .valueError(desc: let desc): + case .valueError(let desc): return .internalError(message: "Unknown error \(String(describing: desc))", error: nil) - case .serverError(code: let code): + case .serverError(let code): return .remoteServiceError(message: "Unknown error \(code)") - case .clientError(code: let code): + case .clientError(let code): return .internalError(message: "Unknown error \(code)", error: nil) } } } -final class EthWalletService: WalletCoreProtocol, @unchecked Sendable { - // MARK: - Constants - let addressRegex = try! NSRegularExpression(pattern: "^0x[a-fA-F0-9]{40}$") - - static let currencyLogo = UIImage.asset(named: "ethereum_wallet") ?? .init() - +final class EthWalletService: WalletCoreProtocol, WalletStaticCoreProtocol, ERC20GasAlgorithmComputable, @unchecked Sendable { + static let currencySymbol = "ETH" + // MARK: - Constants + let addressRegex = try! NSRegularExpression(pattern: "^0x[a-fA-F0-9]{40}$") + + static var coinInfo: CoinInfoDTO? { + CoinInfoProvider.storage?[currencySymbol] + } + + static let currencyLogo = UIImage.asset(named: "ethereum_wallet") ?? .init() + var tokenSymbol: String { - return type(of: self).currencySymbol + type(of: self).currencySymbol } - + var tokenLogo: UIImage { - return type(of: self).currencyLogo + type(of: self).currencyLogo } - + static var tokenNetworkSymbol: String { - return "ERC20" + "ERC20" } - + var tokenContract: String { - return "" + "" } - - var tokenUnicID: String { + + var tokenUniqueID: String { Self.tokenNetworkSymbol + tokenSymbol } - + var richMessageType: String { - return Self.richMessageType - } + Self.richMessageType + } var qqPrefix: String { - return Self.qqPrefix - } + Self.qqPrefix + } var isSupportIncreaseFee: Bool { - return true + true } - + var isIncreaseFeeEnabled: Bool { - return increaseFeeService.isIncreaseFeeEnabled(for: tokenUnicID) + increaseFeeService.isIncreaseFeeEnabled(for: tokenUniqueID) } - + var nodeGroups: [NodeGroup] { [.eth] } - + var explorerAddress: String { Self.explorerAddress } - + @Atomic private(set) var isDynamicFee: Bool = true @Atomic private(set) var transactionFee: Decimal = 0.0 @Atomic private(set) var gasPrice: BigUInt = 0 @Atomic private(set) var gasLimit: BigUInt = 0 @Atomic private(set) var isWarningGasPrice = false @Atomic private var balanceInvalidationSubscription: AnyCancellable? - - static let transferGas: Decimal = 21000 - static let kvsAddress = "eth:address" - + + static let transferGas: Decimal = 21000 + static let kvsAddress = "eth:address" + static let walletPath = "m/44'/60'/3'/1" static let walletPassword = "" - + // MARK: - Dependencies weak var accountService: AccountService? var apiService: AdamantApiServiceProtocol! - var ethApiService: EthApiService! + var ethApiService: EthApiServiceProtocol! var dialogService: DialogService! var increaseFeeService: IncreaseFeeService! var vibroService: VibroService! var coreDataStack: CoreDataStack! - + var ethBIP32Service: EthBIP32ServiceProtocol! + // MARK: - Notifications let walletUpdatedNotification = Notification.Name("adamant.ethWallet.walletUpdated") let serviceEnabledChanged = Notification.Name("adamant.ethWallet.enabledChanged") let transactionFeeUpdated = Notification.Name("adamant.ethWallet.feeUpdated") let serviceStateChanged = Notification.Name("adamant.ethWallet.stateChanged") - + + @MainActor + private let walletUpdateSender = ObservableSender() + @MainActor + var walletUpdatePublisher: AnyObservable { + walletUpdateSender.eraseToAnyPublisher() + } + // MARK: RichMessageProvider properties static let richMessageType = "eth_transaction" - - // MARK: - Properties - + + var increasedGasPricePercent: Decimal { + Decimal(Self.coinInfo?.increasedGasPricePercent ?? 50) + } + + // MARK: - Properties + public static let transactionsListApiSubpath = "ethtxs" @Atomic private(set) var enabled = true @Atomic private var subscriptions = Set() @Atomic private var cachedWalletAddress: [String: String] = [:] - + @ObservableValue private(set) var historyTransactions: [TransactionDetails] = [] @ObservableValue private(set) var hasMoreOldTransactions: Bool = true var transactionsPublisher: AnyObservable<[TransactionDetails]> { $historyTransactions.eraseToAnyPublisher() } - + var hasMoreOldTransactionsPublisher: AnyObservable { $hasMoreOldTransactions.eraseToAnyPublisher() } - + @MainActor var hasEnabledNode: Bool { ethApiService.hasEnabledNode } - + @MainActor var hasEnabledNodePublisher: AnyObservable { ethApiService.hasEnabledNodePublisher } - + private(set) lazy var coinStorage: CoinStorageService = AdamantCoinStorageService( - coinId: tokenUnicID, + coinId: tokenUniqueID, coreDataStack: coreDataStack, blockchainType: richMessageType ) - + // MARK: - State @Atomic private(set) var state: WalletServiceState = .notInitiated - + private func setState(_ newState: WalletServiceState, silent: Bool = false) { guard newState != state else { return } - + state = newState - + if !silent { NotificationCenter.default.post( name: serviceStateChanged, @@ -205,21 +223,21 @@ final class EthWalletService: WalletCoreProtocol, @unchecked Sendable { ) } } - + @Atomic private(set) var ethWallet: EthWallet? @Atomic private var walletStorage: EthWalletStorage? - + var wallet: WalletAccount? { return ethWallet } - + // MARK: - Delayed KVS save @Atomic private var balanceObserver: NSObjectProtocol? - + // MARK: - Logic init() { // Notifications addObservers() } - + func addObservers() { NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedIn, object: nil) @@ -227,14 +245,14 @@ final class EthWalletService: WalletCoreProtocol, @unchecked Sendable { self?.update() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantAccountService.accountDataUpdated, object: nil) .sink { @MainActor [weak self] _ in self?.update() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedOut, object: nil) .sink { @MainActor [weak self] _ in @@ -250,7 +268,7 @@ final class EthWalletService: WalletCoreProtocol, @unchecked Sendable { } .store(in: &subscriptions) } - + func addTransactionObserver() { coinStorage.transactionsPublisher .sink { [weak self] transactions in @@ -258,130 +276,135 @@ final class EthWalletService: WalletCoreProtocol, @unchecked Sendable { } .store(in: &subscriptions) } - + func getWallet() async -> EthWallet? { if let wallet = ethWallet { return wallet } - + guard let storage = walletStorage else { return nil } return storage.getWallet() } - + func update() { Task { await update() } } - + @MainActor func update() async { guard let wallet = await getWallet() else { return } - + switch state { case .notInitiated, .updating, .initiationFailed: return - + case .upToDate: break } - + setState(.updating) - + if let balance = try? await getBalance(forAddress: wallet.ethAddress) { if wallet.balance < balance, wallet.isBalanceInitialized { vibroService.applyVibration(.success) } - + wallet.balance = balance markBalanceAsFresh(wallet) - - NotificationCenter.default.post( - name: walletUpdatedNotification, - object: self, - userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet] - ) + + walletUpdateSender.send() + } else { + wallet.isBalanceInitialized = false } - + + NotificationCenter.default.post( + name: walletUpdatedNotification, + object: self, + userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet] + ) + setState(.upToDate) await calculateFee() - } - + } + private func markBalanceAsFresh(_ wallet: EthWallet) { wallet.isBalanceInitialized = true - + balanceInvalidationSubscription = Task { [weak self] in try await Task.sleep(interval: Self.balanceLifetime, pauseInBackground: true) guard let self else { return } wallet.isBalanceInitialized = false - + NotificationCenter.default.post( name: walletUpdatedNotification, object: self, userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet] ) + + await self.walletUpdateSender.send() }.eraseToAnyCancellable() } - + func calculateFee(for address: EthereumAddress? = nil) async { - let priceRaw = try? await getGasPrices() - let gasLimitRaw = try? await getGasLimit(to: address) - - var price = priceRaw ?? defaultGasPriceGwei.toWei() - var gasLimit = gasLimitRaw ?? defaultGasLimit - - let pricePercent = price * reliabilityGasPricePercent / 100 - let gasLimitPercent = gasLimit * reliabilityGasLimitPercent / 100 - - price = priceRaw == nil - ? price - : price + pricePercent - - gasLimit = gasLimitRaw == nil - ? gasLimit - : gasLimit + gasLimitPercent - - var newFee = (price * gasLimit).asDecimal(exponent: EthWalletService.currencyExponent) - - newFee = isIncreaseFeeEnabled - ? newFee * defaultIncreaseFee - : newFee - - guard transactionFee != newFee else { return } - - transactionFee = newFee - let incGasPrice = UInt64(price.asDouble() * defaultIncreaseFee.doubleValue) - - gasPrice = isIncreaseFeeEnabled - ? BigUInt(integerLiteral: incGasPrice) - : price - - isWarningGasPrice = gasPrice >= warningGasPriceGwei.toWei() - self.gasLimit = gasLimit - - NotificationCenter.default.post(name: transactionFeeUpdated, object: self, userInfo: nil) - } - - // MARK: - Tools - - func validate(address: String) -> AddressValidationResult { - return addressRegex.perfectMatch(with: address) ? .valid : .invalid(description: nil) - } - - func getGasPrices() async throws -> BigUInt { + async let pricePriceAsync = getGasPrices() + async let gasLimitAsync = getGasLimit(to: address) + var gasPriceCoeficient: Decimal = 1 + if isIncreaseFeeEnabled { + gasPriceCoeficient += increasedGasPricePercent / 100 + } + + let gasPrice: BigUInt + let gasLimit: BigUInt + + // Getting gas data + do { + let (gasPriceFromChain, gasLimitFromChain) = try await (pricePriceAsync, gasLimitAsync) + try Task.checkCancellation() + gasPrice = gasPriceFromChain + gasLimit = gasLimitFromChain + } catch { + gasPrice = BigUInt(defaultGasPriceGwei).toWei() + gasLimit = BigUInt(defaultGasLimit) + } + + // Updating localy + updateGasAndFee( + gasPrice: gasPrice, + gasLimit: gasLimit, + gasPriceCoeficient: gasPriceCoeficient + ) { [weak self] gasPrice, gasLimit, newFee in + guard let self else { return } + self.gasPrice = gasPrice + self.gasLimit = gasLimit + guard transactionFee != newFee else { return } + transactionFee = newFee + isWarningGasPrice = gasPrice >= BigUInt(warningGasPriceGwei).toWei() + NotificationCenter.default.post(name: transactionFeeUpdated, object: self, userInfo: nil) + } + } + + // MARK: - Tools + + func validate(address: String) -> AddressValidationResult { + return addressRegex.perfectMatch(with: address) ? .valid : .invalid(description: nil) + } + + func getGasPrices() async throws -> BigUInt { try await ethApiService.requestWeb3(waitsForConnectivity: false) { web3 in try await web3.eth.gasPrice() }.get() - } - + } + func getGasLimit(to address: EthereumAddress?) async throws -> BigUInt { guard let ethWallet = ethWallet else { throw WalletServiceError.internalError(.endpointBuildFailed) } var transaction: CodableTransaction = .emptyTransaction transaction.from = ethWallet.ethAddress transaction.to = address ?? ethWallet.ethAddress - + return try await ethApiService.requestWeb3(waitsForConnectivity: false) { [transaction] web3 in try await web3.eth.estimateGas(for: transaction) }.get() @@ -390,57 +413,47 @@ final class EthWalletService: WalletCoreProtocol, @unchecked Sendable { // MARK: - WalletInitiatedWithPassphrase extension EthWalletService { - func initWallet(withPassphrase passphrase: String) async throws -> WalletAccount { + func initWallet(withPassphrase passphrase: String, withPassword password: String) async throws -> WalletAccount { guard let adamant = accountService?.account else { throw WalletServiceError.notLogged } - + // MARK: 1. Prepare setState(.notInitiated) - + if enabled { enabled = false NotificationCenter.default.post(name: serviceEnabledChanged, object: self) } - + // MARK: 2. Create keys and addresses - do { - guard let store = try BIP32Keystore(mnemonics: passphrase, - password: EthWalletService.walletPassword, - mnemonicsPassword: "", - language: .english, - prefixPath: EthWalletService.walletPath - ) else { - throw WalletServiceError.internalError(message: "ETH Wallet: failed to create Keystore", error: nil) - } - - walletStorage = .init(keystore: store, unicId: tokenUnicID) - await ethApiService.setKeystoreManager(.init([store])) - } catch { - throw WalletServiceError.internalError(message: "ETH Wallet: failed to create Keystore", error: error) - } - + + let store = try await ethBIP32Service.keyStore(passphrase: passphrase) + walletStorage = .init(keystore: store, unicId: tokenUniqueID) + let eWallet = walletStorage?.getWallet() - + guard let eWallet = eWallet else { throw WalletServiceError.internalError(message: "ETH Wallet: failed to create Keystore", error: nil) } - + // MARK: 3. Update ethWallet = eWallet let kvsAddressModel = makeKVSAddressModel(wallet: eWallet) - + NotificationCenter.default.post( name: walletUpdatedNotification, object: self, userInfo: [AdamantUserInfoKey.WalletService.wallet: eWallet] ) - + + await walletUpdateSender.send() + if !enabled { enabled = true NotificationCenter.default.post(name: serviceEnabledChanged, object: self) } - + // MARK: 4. Save into KVS let service = self do { @@ -450,13 +463,13 @@ extension EthWalletService { service.kvsSaveCompletionRecursion(kvsAddressModel, result: result) } } - + service.setState(.upToDate) - + Task { await service.update() } - + return eWallet } catch let error as WalletServiceError { switch error { @@ -464,59 +477,63 @@ extension EthWalletService { /// The ADM Wallet is not initialized. Check the balance of the current wallet /// and save the wallet address to kvs when dropshipping ADM service.setState(.upToDate) - + Task { await service.update() } - + if let kvsAddressModel { service.save(kvsAddressModel) { result in service.kvsSaveCompletionRecursion(kvsAddressModel, result: result) } } - + return eWallet - + default: service.setState(.upToDate) throw error } } } - + func setInitiationFailed(reason: String) { setState(.initiationFailed(reason: reason)) ethWallet = nil } - + /// New accounts doesn't have enought money to save KVS. We need to wait for balance update, and then - retry save private func kvsSaveCompletionRecursion(_ model: KVSValueModel, result: WalletServiceSimpleResult) { if let observer = balanceObserver { NotificationCenter.default.removeObserver(observer) balanceObserver = nil } - + switch result { case .success: break - + case .failure(let error): switch error { case .notEnoughMoney: // Possibly new account, we need to wait for dropship // Register observer - let observer = NotificationCenter.default.addObserver(forName: NSNotification.Name.AdamantAccountService.accountDataUpdated, object: nil, queue: nil) { [weak self] _ in + let observer = NotificationCenter.default.addObserver( + forName: NSNotification.Name.AdamantAccountService.accountDataUpdated, + object: nil, + queue: nil + ) { [weak self] _ in guard let balance = self?.accountService?.account?.balance, balance > AdamantApiService.KvsFee else { return } - + self?.save(model) { [weak self] result in self?.kvsSaveCompletionRecursion(model, result: result) } } - + // Save referense to unregister it later balanceObserver = observer - + default: print("\(error.localizedDescription)") } @@ -535,7 +552,8 @@ extension EthWalletService: SwinjectDependentService { ethApiService = container.resolve(EthApiService.self) vibroService = container.resolve(VibroService.self) coreDataStack = container.resolve(CoreDataStack.self) - + ethBIP32Service = container.resolve(EthBIP32ServiceProtocol.self) + addTransactionObserver() } } @@ -546,41 +564,50 @@ extension EthWalletService { guard let address = EthereumAddress(address) else { throw WalletServiceError.internalError(message: "Incorrect address", error: nil) } - + return try await getBalance(forAddress: address) } - - func getBalance(forAddress address: EthereumAddress) async throws -> Decimal { + + func getBalance(forAddress address: EthereumAddress) async throws -> Decimal { let balance = try await ethApiService.requestWeb3(waitsForConnectivity: false) { web3 in try await web3.eth.getBalance(for: address) }.get() - + return balance.asDecimal(exponent: EthWalletService.currencyExponent) - } - - func getWalletAddress(byAdamantAddress address: String) async throws -> String { + } + + func getWalletAddress(byAdamantAddress address: String) async throws -> String { if let address = cachedWalletAddress[address], !address.isEmpty { return address } - + do { let result = try await apiService.get(key: EthWalletService.kvsAddress, sender: address).get() - + guard let result = result else { throw WalletServiceError.walletNotInitiated } - + cachedWalletAddress[address] = result - + return result } catch _ as ApiServiceError { throw WalletServiceError.remoteServiceError( message: "ETH Wallet: failed to get address from KVS" ) } - } + } } +#if DEBUG + extension EthWalletService { + @available(*, deprecated, message: "For testing purposes only") + func setWalletForTests(_ wallet: EthWallet?) { + self.ethWallet = wallet + } + } +#endif + // MARK: - KVS extension EthWalletService { /// - Parameters: @@ -592,28 +619,28 @@ extension EthWalletService { completion(.failure(error: .notLogged)) return } - + guard adamant.balance >= AdamantApiService.KvsFee else { completion(.failure(error: .notEnoughMoney)) return } - + Task { - let result = await apiService.store(model) - + let result = await apiService.store(model, date: .now) + switch result { case .success: completion(.success) - + case .failure(let error): completion(.failure(error: .apiError(error))) } } } - + private func makeKVSAddressModel(wallet: WalletAccount) -> KVSValueModel? { guard let keypair = accountService?.keypair else { return nil } - + return .init( key: Self.kvsAddress, value: wallet.address.lowercased(), @@ -626,28 +653,28 @@ extension EthWalletService { extension EthWalletService { func getTransaction(by hash: String) async throws -> EthTransaction { let sender = wallet?.address - + // MARK: 1. Transaction details let details = try await ethApiService.requestWeb3(waitsForConnectivity: false) { web3 in try await web3.eth.transactionDetails(hash) }.get() - + let isOutgoing: Bool if let sender = sender { isOutgoing = details.transaction.to.address != sender } else { isOutgoing = false } - + // MARK: 2. Transaction receipt do { let receipt = try await ethApiService.requestWeb3(waitsForConnectivity: false) { web3 in try await web3.eth.transactionReceipt(hash) }.get() - + // MARK: 3. Check if transaction is delivered guard receipt.status == .ok, - let blockNumber = details.blockNumber + let blockNumber = details.blockNumber else { let transaction = details.transaction.asEthTransaction( date: nil, @@ -660,18 +687,18 @@ extension EthWalletService { ) return transaction } - + // MARK: 4. Block timestamp & confirmations let currentBlock = try await ethApiService.requestWeb3(waitsForConnectivity: false) { web3 in try await web3.eth.blockNumber() }.get() - + let block = try await ethApiService.requestWeb3(waitsForConnectivity: false) { web3 in try await web3.eth.block(by: receipt.blockHash) }.get() - + let confirmations = currentBlock - blockNumber - + let transaction = details.transaction.asEthTransaction( date: block.timestamp, gasUsed: receipt.gasUsed, @@ -682,11 +709,11 @@ extension EthWalletService { isOutgoing: isOutgoing, hash: details.transaction.txHash ) - + return transaction } catch let error as Web3Error { switch error { - // Transaction not delivired yet + // Transaction not delivired yet case .inputError, .nodeError: let transaction = details.transaction.asEthTransaction( date: nil, @@ -698,7 +725,7 @@ extension EthWalletService { isOutgoing: isOutgoing ) return transaction - + default: throw error } @@ -708,7 +735,7 @@ extension EthWalletService { throw error } } - + func getTransactionsHistory( address: String, offset: Int, @@ -716,7 +743,7 @@ extension EthWalletService { ) async throws -> [EthTransactionShort] { let columns = "time,txfrom,txto,gas,gasprice,block,txhash,value" let order = "time.desc" - + let txFromQueryParameters = [ "select": columns, "limit": String(limit), @@ -725,7 +752,7 @@ extension EthWalletService { "order": order, "contract_to": "eq." ] - + let txToQueryParameters = [ "select": columns, "limit": String(limit), @@ -734,7 +761,7 @@ extension EthWalletService { "order": order, "contract_to": "eq." ] - + let transactionsFrom: [EthTransactionShort] = try await ethApiService.requestApiCore(waitsForConnectivity: false) { core, origin in await core.sendRequestJsonResponse( origin: origin, @@ -744,7 +771,7 @@ extension EthWalletService { encoding: .url ) }.get() - + let transactionsTo: [EthTransactionShort] = try await ethApiService.requestApiCore(waitsForConnectivity: false) { core, origin in await core.sendRequestJsonResponse( origin: origin, @@ -754,39 +781,39 @@ extension EthWalletService { encoding: .url ) }.get() - + let transactions = transactionsFrom + transactionsTo return transactions.sorted { $0.date.compare($1.date) == .orderedDescending } } - + func loadTransactions(offset: Int, limit: Int) async throws -> Int { let trs = try await getTransactionsHistory(offset: offset, limit: limit) - + guard trs.count > 0 else { hasMoreOldTransactions = false return .zero } - + coinStorage.append(trs) - + return trs.count } - + func getTransactionsHistory(offset: Int, limit: Int) async throws -> [TransactionDetails] { guard let address = wallet?.address else { throw WalletServiceError.accountNotFound } - + let trs = try await getTransactionsHistory( address: address, offset: offset, limit: limit ) - + guard trs.count > 0 else { return [] } - + return trs.map { transaction in let isOutgoing: Bool = transaction.from == address return SimpleTransactionDetails( @@ -804,11 +831,11 @@ extension EthWalletService { ) } } - + func getLocalTransactionHistory() -> [TransactionDetails] { historyTransactions } - + func updateStatus(for id: String, status: TransactionStatus?) { coinStorage.updateStatus(for: id, status: status) } @@ -819,24 +846,32 @@ extension EthWalletService: PrivateKeyGenerator { var rowTitle: String { return "Ethereum" } - + var rowImage: UIImage? { return .asset(named: "ethereum_wallet_row") } - + var keyFormat: KeyFormat { .HEX } - + func generatePrivateKeyFor(passphrase: String) -> String? { guard AdamantUtilities.validateAdamantPassphrase(passphrase: passphrase) else { return nil } - - guard let keystore = try? BIP32Keystore(mnemonics: passphrase, password: EthWalletService.walletPassword, mnemonicsPassword: "", language: .english, prefixPath: EthWalletService.walletPath), + + guard + let keystore = try? BIP32Keystore( + mnemonics: passphrase, + password: EthWalletService.walletPassword, + mnemonicsPassword: "", + language: .english, + prefixPath: EthWalletService.walletPath + ), let account = keystore.addresses?.first, - let privateKeyData = try? keystore.UNSAFE_getPrivateKeyData(password: EthWalletService.walletPassword, account: account) else { + let privateKeyData = try? keystore.UNSAFE_getPrivateKeyData(password: EthWalletService.walletPassword, account: account) + else { return nil } - + return privateKeyData.toHexString() } } diff --git a/Adamant/Modules/Wallets/Ethereum/EthWalletViewController.swift b/Adamant/Modules/Wallets/Ethereum/EthWalletViewController.swift index 9345c01d1..c345010ff 100644 --- a/Adamant/Modules/Wallets/Ethereum/EthWalletViewController.swift +++ b/Adamant/Modules/Wallets/Ethereum/EthWalletViewController.swift @@ -6,14 +6,14 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit import CommonKit +import UIKit extension String.adamant.wallets { static var ethereum: String { String.localized("AccountTab.Wallets.ethereum_wallet", comment: "Account tab: Ethereum wallet") } - + static var sendEth: String { String.localized("AccountTab.Row.SendEth", comment: "Account tab: 'Send ETH tokens' button") } @@ -23,11 +23,11 @@ final class EthWalletViewController: WalletViewControllerBase { override func sendRowLocalizedLabel() -> NSAttributedString { return NSAttributedString(string: String.adamant.wallets.sendEth) } - + override func encodeForQr(address: String) -> String? { return "ethereum:\(address)" } - + override func setTitle() { walletTitleLabel.text = String.adamant.wallets.ethereum } diff --git a/Adamant/Modules/Wallets/Klayr/KLYWalletService+DynamicConstants.swift b/Adamant/Modules/Wallets/Klayr/KLYWalletService+DynamicConstants.swift deleted file mode 100644 index 9c4065021..000000000 --- a/Adamant/Modules/Wallets/Klayr/KLYWalletService+DynamicConstants.swift +++ /dev/null @@ -1,95 +0,0 @@ -import Foundation -import BigInt -import CommonKit - -extension KlyWalletService { - // MARK: - Constants - static let fixedFee: Decimal = 0.00164 - static let currencySymbol = "KLY" - static let currencyExponent: Int = -8 - static let qqPrefix: String = "klayr" - - static let healthCheckParameters = CoinHealthCheckParameters( - normalUpdateInterval: 270, - crucialUpdateInterval: 30, - onScreenUpdateInterval: 10, - threshold: 5, - normalServiceUpdateInterval: 330, - crucialServiceUpdateInterval: 30, - onScreenServiceUpdateInterval: 10 - ) - - static var newPendingInterval: Int { - 3000 - } - - static var oldPendingInterval: Int { - 3000 - } - - static var registeredInterval: Int { - 5000 - } - - static var newPendingAttempts: Int { - 15 - } - - static var oldPendingAttempts: Int { - 4 - } - - var tokenName: String { - "Klayr" - } - - var consistencyMaxTime: Double { - 60 - } - - var minBalance: Decimal { - 0.05 - } - - var minAmount: Decimal { - 0 - } - - var defaultVisibility: Bool { - true - } - - var defaultOrdinalLevel: Int? { - 50 - } - - static var minNodeVersion: String? { - nil - } - - var transferDecimals: Int { - 8 - } - - static let explorerTx = "https://explorer.klayr.xyz/transaction/" - static let explorerAddress = "https://explorer.klayr.xyz/account/" - static var nodes: [Node] { - [ - Node.makeDefaultNode(url: URL(string: "https://klynode2.adamant.im")!, altUrl: URL(string: "http://109.176.199.130:44099")), -Node.makeDefaultNode(url: URL(string: "https://klynode3.adm.im")!, altUrl: URL(string: "http://37.27.205.78:44099")), - ] - } - - static var serviceNodes: [Node] { - [ - Node.makeDefaultNode( - url: URL(string: "https://klyservice2.adamant.im")!, - altUrl: URL(string: "http://109.176.199.130:44098")! - ), - Node.makeDefaultNode( - url: URL(string: "https://klyservice3.adm.im")!, - altUrl: URL(string: "http://37.27.205.78:44098")! - ), - ] - } -} diff --git a/Adamant/Modules/Wallets/Klayr/KlyApiCore.swift b/Adamant/Modules/Wallets/Klayr/KlyApiCore.swift index ae9f70291..a51a5b69b 100644 --- a/Adamant/Modules/Wallets/Klayr/KlyApiCore.swift +++ b/Adamant/Modules/Wallets/Klayr/KlyApiCore.swift @@ -12,13 +12,15 @@ import Foundation class KlyApiCore: BlockchainHealthCheckableService, @unchecked Sendable { func makeClient(origin: NodeOrigin) -> APIClient { - .init(options: .init( - nodes: [.init(origin: origin.asString())], - nethash: .mainnet, - randomNode: false - )) + .init( + options: .init( + nodes: [.init(origin: origin.asString())], + nethash: .mainnet, + randomNode: false + ) + ) } - + func request( origin: NodeOrigin, body: @escaping @Sendable ( @@ -32,39 +34,39 @@ class KlyApiCore: BlockchainHealthCheckableService, @unchecked Sendable { } } } - + func request( origin: NodeOrigin, _ body: @Sendable @escaping (APIClient) async throws -> Output ) async -> WalletServiceResult { let client = makeClient(origin: origin) - + do { return .success(try await body(client)) } catch { return .failure(mapError(error)) } } - + func getStatusInfo(origin: NodeOrigin) async -> WalletServiceResult { let startTimestamp = Date.now.timeIntervalSince1970 - + return await request(origin: origin) { client in try await LiskKit.Node(client: client).info() }.map { model in - .init( - ping: Date.now.timeIntervalSince1970 - startTimestamp, - height: model.height ?? .zero, - wsEnabled: false, - wsPort: nil, - version: .init(model.version) - ) + .init( + ping: Date.now.timeIntervalSince1970 - startTimestamp, + height: model.height ?? .zero, + wsEnabled: false, + wsPort: nil, + version: .init(model.version) + ) } } } -private extension LiskKit.Result { - func asWalletServiceResult() -> WalletServiceResult { +extension LiskKit.Result { + fileprivate func asWalletServiceResult() -> WalletServiceResult { switch self { case let .success(response): return .success(response) @@ -87,6 +89,6 @@ private func mapError(_ error: Error) -> WalletServiceError { if let error = error as? APIError { return mapError(error) } - + return .remoteServiceError(message: error.localizedDescription, error: error) } diff --git a/Adamant/Modules/Wallets/Klayr/KlyNodeApiService.swift b/Adamant/Modules/Wallets/Klayr/KlyNodeApiService.swift index 8252b3673..80e614782 100644 --- a/Adamant/Modules/Wallets/Klayr/KlyNodeApiService.swift +++ b/Adamant/Modules/Wallets/Klayr/KlyNodeApiService.swift @@ -6,25 +6,25 @@ // Copyright © 2024 Adamant. All rights reserved. // -import LiskKit -import Foundation import CommonKit +import Foundation +import LiskKit -final class KlyNodeApiService: ApiServiceProtocol { +final class KlyNodeApiService: KlyNodeApiServiceProtocol { let api: BlockchainHealthCheckWrapper - + @MainActor var nodesInfoPublisher: AnyObservable { api.nodesInfoPublisher } - + @MainActor var nodesInfo: NodesListInfo { api.nodesInfo } - + func healthCheck() { api.healthCheck() } - + init(api: BlockchainHealthCheckWrapper) { self.api = api } - + func requestNodeApi( body: @escaping @Sendable ( _ api: LiskKit.Node, @@ -35,7 +35,7 @@ final class KlyNodeApiService: ApiServiceProtocol { body(.init(client: client), completion) } } - + func requestTransactionsApi( _ request: @Sendable @escaping (Transactions) async throws -> Output ) async -> WalletServiceResult { @@ -43,7 +43,7 @@ final class KlyNodeApiService: ApiServiceProtocol { try await request(Transactions(client: client)) } } - + func requestAccountsApi( _ request: @Sendable @escaping (Accounts) async throws -> Output ) async -> WalletServiceResult { @@ -51,7 +51,7 @@ final class KlyNodeApiService: ApiServiceProtocol { try await request(Accounts(client: client)) } } - + func getStatusInfo() async -> WalletServiceResult { await api.request(waitsForConnectivity: false) { core, origin in await core.getStatusInfo(origin: origin) @@ -59,8 +59,8 @@ final class KlyNodeApiService: ApiServiceProtocol { } } -private extension KlyNodeApiService { - func requestClient( +extension KlyNodeApiService { + fileprivate func requestClient( waitsForConnectivity: Bool, body: @escaping @Sendable ( _ client: APIClient, @@ -71,8 +71,8 @@ private extension KlyNodeApiService { await core.request(origin: origin, body: body) } } - - func requestClient( + + fileprivate func requestClient( waitsForConnectivity: Bool, _ body: @Sendable @escaping (APIClient) async throws -> Output ) async -> WalletServiceResult { diff --git a/Adamant/Modules/Wallets/Klayr/KlyNodeApiServiceProtocol.swift b/Adamant/Modules/Wallets/Klayr/KlyNodeApiServiceProtocol.swift new file mode 100644 index 000000000..061dfef78 --- /dev/null +++ b/Adamant/Modules/Wallets/Klayr/KlyNodeApiServiceProtocol.swift @@ -0,0 +1,22 @@ +// +// KlyNodeApiServiceProtocol.swift +// Adamant +// +// Created by Christian Benua on 22.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import CommonKit +import LiskKit + +// sourcery: AutoMockable +protocol KlyNodeApiServiceProtocol: ApiServiceProtocol { + + func requestTransactionsApi( + _ request: @Sendable @escaping (Transactions) async throws -> Output + ) async -> WalletServiceResult + + func requestAccountsApi( + _ request: @Sendable @escaping (Accounts) async throws -> Output + ) async -> WalletServiceResult +} diff --git a/Adamant/Modules/Wallets/Klayr/KlyServiceApiService.swift b/Adamant/Modules/Wallets/Klayr/KlyServiceApiService.swift index 97d0bcb29..6fbf44537 100644 --- a/Adamant/Modules/Wallets/Klayr/KlyServiceApiService.swift +++ b/Adamant/Modules/Wallets/Klayr/KlyServiceApiService.swift @@ -6,16 +6,16 @@ // Copyright © 2024 Adamant. All rights reserved. // -@preconcurrency import LiskKit -import Foundation import CommonKit +import Foundation +@preconcurrency import LiskKit final class KlyServiceApiCore: KlyApiCore, @unchecked Sendable { override func getStatusInfo( origin: NodeOrigin ) async -> WalletServiceResult { let startTimestamp = Date.now.timeIntervalSince1970 - + return await request(origin: origin) { client in let service = LiskKit.Service(client: client) return try await (fee: service.fees(), info: service.info()) @@ -33,19 +33,19 @@ final class KlyServiceApiCore: KlyApiCore, @unchecked Sendable { final class KlyServiceApiService: ApiServiceProtocol { let api: BlockchainHealthCheckWrapper - + @MainActor var nodesInfoPublisher: AnyObservable { api.nodesInfoPublisher } - + @MainActor var nodesInfo: NodesListInfo { api.nodesInfo } - + func healthCheck() { api.healthCheck() } - + init(api: BlockchainHealthCheckWrapper) { self.api = api } - + func requestServiceApi( waitsForConnectivity: Bool, body: @escaping @Sendable ( @@ -57,7 +57,7 @@ final class KlyServiceApiService: ApiServiceProtocol { body(.init(client: client, version: .v3), completion) } } - + func requestServiceApi( waitsForConnectivity: Bool, _ request: @Sendable @escaping (LiskKit.Service) async throws -> Output @@ -68,8 +68,8 @@ final class KlyServiceApiService: ApiServiceProtocol { } } -private extension KlyServiceApiService { - func requestClient( +extension KlyServiceApiService { + fileprivate func requestClient( waitsForConnectivity: Bool, body: @escaping @Sendable ( _ client: APIClient, @@ -80,8 +80,8 @@ private extension KlyServiceApiService { await core.request(origin: origin, body: body) } } - - func requestClient( + + fileprivate func requestClient( waitsForConnectivity: Bool, _ body: @Sendable @escaping (APIClient) async throws -> Output ) async -> WalletServiceResult { diff --git a/Adamant/Modules/Wallets/Klayr/KlyTransactionDetailsViewController.swift b/Adamant/Modules/Wallets/Klayr/KlyTransactionDetailsViewController.swift index 82e39d669..4f2679c84 100644 --- a/Adamant/Modules/Wallets/Klayr/KlyTransactionDetailsViewController.swift +++ b/Adamant/Modules/Wallets/Klayr/KlyTransactionDetailsViewController.swift @@ -6,72 +6,72 @@ // Copyright © 2024 Adamant. All rights reserved. // -import UIKit -import CommonKit import Combine +import CommonKit +import UIKit final class KlyTransactionDetailsViewController: TransactionDetailsViewControllerBase { // MARK: - Dependencies - + weak var service: KlyWalletService? { walletService?.core as? KlyWalletService } - + // MARK: - Properties - + private let autoupdateInterval: TimeInterval = 5.0 private var timerSubscription: AnyCancellable? - + private lazy var refreshControl: UIRefreshControl = { let control = UIRefreshControl() control.tintColor = .adamant.primary control.addTarget(self, action: #selector(refresh), for: UIControl.Event.valueChanged) return control }() - + override var showTxBlockchainComment: Bool { true } // MARK: - Lifecycle - + override func viewDidLoad() { currencySymbol = KlyWalletService.currencySymbol - + super.viewDidLoad() - + if service != nil { tableView.refreshControl = refreshControl } - + refresh(silent: true) - + if transaction != nil { startUpdate() } } - + // MARK: - Overrides - + override func explorerUrl(for transaction: TransactionDetails) -> URL? { let id = transaction.txId - + return URL(string: "\(KlyWalletService.explorerTx)\(id)") } - + @MainActor @objc func refresh(silent: Bool = false) { refreshTask = Task { guard let id = transaction?.txId, - let service = service + let service = service else { refreshControl.endRefreshing() return } - + do { var trs = try await service.getTransaction(by: id, waitsForConnectivity: false) let result = try await service.getCurrentFee() - + let lastHeight = result.lastHeight trs.updateConfirmations(value: lastHeight) transaction = trs @@ -82,18 +82,19 @@ final class KlyTransactionDetailsViewController: TransactionDetailsViewControlle } catch { refreshControl.endRefreshing() updateTransactionStatus() - + guard !silent else { return } dialogService.showRichError(error: error) } } } - + // MARK: - Autoupdate - + func startUpdate() { refresh(silent: true) - timerSubscription = Timer + timerSubscription = + Timer .publish(every: autoupdateInterval, on: .main, in: .common) .autoconnect() .sink { [weak self] _ in diff --git a/Adamant/Modules/Wallets/Klayr/KlyTransactionsViewController.swift b/Adamant/Modules/Wallets/Klayr/KlyTransactionsViewController.swift index 89bf30ffd..e267a2446 100644 --- a/Adamant/Modules/Wallets/Klayr/KlyTransactionsViewController.swift +++ b/Adamant/Modules/Wallets/Klayr/KlyTransactionsViewController.swift @@ -6,12 +6,12 @@ // Copyright © 2024 Adamant. All rights reserved. // -import UIKit -import LiskKit -import web3swift import BigInt -import CommonKit import Combine +import CommonKit +import LiskKit +import UIKit +import web3swift final class KlyTransactionsViewController: TransactionsListViewControllerBase { func tableView( @@ -20,21 +20,21 @@ final class KlyTransactionsViewController: TransactionsListViewControllerBase { ) { tableView.deselectRow(at: indexPath, animated: true) guard let address = walletService.core.wallet?.address, - let transaction = transactions[safe: indexPath.row] + let transaction = transactions[safe: indexPath.row] else { return } - + let controller = screensFactory.makeDetailsVC(service: walletService) - + controller.transaction = transaction - + if transaction.senderAddress.caseInsensitiveCompare(address) == .orderedSame { controller.senderName = String.adamant.transactionDetails.yourAddress } - + if transaction.recipientAddress.caseInsensitiveCompare(address) == .orderedSame { controller.recipientName = String.adamant.transactionDetails.yourAddress } - + navigationController?.pushViewController(controller, animated: true) } } @@ -43,62 +43,62 @@ extension Transactions.TransactionModel: TransactionDetails, @unchecked @retroac var nonceRaw: String? { return self.nonce } - + var defaultCurrencySymbol: String? { KlyWalletService.currencySymbol } - + var txId: String { return id } - + var dateValue: Date? { return timestamp.map { Date(timeIntervalSince1970: TimeInterval($0)) } } - + var amountValue: Decimal? { let value = BigUInt(self.amount) ?? BigUInt(0) - + return value.asDecimal(exponent: KlyWalletService.currencyExponent) } - + var feeValue: Decimal? { let value = BigUInt(self.fee) ?? BigUInt(0) - + return value.asDecimal(exponent: KlyWalletService.currencyExponent) } - + var confirmationsValue: String? { guard let confirmations = confirmations, - let height = height, - confirmations >= height + let height = height, + confirmations >= height else { return "0" } - + if confirmations > 0 { return "\(confirmations - height + 1)" } - + return "\(confirmations)" } - + var blockHeight: UInt64? { return self.height } - + var blockValue: String? { return self.blockId } - + var transactionStatus: TransactionStatus? { guard let confirmations = confirmations, - let height = height, - confirmations > .zero + let height = height, + confirmations > .zero else { return .notInitiated } - + if confirmations < height { return .registered } - + guard executionStatus != .failed else { return .failed } - + if confirmations > 0 && height > 0 { let conf = (confirmations - height) + 1 if conf > 1 { @@ -109,7 +109,7 @@ extension Transactions.TransactionModel: TransactionDetails, @unchecked @retroac } return .notInitiated } - + var senderAddress: String { return self.senderId } @@ -121,7 +121,7 @@ extension Transactions.TransactionModel: TransactionDetails, @unchecked @retroac var sentDate: Date? { timestamp.map { Date(timeIntervalSince1970: TimeInterval($0)) } } - + var txBlockchainComment: String? { txData } @@ -130,108 +130,108 @@ extension Transactions.TransactionModel: TransactionDetails, @unchecked @retroac extension LocalTransaction: TransactionDetails, @unchecked @retroactive Sendable { var defaultCurrencySymbol: String? { KlyWalletService.currencySymbol } - + var txId: String { return id ?? "" } - + var senderAddress: String { return "" } - + var recipientAddress: String { return self.recipientId ?? "" } - + var dateValue: Date? { return Date(timeIntervalSince1970: TimeInterval(self.timestamp)) } - + var amountValue: Decimal? { let value = BigUInt(self.amount) - + return value.asDecimal(exponent: KlyWalletService.currencyExponent) } - + var feeValue: Decimal? { let value = BigUInt(self.fee) - + return value.asDecimal(exponent: KlyWalletService.currencyExponent) } - + var confirmationsValue: String? { return nil } - + var blockHeight: UInt64? { return nil } - + var blockValue: String? { return nil } - + var isOutgoing: Bool { return true } - + var transactionStatus: TransactionStatus? { return .notInitiated } - + var nonceRaw: String? { nil } } extension TransactionEntity: TransactionDetails { - + var defaultCurrencySymbol: String? { KlyWalletService.currencySymbol } - + var txId: String { return id } - + var recipientAddress: String { recipientAddressBase32 } - + var dateValue: Date? { return nil } - + var amountValue: Decimal? { let value = BigUInt(self.params.amount) - + return value.asDecimal(exponent: KlyWalletService.currencyExponent) } - + var feeValue: Decimal? { let value = BigUInt(self.fee) - + return value.asDecimal(exponent: KlyWalletService.currencyExponent) } - + var confirmationsValue: String? { return nil } - + var blockHeight: UInt64? { return nil } - + var blockValue: String? { return nil } - + var isOutgoing: Bool { return true } - + var transactionStatus: TransactionStatus? { return .notInitiated } - + var nonceRaw: String? { String(nonce) } diff --git a/Adamant/Modules/Wallets/Klayr/KlyTransferViewController.swift b/Adamant/Modules/Wallets/Klayr/KlyTransferViewController.swift index 331fd708a..a6b23baaa 100644 --- a/Adamant/Modules/Wallets/Klayr/KlyTransferViewController.swift +++ b/Adamant/Modules/Wallets/Klayr/KlyTransferViewController.swift @@ -6,59 +6,60 @@ // Copyright © 2024 Adamant. All rights reserved. // -import UIKit +import CommonKit import Eureka import LiskKit -import CommonKit +import UIKit @MainActor final class KlyTransferViewController: TransferViewControllerBase { - + // MARK: Properties - + private let prefix = "kly" - + override var blockchainCommentsEnabled: Bool { !commentsEnabled } - + override var transactionFee: Decimal { - let blockchainComment: String = (form.rowBy( - tag: BaseRows.blockchainComments( - coin: walletCore.tokenName - ).tag - ) as? TextAreaRow)?.value ?? .empty - + let blockchainComment: String = + (form.rowBy( + tag: BaseRows.blockchainComments( + coin: walletCore.tokenName + ).tag + ) as? TextAreaRow)?.value ?? .empty + let baseFee = walletCore.getFee(comment: blockchainComment) let additionalyFee = walletCore.additionalFee - + return addAdditionalFee - ? baseFee + additionalyFee - : baseFee + ? baseFee + additionalyFee + : baseFee } - + override func checkForAdditionalFee() { Task { guard let recipientAddress = recipientAddress, - validateRecipient(recipientAddress).isValid + validateRecipient(recipientAddress).isValid else { addAdditionalFee = false return } - + let exist = try await walletCore.isExist(address: recipientAddress) - + guard !exist else { addAdditionalFee = false return } - + addAdditionalFee = true } } - + // MARK: Send - + @MainActor override func sendFunds() { let comments: String @@ -67,32 +68,33 @@ final class KlyTransferViewController: TransferViewControllerBase { } else { comments = "" } - - let blockchainComment: String? = (form.rowBy( - tag: BaseRows.blockchainComments( - coin: walletCore.tokenName - ).tag - ) as? TextAreaRow)?.value - + + let blockchainComment: String? = + (form.rowBy( + tag: BaseRows.blockchainComments( + coin: walletCore.tokenName + ).tag + ) as? TextAreaRow)?.value + guard let service = walletCore as? KlyWalletService, - let recipient = recipientAddress, - let amount = amount + let recipient = recipientAddress, + let amount = amount else { return } - + dialogService.showProgress(withMessage: String.adamant.transfer.transferProcessingMessage, userInteractionEnable: false) - + Task { do { // Create transaction let transaction = try await service.createTransaction( recipient: recipient, amount: amount, - fee: transactionFee, + fee: transactionFee, comment: blockchainComment ) - + if await !doesNotContainSendingTx( with: String(transaction.nonce), senderAddress: transaction.senderAddress @@ -100,7 +102,7 @@ final class KlyTransferViewController: TransferViewControllerBase { presentSendingError() return } - + // Send adm report if let reportRecipient = admReportRecipient { try await reportTransferTo( @@ -110,7 +112,7 @@ final class KlyTransferViewController: TransferViewControllerBase { hash: transaction.id ) } - + do { service.coinStorage.append(transaction) try await service.sendTransaction(transaction) @@ -119,15 +121,15 @@ final class KlyTransferViewController: TransferViewControllerBase { for: transaction.id, status: .failed ) - + throw error } - + service.update() - + dialogService.dismissProgress() dialogService.showSuccess(withMessage: String.adamant.transfer.transferSuccess) - + // Present detail VC presentDetailTransactionVC( transactionId: transaction.id, @@ -141,7 +143,7 @@ final class KlyTransferViewController: TransferViewControllerBase { } } } - + private func presentDetailTransactionVC( transactionId: String, transaction: TransactionEntity, @@ -152,24 +154,24 @@ final class KlyTransferViewController: TransferViewControllerBase { detailsVc.transaction = transaction detailsVc.senderName = String.adamant.transactionDetails.yourAddress detailsVc.recipientName = recipientName - + if comments.count > 0 { detailsVc.comment = comments } - + delegate?.transferViewController( self, didFinishWithTransfer: transaction, detailsViewController: detailsVc ) } - + // MARK: Overrides - + override var recipientAddress: String? { set { let _recipient = newValue?.addPrefixIfNeeded(prefix: prefix) - + if let row: TextRow = form.rowBy(tag: BaseRows.address.tag) { row.value = _recipient row.updateCell() @@ -180,12 +182,12 @@ final class KlyTransferViewController: TransferViewControllerBase { return row?.value?.addPrefixIfNeeded(prefix: prefix) } } - + override func validateRecipient(_ address: String) -> AddressValidationResult { let fixedAddress = address.addPrefixIfNeeded(prefix: prefix) return walletCore.validate(address: fixedAddress) } - + override func recipientRow() -> BaseRow { let row = TextRow { $0.tag = BaseRows.address.tag @@ -193,7 +195,7 @@ final class KlyTransferViewController: TransferViewControllerBase { $0.cell.textField.keyboardType = .namePhonePad $0.cell.textField.autocorrectionType = .no $0.cell.textField.setLineBreakMode() - + $0.value = recipientAddress?.components( separatedBy: TransferViewControllerBase.invalidCharacters ).joined() @@ -201,13 +203,13 @@ final class KlyTransferViewController: TransferViewControllerBase { let prefixLabel = UILabel() prefixLabel.text = prefix prefixLabel.sizeToFit() - + let view = UIView() view.addSubview(prefixLabel) view.frame = prefixLabel.frame $0.cell.textField.leftView = view $0.cell.textField.leftViewMode = .always - + if recipientIsReadonly { $0.disabled = true $0.cell.textField.isEnabled = false @@ -217,11 +219,11 @@ final class KlyTransferViewController: TransferViewControllerBase { cell.textField.text = row.value?.components( separatedBy: TransferViewControllerBase.invalidCharacters ).joined() - + self?.checkForAdditionalFee() - + guard self?.recipientIsReadonly == false else { return } - + cell.textField.leftView?.subviews.forEach { view in guard let label = view as? UILabel else { return } label.textColor = UIColor.adamant.primary @@ -230,19 +232,20 @@ final class KlyTransferViewController: TransferViewControllerBase { defer { self?.updateToolbar(for: row) } - + guard let text = row.value else { return } - + var trimmed = text.components( separatedBy: TransferViewControllerBase.invalidCharacters ).joined() - + if let prefix = self?.prefix, - trimmed.starts(with: prefix) { + trimmed.starts(with: prefix) + { let i = trimmed.index(trimmed.startIndex, offsetBy: prefix.count) trimmed = String(trimmed[i...]) } - + if text != trimmed { DispatchQueue.main.async { row.value = trimmed @@ -255,7 +258,7 @@ final class KlyTransferViewController: TransferViewControllerBase { return row } - + override func defaultSceneTitle() -> String? { String.adamant.sendKly } diff --git a/Adamant/Modules/Wallets/Klayr/KlyWallet.swift b/Adamant/Modules/Wallets/Klayr/KlyWallet.swift index 2c1fec650..6322df5ca 100644 --- a/Adamant/Modules/Wallets/Klayr/KlyWallet.swift +++ b/Adamant/Modules/Wallets/Klayr/KlyWallet.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation import LiskKit final class KlyWallet: WalletAccount, @unchecked Sendable { @@ -15,7 +15,7 @@ final class KlyWallet: WalletAccount, @unchecked Sendable { let legacyAddress: String let kly32Address: String let keyPair: KeyPair - + @Atomic var balance: Decimal = 0.0 @Atomic var notifications: Int = 0 @Atomic var isNewApi: Bool = true @@ -23,15 +23,15 @@ final class KlyWallet: WalletAccount, @unchecked Sendable { @Atomic var minBalance: Decimal = 0.05 @Atomic var minAmount: Decimal = 0 @Atomic var isBalanceInitialized: Bool = false - + var address: String { return isNewApi ? kly32Address : legacyAddress } var binaryAddress: String { - return isNewApi - ? LiskKit.Crypto.getBinaryAddressFromBase32(kly32Address) ?? .empty - : legacyAddress + return isNewApi + ? LiskKit.Crypto.getBinaryAddressFromBase32(kly32Address) ?? .empty + : legacyAddress } init( diff --git a/Adamant/Modules/Wallets/Klayr/KlyWalletFactory.swift b/Adamant/Modules/Wallets/Klayr/KlyWalletFactory.swift index f09d2701a..80a98c0c1 100644 --- a/Adamant/Modules/Wallets/Klayr/KlyWalletFactory.swift +++ b/Adamant/Modules/Wallets/Klayr/KlyWalletFactory.swift @@ -6,17 +6,17 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Swinject -import UIKit import CommonKit import LiskKit +import Swinject +import UIKit struct KlyWalletFactory: WalletFactory { typealias Service = WalletService - + let typeSymbol: String = KlyWalletService.richMessageType let assembler: Assembler - + func makeWalletVC(service: Service, screensFactory: ScreensFactory) -> WalletViewController { KlyWalletViewController( dialogService: assembler.resolve(DialogService.self)!, @@ -27,7 +27,7 @@ struct KlyWalletFactory: WalletFactory { service: service ) } - + func makeTransferListVC(service: Service, screensFactory: ScreensFactory) -> UIViewController { KlyTransactionsViewController( walletService: service, @@ -36,7 +36,7 @@ struct KlyWalletFactory: WalletFactory { screensFactory: screensFactory ) } - + func makeTransferVC(service: Service, screensFactory: ScreensFactory) -> TransferViewControllerBase { KlyTransferViewController( chatsProvider: assembler.resolve(ChatsProvider.self)!, @@ -52,18 +52,18 @@ struct KlyWalletFactory: WalletFactory { apiServiceCompose: assembler.resolve(ApiServiceComposeProtocol.self)! ) } - + func makeDetailsVC(service: Service, transaction: RichMessageTransaction) -> UIViewController? { guard let hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) else { return nil } - + let comment: String? if let raw = transaction.getRichValue(for: RichContentKeys.transfer.comments), raw.count > 0 { comment = raw } else { comment = nil } - + return makeTransactionDetailsVC( hash: hash, senderId: transaction.senderId, @@ -76,13 +76,13 @@ struct KlyWalletFactory: WalletFactory { service: service ) } - + func makeDetailsVC(service: Service) -> TransactionDetailsViewControllerBase { makeTransactionDetailsVC(service: service) } } -private extension KlyWalletFactory { +extension KlyWalletFactory { private func makeTransactionDetailsVC( hash: String, senderId: String?, @@ -98,15 +98,16 @@ private extension KlyWalletFactory { vc.senderId = senderId vc.recipientId = recipientId vc.comment = comment - + let amount: Decimal if let amountRaw = richTransaction.getRichValue(for: RichContentKeys.transfer.amount), - let decimal = Decimal(string: amountRaw) { + let decimal = Decimal(string: amountRaw) + { amount = decimal } else { amount = 0 } - + let failedTransaction = SimpleTransactionDetails( txId: hash, senderAddress: senderAddress, @@ -125,13 +126,13 @@ private extension KlyWalletFactory { vc.richTransaction = richTransaction return vc } - - func makeTransactionDetailsVC(service: Service) -> KlyTransactionDetailsViewController { + + fileprivate func makeTransactionDetailsVC(service: Service) -> KlyTransactionDetailsViewController { KlyTransactionDetailsViewController( dialogService: assembler.resolve(DialogService.self)!, currencyInfo: assembler.resolve(InfoServiceProtocol.self)!, addressBookService: assembler.resolve(AddressBookService.self)!, - accountService: assembler.resolve(AccountService.self)!, + accountService: assembler.resolve(AccountService.self)!, walletService: service, languageService: assembler.resolve(LanguageStorageProtocol.self)! ) diff --git a/Adamant/Modules/Wallets/Klayr/KlyWalletViewController.swift b/Adamant/Modules/Wallets/Klayr/KlyWalletViewController.swift index 3a3934c90..83026364f 100644 --- a/Adamant/Modules/Wallets/Klayr/KlyWalletViewController.swift +++ b/Adamant/Modules/Wallets/Klayr/KlyWalletViewController.swift @@ -6,14 +6,14 @@ // Copyright © 2024 Adamant. All rights reserved. // -import UIKit import CommonKit +import UIKit extension String.adamant { static var kly: String { String.localized("AccountTab.Wallets.kly_wallet", comment: "Account tab: Klayr wallet") } - + static var sendKly: String { String.localized("AccountTab.Row.SendKly", comment: "Account tab: 'Send KLY tokens' button") } @@ -23,11 +23,11 @@ final class KlyWalletViewController: WalletViewControllerBase { override func sendRowLocalizedLabel() -> NSAttributedString { return NSAttributedString(string: String.adamant.sendKly) } - + override func encodeForQr(address: String) -> String? { return "klayr:\(address)" } - + override func setTitle() { walletTitleLabel.text = String.adamant.kly } diff --git a/Adamant/Modules/Wallets/Klayr/WalletService/KlyTransactionFactory.swift b/Adamant/Modules/Wallets/Klayr/WalletService/KlyTransactionFactory.swift new file mode 100644 index 000000000..5e3fd29af --- /dev/null +++ b/Adamant/Modules/Wallets/Klayr/WalletService/KlyTransactionFactory.swift @@ -0,0 +1,30 @@ +// +// KlyTransactionFactory.swift +// Adamant +// +// Created by Christian Benua on 21.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import Foundation +import LiskKit + +final class KlyTransactionFactory: KlyTransactionFactoryProtocol { + func createTx( + amount: Decimal, + fee: Decimal, + nonce: UInt64, + senderPublicKey: String, + recipientAddressBinary: String, + comment: String + ) -> TransactionEntity { + TransactionEntity().createTx( + amount: amount, + fee: fee, + nonce: nonce, + senderPublicKey: senderPublicKey, + recipientAddressBinary: recipientAddressBinary, + comment: comment + ) + } +} diff --git a/Adamant/Modules/Wallets/Klayr/WalletService/KlyTransactionFactoryProtocol.swift b/Adamant/Modules/Wallets/Klayr/WalletService/KlyTransactionFactoryProtocol.swift new file mode 100644 index 000000000..7f6785333 --- /dev/null +++ b/Adamant/Modules/Wallets/Klayr/WalletService/KlyTransactionFactoryProtocol.swift @@ -0,0 +1,22 @@ +// +// KlyTransactionFactoryProtocol.swift +// Adamant +// +// Created by Christian Benua on 21.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import Foundation +import LiskKit + +// sourcery: AutoMockable +protocol KlyTransactionFactoryProtocol: AnyObject { + func createTx( + amount: Decimal, + fee: Decimal, + nonce: UInt64, + senderPublicKey: String, + recipientAddressBinary: String, + comment: String + ) -> TransactionEntity +} diff --git a/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService+RichMessageProvider.swift b/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService+RichMessageProvider.swift index 5a341130c..8e490f067 100644 --- a/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService+RichMessageProvider.swift +++ b/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService+RichMessageProvider.swift @@ -6,58 +6,58 @@ // Copyright © 2024 Adamant. All rights reserved. // +import CommonKit import Foundation import LiskKit -import CommonKit extension KlyWalletService { var newPendingInterval: TimeInterval { .init(milliseconds: type(of: self).newPendingInterval) } - + var oldPendingInterval: TimeInterval { .init(milliseconds: type(of: self).oldPendingInterval) } - + var registeredInterval: TimeInterval { .init(milliseconds: type(of: self).registeredInterval) } - + var newPendingAttempts: Int { type(of: self).newPendingAttempts } - + var oldPendingAttempts: Int { type(of: self).oldPendingAttempts } - + var dynamicRichMessageType: String { return type(of: self).richMessageType } - + // MARK: Short description - + func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString { let amount: String - + guard let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount) else { return NSAttributedString(string: "⬅️ \(KlyWalletService.currencySymbol)") } - + if let decimal = Decimal(string: raw) { amount = AdamantBalanceFormat.full.format(decimal) } else { amount = raw } - + let string: String if transaction.isOutgoing { string = "⬅️ \(amount) \(KlyWalletService.currencySymbol)" } else { string = "➡️ \(amount) \(KlyWalletService.currencySymbol)" } - + return NSAttributedString(string: string) } } diff --git a/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService+Send.swift b/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService+Send.swift index 7aec6e115..0eb5b65ba 100644 --- a/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService+Send.swift +++ b/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService+Send.swift @@ -6,13 +6,13 @@ // Copyright © 2024 Adamant. All rights reserved. // -import UIKit -@preconcurrency import LiskKit import CommonKit +@preconcurrency import LiskKit +import UIKit extension KlyWalletService: WalletServiceTwoStepSend { typealias T = TransactionEntity - + // MARK: Create & Send func createTransaction( recipient: String, @@ -21,17 +21,19 @@ extension KlyWalletService: WalletServiceTwoStepSend { comment: String? ) async throws -> TransactionEntity { // MARK: 1. Prepare - guard let wallet = klyWallet, - let binaryAddress = LiskKit.Crypto.getBinaryAddressFromBase32(recipient) - else { + guard let wallet = klyWallet else { throw WalletServiceError.notLogged } - + + guard let binaryAddress = LiskKit.Crypto.getBinaryAddressFromBase32(recipient) else { + throw WalletServiceError.accountNotFound + } + let keys = wallet.keyPair - + // MARK: 2. Create local transaction - - let transaction = TransactionEntity().createTx( + + let transaction = klyTransactionFactory.createTx( amount: amount, fee: fee, nonce: wallet.nonce, @@ -39,11 +41,11 @@ extension KlyWalletService: WalletServiceTwoStepSend { recipientAddressBinary: binaryAddress, comment: comment ?? .empty ) - + let signedTransaction = transaction.sign(with: keys, for: Constants.chainID) return signedTransaction } - + func sendTransaction(_ transaction: TransactionEntity) async throws { _ = try await klyNodeApiService.requestTransactionsApi { api in try await api.submit(transaction: transaction) diff --git a/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService+StatusCheck.swift b/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService+StatusCheck.swift index 17888d632..9fdbf0d95 100644 --- a/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService+StatusCheck.swift +++ b/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService+StatusCheck.swift @@ -6,33 +6,33 @@ // Copyright © 2024 Adamant. All rights reserved. // -import LiskKit import CommonKit +import LiskKit extension KlyWalletService { func statusInfoFor(transaction: CoinTransaction) async -> TransactionStatusInfo { let hash: String? - + if let transaction = transaction as? RichMessageTransaction { hash = transaction.getRichValue(for: RichContentKeys.transfer.hash) } else { hash = transaction.txId } - + guard let hash = hash else { return .init(sentDate: nil, status: .inconsistent(.wrongTxHash)) } - + var klyTransaction: Transactions.TransactionModel - + do { klyTransaction = try await getTransaction(by: hash, waitsForConnectivity: true) } catch { return .init(error: error) } - + klyTransaction.updateConfirmations(value: lastHeight) - + return await .init( sentDate: klyTransaction.sentDate, status: getStatus( @@ -43,53 +43,53 @@ extension KlyWalletService { } } -private extension KlyWalletService { - func getStatus( +extension KlyWalletService { + fileprivate func getStatus( klyTransaction: Transactions.TransactionModel, transaction: CoinTransaction ) async -> TransactionStatus { guard klyTransaction.blockId != nil else { return .registered } - + guard klyTransaction.executionStatus != .failed else { return .failed } - + guard let status = klyTransaction.transactionStatus else { return .inconsistent(.unknown) } - + guard status == .success else { return status } - + // MARK: Check address - + var realSenderAddress = klyTransaction.senderAddress var realRecipientAddress = klyTransaction.recipientAddress - + if transaction is RichMessageTransaction { guard let senderAddress = try? await getWalletAddress(byAdamantAddress: transaction.senderAddress) else { return .inconsistent(.senderCryptoAddressUnavailable(tokenSymbol)) } - + guard let recipientAddress = try? await getWalletAddress(byAdamantAddress: transaction.recipientAddress) else { return .inconsistent(.recipientCryptoAddressUnavailable(tokenSymbol)) } - + realSenderAddress = senderAddress realRecipientAddress = recipientAddress } - + guard klyTransaction.senderAddress.caseInsensitiveCompare(realSenderAddress) == .orderedSame else { return .inconsistent(.senderCryptoAddressMismatch(tokenSymbol)) } - + guard klyTransaction.recipientAddress.caseInsensitiveCompare(realRecipientAddress) == .orderedSame else { return .inconsistent(.recipientCryptoAddressMismatch(tokenSymbol)) } - + if transaction.isOutgoing { guard klyWallet?.address.caseInsensitiveCompare(klyTransaction.senderAddress) == .orderedSame else { return .inconsistent(.senderCryptoAddressMismatch(tokenSymbol)) @@ -99,30 +99,33 @@ private extension KlyWalletService { return .inconsistent(.recipientCryptoAddressMismatch(tokenSymbol)) } } - + // MARK: Check amount - guard isAmountCorrect( - transaction: transaction, - klyTransaction: klyTransaction - ) else { return .inconsistent(.wrongAmount) } - + guard + isAmountCorrect( + transaction: transaction, + klyTransaction: klyTransaction + ) + else { return .inconsistent(.wrongAmount) } + return .success } - - func isAmountCorrect( + + fileprivate func isAmountCorrect( transaction: CoinTransaction, klyTransaction: Transactions.TransactionModel ) -> Bool { if let transaction = transaction as? RichMessageTransaction, - let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), - let reported = AdamantBalanceFormat.deserializeBalance(from: raw) { - let min = reported - reported*0.005 - let max = reported + reported*0.005 - + let raw = transaction.getRichValue(for: RichContentKeys.transfer.amount), + let reported = AdamantBalanceFormat.deserializeBalance(from: raw) + { + let min = reported - reported * 0.005 + let max = reported + reported * 0.005 + let amount = klyTransaction.amountValue ?? 0 return amount <= max && amount >= min } - + return transaction.amountValue == klyTransaction.amountValue } } diff --git a/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService+WalletCore.swift b/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService+WalletCore.swift index f01b19554..c064b1f51 100644 --- a/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService+WalletCore.swift +++ b/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService+WalletCore.swift @@ -6,63 +6,63 @@ // Copyright © 2024 Adamant. All rights reserved. // -import UIKit import CommonKit import LiskKit +import UIKit extension KlyWalletService { var wallet: WalletAccount? { klyWallet } - + var tokenSymbol: String { Self.currencySymbol } - + var tokenLogo: UIImage { Self.currencyLogo } - + static var tokenNetworkSymbol: String { Self.currencySymbol } - + var tokenContract: String { .empty } - - var tokenUnicID: String { + + var tokenUniqueID: String { Self.tokenNetworkSymbol + tokenSymbol } - + var qqPrefix: String { Self.qqPrefix } - + var additionalFee: Decimal { 0.05 } - + var nodeGroups: [NodeGroup] { [.klyNode, .klyService] } - + var transactionFee: Decimal { transactionFeeRaw.asDecimal(exponent: KlyWalletService.currencyExponent) } - + var richMessageType: String { Self.richMessageType } - + var transactionsPublisher: AnyObservable<[TransactionDetails]> { $transactions.eraseToAnyPublisher() } - + var hasMoreOldTransactionsPublisher: AnyObservable { $hasMoreOldTransactions.eraseToAnyPublisher() } - + var explorerAddress: String { Self.explorerAddress } @@ -72,23 +72,23 @@ extension KlyWalletService: PrivateKeyGenerator { var rowTitle: String { tokenName } - + var rowImage: UIImage? { .asset(named: "klayr_wallet_row") } - + var keyFormat: KeyFormat { .HEX } - + func generatePrivateKeyFor(passphrase: String) -> String? { guard AdamantUtilities.validateAdamantPassphrase(passphrase), - let keypair = try? LiskKit.Crypto.keyPair( + let keypair = try? LiskKit.Crypto.keyPair( fromPassphrase: passphrase, salt: salt - ) + ) else { return nil } - + return keypair.privateKeyString } } diff --git a/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService.swift b/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService.swift index 045164a30..6ec4e6814 100644 --- a/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService.swift +++ b/Adamant/Modules/Wallets/Klayr/WalletService/KlyWalletService.swift @@ -6,43 +6,54 @@ // Copyright © 2024 Adamant. All rights reserved. // +import Combine +import CommonKit import Foundation +@preconcurrency import LiskKit import Swinject import UIKit -import CommonKit -import Combine + @preconcurrency import struct BigInt.BigUInt -@preconcurrency import LiskKit -final class KlyWalletService: WalletCoreProtocol, @unchecked Sendable { +final class KlyWalletService: WalletCoreProtocol, WalletStaticCoreProtocol, @unchecked Sendable { + static let currencySymbol = "KLY" struct CurrentFee: Sendable { let fee: BigUInt let lastHeight: UInt64 let minFeePerByte: UInt64 } - + // MARK: Dependencies - + var apiService: AdamantApiServiceProtocol! - var klyNodeApiService: KlyNodeApiService! + var klyNodeApiService: KlyNodeApiServiceProtocol! var klyServiceApiService: KlyServiceApiService! + var klyTransactionFactory: KlyTransactionFactoryProtocol! var accountService: AccountService! var dialogService: DialogService! var vibroService: VibroService! var coreDataStack: CoreDataStack! - + // MARK: Proprieties - + static let richMessageType = "kly_transaction" static let currencyLogo = UIImage.asset(named: "klayr_wallet") ?? .init() static let kvsAddress = "kly:address" static let defaultFee: BigUInt = 141000 - + static var serviceNodes: [CommonKit.Node] { + coinInfo?.services?.klyService?.list.map { serviceNode in + Node.makeDefaultNode( + url: URL(string: serviceNode.url)!, + altUrl: serviceNode.altIP.flatMap { URL(string: $0) } + ) + } ?? [] + } + @MainActor var hasEnabledNode: Bool { klyNodeApiService.hasEnabledNode && klyServiceApiService.hasEnabledNode } - + @MainActor var hasEnabledNodePublisher: AnyObservable { klyNodeApiService.hasEnabledNodePublisher @@ -56,7 +67,7 @@ final class KlyWalletService: WalletCoreProtocol, @unchecked Sendable { @Atomic private var cachedWalletAddress: [String: String] = [:] @Atomic private var subscriptions = Set() @Atomic private var balanceObserver: AnyCancellable? - + @Atomic private(set) var klyWallet: KlyWallet? @Atomic private(set) var enabled = true @Atomic private(set) var isWarningGasPrice = false @@ -64,109 +75,117 @@ final class KlyWalletService: WalletCoreProtocol, @unchecked Sendable { @Atomic private(set) var lastHeight: UInt64 = .zero @Atomic private(set) var lastMinFeePerByte: UInt64 = .zero @Atomic private var balanceInvalidationSubscription: AnyCancellable? - + @ObservableValue private(set) var transactions: [TransactionDetails] = [] @ObservableValue private(set) var hasMoreOldTransactions: Bool = true - + private(set) lazy var coinStorage: CoinStorageService = AdamantCoinStorageService( - coinId: tokenUnicID, + coinId: tokenUniqueID, coreDataStack: coreDataStack, blockchainType: richMessageType ) - + let salt = "adm" - + // MARK: Notifications - + let walletUpdatedNotification = Notification.Name("adamant.klyWallet.walletUpdated") let serviceEnabledChanged = Notification.Name("adamant.klyWallet.enabledChanged") let transactionFeeUpdated = Notification.Name("adamant.klyWallet.feeUpdated") let serviceStateChanged = Notification.Name("adamant.klyWallet.stateChanged") - + + @MainActor + private let walletUpdateSender = ObservableSender() + @MainActor + var walletUpdatePublisher: AnyObservable { + walletUpdateSender.eraseToAnyPublisher() + } + init() { addObservers() } - + // MARK: - - + func initWallet( - withPassphrase passphrase: String + withPassphrase passphrase: String, + withPassword password: String ) async throws -> WalletAccount { - try await initWallet(passphrase: passphrase) + try await initWallet(passphrase: passphrase, password: password) } - + func setInitiationFailed(reason: String) { setState(.initiationFailed(reason: reason)) klyWallet = nil } - + func update() { Task { await update() } } - + func updateStatus(for id: String, status: TransactionStatus?) { coinStorage.updateStatus(for: id, status: status) } - + func validate(address: String) -> AddressValidationResult { LiskKit.Crypto.isValidBase32(address: address) - ? .valid - : .invalid(description: nil) + ? .valid + : .invalid(description: nil) } - + func getBalance(address: String) async throws -> Decimal { try await getBalance(for: address) } - + func getCurrentFee() async throws -> CurrentFee { try await getFees(comment: .empty) } - + func getFee(comment: String) -> Decimal { let fee = try? getFee( minFeePerByte: lastMinFeePerByte, comment: comment ).asDecimal(exponent: Self.currencyExponent) - + return fee ?? transactionFee } - + func getWalletAddress(byAdamantAddress address: String) async throws -> String { try await getKlyWalletAddress(byAdamantAddress: address) } - + func getLocalTransactionHistory() -> [TransactionDetails] { transactions } - + func getTransactionsHistory( offset: Int, limit: Int ) async throws -> [TransactionDetails] { try await getTransactions(offset: UInt(offset), limit: UInt(limit)) } - + func loadTransactions(offset: Int, limit: Int) async throws -> Int { let trs = try await getTransactionsHistory(offset: offset, limit: limit) - + guard trs.count > 0 else { hasMoreOldTransactions = false return .zero } - + coinStorage.append(trs) return trs.count } - + func getTransaction( by hash: String, waitsForConnectivity: Bool ) async throws -> Transactions.TransactionModel { try await getTransaction(hash: hash, waitsForConnectivity: waitsForConnectivity) } - + func isExist(address: String) async throws -> Bool { try await isAccountExist(with: address) } @@ -180,13 +199,14 @@ extension KlyWalletService: SwinjectDependentService { apiService = container.resolve(AdamantApiServiceProtocol.self) dialogService = container.resolve(DialogService.self) klyServiceApiService = container.resolve(KlyServiceApiService.self) + klyTransactionFactory = container.resolve(KlyTransactionFactoryProtocol.self) klyNodeApiService = container.resolve(KlyNodeApiService.self) vibroService = container.resolve(VibroService.self) coreDataStack = container.resolve(CoreDataStack.self) - + addTransactionObserver() } - + func addTransactionObserver() { coinStorage.transactionsPublisher .sink { [weak self] transactions in @@ -196,27 +216,27 @@ extension KlyWalletService: SwinjectDependentService { } } -private extension KlyWalletService { - func addObservers() { +extension KlyWalletService { + fileprivate func addObservers() { NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedIn, object: nil) .sink { @MainActor [weak self] _ in self?.update() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantAccountService.accountDataUpdated, object: nil) .sink { @MainActor [weak self] _ in self?.update() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedOut, object: nil) .sink { @MainActor [weak self] _ in self?.klyWallet = nil - + if let balanceObserver = self?.balanceObserver { NotificationCenter.default.removeObserver(balanceObserver) self?.balanceObserver = nil @@ -228,86 +248,93 @@ private extension KlyWalletService { } .store(in: &subscriptions) } - + @MainActor - func update() async { + fileprivate func update() async { guard let wallet = klyWallet else { return } - + switch state { case .notInitiated, .updating, .initiationFailed: return - + case .upToDate: break } - + setState(.updating) - + if let balance = try? await getBalance() { if wallet.balance < balance, wallet.isBalanceInitialized { vibroService.applyVibration(.success) } - + wallet.balance = balance markBalanceAsFresh(wallet) - - NotificationCenter.default.post( - name: walletUpdatedNotification, - object: self, - userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet] - ) + + walletUpdateSender.send() + } else { + wallet.isBalanceInitialized = false } - + + NotificationCenter.default.post( + name: walletUpdatedNotification, + object: self, + userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet] + ) + if let nonce = try? await getNonce(address: wallet.address) { wallet.nonce = nonce } - + if let result = try? await getFees(comment: .empty) { self.lastHeight = result.lastHeight - self.transactionFeeRaw = result.fee > KlyWalletService.defaultFee - ? result.fee - : KlyWalletService.defaultFee + self.transactionFeeRaw = + result.fee > KlyWalletService.defaultFee + ? result.fee + : KlyWalletService.defaultFee self.lastMinFeePerByte = result.minFeePerByte } - + setState(.upToDate) } - - func markBalanceAsFresh(_ wallet: KlyWallet) { + + fileprivate func markBalanceAsFresh(_ wallet: KlyWallet) { wallet.isBalanceInitialized = true - + balanceInvalidationSubscription = Task { [weak self] in try await Task.sleep(interval: Self.balanceLifetime, pauseInBackground: true) guard let self else { return } wallet.isBalanceInitialized = false - + NotificationCenter.default.post( name: walletUpdatedNotification, object: self, userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet] ) + + await walletUpdateSender.send() }.eraseToAnyCancellable() } } -private extension KlyWalletService { - func getBalance() async throws -> Decimal { +extension KlyWalletService { + fileprivate func getBalance() async throws -> Decimal { guard let address = klyWallet?.address else { throw WalletServiceError.notLogged } - + return try await getBalance(address: address) } - - func getBalance(for address: String) async throws -> Decimal { + + fileprivate func getBalance(for address: String) async throws -> Decimal { let result = await klyNodeApiService.requestAccountsApi { api in let balanceRaw = try await api.balance(address: address) let balance = BigUInt(balanceRaw?.availableBalance ?? "0") ?? .zero return balance } - + switch result { case let .success(balance): return balance.asDecimal(exponent: KlyWalletService.currencyExponent) @@ -315,24 +342,24 @@ private extension KlyWalletService { throw error } } - - func getNonce(address: String) async throws -> UInt64 { + + fileprivate func getNonce(address: String) async throws -> UInt64 { let nonce = try await klyNodeApiService.requestAccountsApi { api in try await api.nonce(address: address) }.get() - + return UInt64(nonce) ?? .zero } - func getFees(comment: String) async throws -> CurrentFee { + fileprivate func getFees(comment: String) async throws -> CurrentFee { guard let wallet = klyWallet else { throw WalletServiceError.notLogged } - + let minFeePerByte = try await klyNodeApiService.requestAccountsApi { api in try await api.getFees().minFeePerByte }.get() - + let tempTransaction = TransactionEntity().createTx( amount: 100000000.0, fee: 0.00141, @@ -344,24 +371,24 @@ private extension KlyWalletService { with: wallet.keyPair, for: Constants.chainID ) - + let feeValue = tempTransaction.getFee(with: minFeePerByte) let fee = BigUInt(feeValue) - + let lastBlock = try await klyNodeApiService.requestAccountsApi { api in try await api.lastBlock() }.get() - + let height = UInt64(lastBlock.header.height) - + return .init(fee: fee, lastHeight: height, minFeePerByte: minFeePerByte) } - - func getFee(minFeePerByte: UInt64, comment: String) throws -> BigUInt { + + fileprivate func getFee(minFeePerByte: UInt64, comment: String) throws -> BigUInt { guard let wallet = klyWallet else { throw WalletServiceError.notLogged } - + let tempTransaction = TransactionEntity().createTx( amount: 100000000.0, fee: 0.00141, @@ -373,22 +400,22 @@ private extension KlyWalletService { with: wallet.keyPair, for: Constants.chainID ) - + let feeValue = tempTransaction.getFee(with: minFeePerByte) let fee = BigUInt(feeValue) - + return fee } - - func setState(_ newState: WalletServiceState, silent: Bool = false) { + + fileprivate func setState(_ newState: WalletServiceState, silent: Bool = false) { guard newState != state else { return } - + state = newState - + guard !silent else { return } - + NotificationCenter.default.post( name: serviceStateChanged, object: self, @@ -398,72 +425,74 @@ private extension KlyWalletService { } // MARK: - Init Wallet -private extension KlyWalletService { - func initWallet(passphrase: String) async throws -> WalletAccount { +extension KlyWalletService { + fileprivate func initWallet(passphrase: String, password: String) async throws -> WalletAccount { guard let adamant = accountService.account else { throw WalletServiceError.notLogged } - + setState(.notInitiated) - + if enabled { enabled = false NotificationCenter.default.post(name: serviceEnabledChanged, object: self) } - + do { let keyPair = try LiskKit.Crypto.keyPair( fromPassphrase: passphrase, - salt: salt + salt: password.isEmpty ? salt : "mnemonic\(password)" ) - + let address = LiskKit.Crypto.address(fromPublicKey: keyPair.publicKeyString) - + let wallet = KlyWallet( - unicId: tokenUnicID, + unicId: tokenUniqueID, address: address, keyPair: keyPair, nonce: .zero, isNewApi: true ) self.klyWallet = wallet - + NotificationCenter.default.post( name: walletUpdatedNotification, object: self, userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet] ) + + await walletUpdateSender.send() } catch { throw WalletServiceError.accountNotFound } - + if !enabled { enabled = true NotificationCenter.default.post(name: serviceEnabledChanged, object: self) } - + guard let eWallet = klyWallet, let kvsAddressModel = makeKVSAddressModel(wallet: eWallet) else { throw WalletServiceError.accountNotFound } - + // Save into KVS - + do { let address = try await getWalletAddress(byAdamantAddress: adamant.address) - + if address != eWallet.address { updateKvsAddress(kvsAddressModel) } - + setState(.upToDate) - + Task { await update() } - + return eWallet } catch let error as WalletServiceError { switch error { @@ -471,13 +500,13 @@ private extension KlyWalletService { /// The ADM Wallet is not initialized. Check the balance of the current wallet /// and save the wallet address to kvs when dropshipping ADM setState(.upToDate) - + Task { await update() } - + updateKvsAddress(kvsAddressModel) - + return eWallet default: setState(.upToDate) @@ -485,8 +514,8 @@ private extension KlyWalletService { } } } - - func updateKvsAddress(_ model: KVSValueModel) { + + fileprivate func updateKvsAddress(_ model: KVSValueModel) { Task { do { try await save(model) @@ -498,18 +527,18 @@ private extension KlyWalletService { } } } - + /// New accounts doesn't have enought money to save KVS. We need to wait for balance update, and then - retry save - func kvsSaveProcessError( + fileprivate func kvsSaveProcessError( _ model: KVSValueModel, error: Error ) { guard let error = error as? WalletServiceError, - case .notEnoughMoney = error + case .notEnoughMoney = error else { return } - + balanceObserver?.cancel() - + balanceObserver = NotificationCenter.default .notifications(named: .AdamantAccountService.accountDataUpdated) .compactMap { [weak self] _ in @@ -518,64 +547,64 @@ private extension KlyWalletService { .filter { $0 > AdamantApiService.KvsFee } .sink { [weak self] _ in guard let self = self else { return } - + Task { try await self.save(model) self.balanceObserver?.cancel() } } } - + /// - Parameters: /// - klyAddress: Klayr address to save into KVS /// - adamantAddress: Owner of Klayr address - func save(_ model: KVSValueModel) async throws { + fileprivate func save(_ model: KVSValueModel) async throws { guard let adamant = accountService.account else { throw WalletServiceError.notLogged } - + guard adamant.balance >= AdamantApiService.KvsFee else { throw WalletServiceError.notEnoughMoney } - - let result = await apiService.store(model) - + + let result = await apiService.store(model, date: .now) + guard case .failure(let error) = result else { return } - + throw WalletServiceError.apiError(error) } - - func makeKVSAddressModel(wallet: WalletAccount) -> KVSValueModel? { + + fileprivate func makeKVSAddressModel(wallet: WalletAccount) -> KVSValueModel? { guard let keypair = accountService.keypair else { return nil } - + return .init( key: Self.kvsAddress, value: wallet.address, keypair: keypair ) } - - func getKlyWalletAddress( + + fileprivate func getKlyWalletAddress( byAdamantAddress address: String ) async throws -> String { if let address = cachedWalletAddress[address], !address.isEmpty { return address } - + do { let result = try await apiService.get( key: KlyWalletService.kvsAddress, sender: address ).get() - + guard let result = result else { throw WalletServiceError.walletNotInitiated } - + cachedWalletAddress[address] = result - + return result } catch _ as ApiServiceError { throw WalletServiceError.remoteServiceError( @@ -585,15 +614,24 @@ private extension KlyWalletService { } } -private extension KlyWalletService { - func getTransactions( +#if DEBUG + extension KlyWalletService { + @available(*, deprecated, message: "For testing purposes only") + func setWalletForTests(_ wallet: KlyWallet?) { + self.klyWallet = wallet + } + } +#endif + +extension KlyWalletService { + fileprivate func getTransactions( offset: UInt, limit: UInt = 100 ) async throws -> [Transactions.TransactionModel] { guard let address = self.klyWallet?.address else { throw WalletServiceError.internalError(message: "KLY Wallet: not found", error: nil) } - + return try await klyServiceApiService.requestServiceApi(waitsForConnectivity: false) { api, completion in api.transactions( ownerAddress: address, @@ -605,14 +643,14 @@ private extension KlyWalletService { ) }.get() } - - func getTransaction(hash: String, waitsForConnectivity: Bool) async throws -> Transactions.TransactionModel { + + fileprivate func getTransaction(hash: String, waitsForConnectivity: Bool) async throws -> Transactions.TransactionModel { guard !hash.isEmpty else { throw ApiServiceError.internalError(message: "No hash", error: nil) } - + let ownerAddress = klyWallet?.address - + let result = try await klyServiceApiService.requestServiceApi( waitsForConnectivity: waitsForConnectivity ) { api, completion in @@ -624,15 +662,15 @@ private extension KlyWalletService { completionHandler: completion ) }.get() - + if let transaction = result.first { return transaction } - + throw WalletServiceError.remoteServiceError(message: "No transaction") } - - func isAccountExist(with address: String) async throws -> Bool { + + fileprivate func isAccountExist(with address: String) async throws -> Bool { try await klyServiceApiService.requestServiceApi(waitsForConnectivity: false) { api in try await withUnsafeThrowingContinuation { continuation in api.exist(address: address) { result in diff --git a/Adamant/Modules/Wallets/Mappers/SimpleTransactionDetails+Hashable.swift b/Adamant/Modules/Wallets/Mappers/SimpleTransactionDetails+Hashable.swift index 8e51c6956..42b1e91d9 100644 --- a/Adamant/Modules/Wallets/Mappers/SimpleTransactionDetails+Hashable.swift +++ b/Adamant/Modules/Wallets/Mappers/SimpleTransactionDetails+Hashable.swift @@ -12,17 +12,19 @@ extension Sequence where Element == SimpleTransactionDetails { func wrappedByHashableId() -> [HashableIDWrapper] { var identifierTable: [String: Int] = [:] var result: [HashableIDWrapper] = [] - + forEach { item in let index = identifierTable[item.txId] ?? .zero identifierTable[item.txId] = index + 1 - - result.append(.init( - identifier: .init(identifier: item.txId, index: index), - value: item - )) + + result.append( + .init( + identifier: .init(identifier: item.txId, index: index), + value: item + ) + ) } - + return result } } diff --git a/Adamant/Modules/Wallets/Models/AdamantTransactionDetails.swift b/Adamant/Modules/Wallets/Models/AdamantTransactionDetails.swift index 6ee5b401a..c2f69bacc 100644 --- a/Adamant/Modules/Wallets/Models/AdamantTransactionDetails.swift +++ b/Adamant/Modules/Wallets/Models/AdamantTransactionDetails.swift @@ -11,49 +11,49 @@ import Foundation protocol AdamantTransactionDetails: TransactionDetails { /// The identifier of the transaction. var txId: String { get } - + /// The sender of the transaction. var senderAddress: String { get } - + /// The reciver of the transaction. var recipientAddress: String { get } - + /// The date the transaction was sent. var dateValue: Date? { get } - + /// The amount of currency that was sent. var amountValue: Decimal? { get } - + /// The amount of fee that taken for transaction process. var feeValue: Decimal? { get } - + /// The confirmations of the transaction. var confirmationsValue: String? { get } - + var blockHeight: UInt64? { get } - + /// The block of the transaction. var blockValue: String? { get } - + var isOutgoing: Bool { get } - + var transactionStatus: TransactionStatus? { get } - + var defaultCurrencySymbol: String? { get } - + var feeCurrencySymbol: String? { get } - + func summary( with url: String?, currentValue: String?, valueAtTimeTxn: String? ) -> String - + var partnerName: String? { get } - + var comment: String? { get } - + var showToChat: Bool? { get } - + var chatRoom: Chatroom? { get } } diff --git a/Adamant/Modules/Wallets/Models/TransactionDetails.swift b/Adamant/Modules/Wallets/Models/TransactionDetails.swift index 876e1610a..322111986 100644 --- a/Adamant/Modules/Wallets/Models/TransactionDetails.swift +++ b/Adamant/Modules/Wallets/Models/TransactionDetails.swift @@ -6,49 +6,49 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation /// A standard protocol representing a Transaction details. protocol TransactionDetails: Sendable { /// The identifier of the transaction. var txId: String { get } - + /// The sender of the transaction. var senderAddress: String { get } - + /// The reciver of the transaction. var recipientAddress: String { get } - + /// The date the transaction was sent. var dateValue: Date? { get } - + /// The amount of currency that was sent. var amountValue: Decimal? { get } - + /// The amount of fee that taken for transaction process. var feeValue: Decimal? { get } - + /// The confirmations of the transaction. var confirmationsValue: String? { get } - + var blockHeight: UInt64? { get } - + /// The block of the transaction. var blockValue: String? { get } - + var isOutgoing: Bool { get } - + var transactionStatus: TransactionStatus? { get } - + var defaultCurrencySymbol: String? { get } - + var feeCurrencySymbol: String? { get } - + var nonceRaw: String? { get } - + var txBlockchainComment: String? { get } - + func summary( with url: String?, currentValue: String?, @@ -58,58 +58,58 @@ protocol TransactionDetails: Sendable { extension TransactionDetails { var feeCurrencySymbol: String? { defaultCurrencySymbol } - + var txBlockchainComment: String? { nil } - + func summary( with url: String? = nil, currentValue: String? = nil, valueAtTimeTxn: String? = nil ) -> String { let symbol = self.defaultCurrencySymbol - + var summary = """ - Transaction \(txId) - - Summary - Sender: \(senderAddress) - Recipient: \(recipientAddress) - Amount: \(AdamantBalanceFormat.full.format(amountValue ?? 0, withCurrencySymbol: symbol)) - """ - + Transaction \(txId) + + Summary + Sender: \(senderAddress) + Recipient: \(recipientAddress) + Amount: \(AdamantBalanceFormat.full.format(amountValue ?? 0, withCurrencySymbol: symbol)) + """ + if let fee = feeValue { summary += "\nFee: \(AdamantBalanceFormat.full.format(fee, withCurrencySymbol: feeCurrencySymbol))" } - + if let date = dateValue { let dateString = DateFormatter.localizedString(from: date, dateStyle: .short, timeStyle: .medium) summary += "\nDate: \(dateString)" } - + if let confirmations = confirmationsValue { summary += "\nConfirmations: \(confirmations)" } - + if let block = blockValue { summary += "\nBlock: \(block)" } - + if let status = transactionStatus { summary += "\nStatus: \(status.localized)" } - + if let value = currentValue { summary += "\nCurrent value: \(value)" } - + if let value = valueAtTimeTxn { summary += "\nValue at time of Txn: \(value)" } - + if let url = url { summary += "\nURL: \(url)" } - + return summary } } diff --git a/Adamant/Modules/Wallets/TransactionDetailsViewControllerBase.swift b/Adamant/Modules/Wallets/TransactionDetailsViewControllerBase.swift index 73bbcd864..63ffccf0f 100644 --- a/Adamant/Modules/Wallets/TransactionDetailsViewControllerBase.swift +++ b/Adamant/Modules/Wallets/TransactionDetailsViewControllerBase.swift @@ -6,22 +6,22 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit +import CommonKit import Eureka import SafariServices -import CommonKit +import UIKit // MARK: - TransactionStatus UI -private extension TransactionStatus { - var color: UIColor { +extension TransactionStatus { + fileprivate var color: UIColor { switch self { case .failed: return .adamant.warning case .notInitiated, .inconsistent, .pending, .registered: return .adamant.attention case .success: return .adamant.success } } - - var descriptionLocalized: String? { + + fileprivate var descriptionLocalized: String? { switch self { case .inconsistent(let reason): return reason.localized @@ -47,9 +47,11 @@ extension String.adamant { } extension String.adamant.alert { - static var exportUrlButton: String { String.localized("TransactionDetailsScene.Share.URL", comment: "Export transaction: 'Share transaction URL' button") + static var exportUrlButton: String { + String.localized("TransactionDetailsScene.Share.URL", comment: "Export transaction: 'Share transaction URL' button") } - static var exportSummaryButton: String { String.localized("TransactionDetailsScene.Share.Summary", comment: "Export transaction: 'Share transaction summary' button") + static var exportSummaryButton: String { + String.localized("TransactionDetailsScene.Share.Summary", comment: "Export transaction: 'Share transaction summary' button") } } @@ -72,7 +74,7 @@ class TransactionDetailsViewControllerBase: FormViewController { case currentFiat case inconsistentReason case txBlockchainComment - + var tag: String { switch self { case .transactionNumber: return "id" @@ -93,7 +95,7 @@ class TransactionDetailsViewControllerBase: FormViewController { case .txBlockchainComment: return "data" } } - + var localized: String { switch self { case .transactionNumber: return .localized("TransactionDetailsScene.Row.Id", comment: "Transaction details: Id row.") @@ -117,23 +119,23 @@ class TransactionDetailsViewControllerBase: FormViewController { .localized("TransactionStatus.Inconsistent.RecordData.Title", comment: "Transaction details: Tx data record") } } - + var image: UIImage? { switch self { case .openInExplorer: return .asset(named: "row_explorer") case .openChat: return .asset(named: "row_chat") - + default: return nil } } } - + enum Sections { case details case comment case actions case inconsistentReason - + var localized: String { switch self { case .details: return "" @@ -143,7 +145,7 @@ class TransactionDetailsViewControllerBase: FormViewController { return .localized("TransactionStatus.Inconsistent.Reason.Title", comment: "Transaction status: inconsistent reason title") } } - + var tag: String { switch self { case .details: return "details" @@ -153,30 +155,30 @@ class TransactionDetailsViewControllerBase: FormViewController { } } } - + // MARK: - Dependencies - + let dialogService: DialogService let currencyInfo: InfoServiceProtocol let addressBookService: AddressBookService let accountService: AccountService let walletService: WalletService? let languageService: LanguageStorageProtocol - + // MARK: - Properties - + var showTxBlockchainComment: Bool { false } - + var transaction: TransactionDetails? { didSet { if !isFiatSet { self.updateFiat() } - + guard let id = transaction?.txId else { return } - + walletService?.core.updateStatus( for: id, status: transaction?.transactionStatus @@ -190,9 +192,9 @@ class TransactionDetailsViewControllerBase: FormViewController { dateFormatter.locale = Locale(identifier: languageService.getLanguage().locale) return dateFormatter }() - + static let awaitingValueString = TransactionStatus.notInitiated.localized - + private lazy var currencyFormatter: NumberFormatter = { return AdamantBalanceFormat.currencyFormatter(for: .full, currencySymbol: currencySymbol) }() @@ -200,57 +202,57 @@ class TransactionDetailsViewControllerBase: FormViewController { var feeFormatter: NumberFormatter { return currencyFormatter } - + private lazy var fiatFormatter: NumberFormatter = { return AdamantBalanceFormat.fiatFormatter(for: currencyInfo.currentCurrency) }() - + private var isFiatSet = false - + var feeCurrencySymbol: String? { currencySymbol } - + var transactionStatus: TransactionStatus? { guard let richTransaction = richTransaction, - let status = richTransaction.transactionStatus + let status = richTransaction.transactionStatus else { return transaction?.transactionStatus } - + return status } - + var refreshTask: Task<(), Never>? - + var richTransaction: RichMessageTransaction? - + var senderId: String? { didSet { senderName = getName(by: senderId) } } - + var recipientId: String? { didSet { recipientName = getName(by: recipientId) } } - + var valueAtTimeTxn: String? { didSet { updateValueAtTimeRowValue() } } - + var feeAtTimeTxn: String? { didSet { updateFeeAtTimeRowValue() } } - + // MARK: - Lifecycle - + init( dialogService: DialogService, currencyInfo: InfoServiceProtocol, @@ -265,39 +267,39 @@ class TransactionDetailsViewControllerBase: FormViewController { self.accountService = accountService self.walletService = walletService self.languageService = languageService - + super.init(style: .grouped) } - + required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func viewDidLoad() { super.viewDidLoad() - - navigationItem.largeTitleDisplayMode = .never // some glitches, again + + navigationItem.largeTitleDisplayMode = .never // some glitches, again navigationItem.title = String.adamant.transactionDetails.title navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(share)) navigationOptions = RowNavigationOptions.Disabled - + // MARK: - Transfer section let detailsSection = Section { $0.tag = Sections.details.tag } - + // MARK: Transaction number let idRow = LabelRow { $0.disabled = true $0.tag = Rows.transactionNumber.tag $0.title = Rows.transactionNumber.localized - + if let value = transaction?.txId { $0.value = value } else { $0.value = TransactionDetailsViewControllerBase.awaitingValueString } - + $0.cell.detailTextLabel?.textAlignment = .right $0.cell.detailTextLabel?.lineBreakMode = .byTruncatingMiddle }.cellSetup { (cell, _) in @@ -315,27 +317,28 @@ class TransactionDetailsViewControllerBase: FormViewController { row.value = TransactionDetailsViewControllerBase.awaitingValueString } } - + detailsSection.append(idRow) - + // MARK: Sender let senderRow = DoubleDetailsRow { [weak self] in $0.disabled = true $0.tag = Rows.from.tag $0.cell.titleLabel.text = Rows.from.localized - + if let transaction = self?.transaction { if let name = self?.senderName { $0.value = DoubleDetail(first: name, second: transaction.senderAddress) } else { - $0.value = transaction.senderAddress.isEmpty - ? DoubleDetail(first: Self.awaitingValueString, second: nil) - : DoubleDetail(first: transaction.senderAddress, second: nil) + $0.value = + transaction.senderAddress.isEmpty + ? DoubleDetail(first: Self.awaitingValueString, second: nil) + : DoubleDetail(first: transaction.senderAddress, second: nil) } } else { $0.value = nil } - + let height = self?.senderName != nil ? DoubleDetailsTableViewCell.fullHeight : DoubleDetailsTableViewCell.compactHeight $0.cell.height = { height } $0.cell.secondDetailsLabel?.textAlignment = .right @@ -349,14 +352,14 @@ class TransactionDetailsViewControllerBase: FormViewController { guard let value = row.value else { return } - + let text: String if let address = value.second { text = address } else { text = value.first } - + self?.shareValue(text, from: cell) }.cellUpdate { [weak self] (cell, row) in cell.textLabel?.textColor = UIColor.adamant.textColor @@ -364,23 +367,24 @@ class TransactionDetailsViewControllerBase: FormViewController { if let name = self?.senderName { row.value = DoubleDetail(first: name, second: transaction.senderAddress) } else { - row.value = transaction.senderAddress.isEmpty - ? DoubleDetail(first: Self.awaitingValueString, second: nil) - : DoubleDetail(first: transaction.senderAddress, second: nil) + row.value = + transaction.senderAddress.isEmpty + ? DoubleDetail(first: Self.awaitingValueString, second: nil) + : DoubleDetail(first: transaction.senderAddress, second: nil) } } else { row.value = nil } } - + detailsSection.append(senderRow) - + // MARK: Recipient let recipientRow = DoubleDetailsRow { [weak self] in $0.disabled = true $0.tag = Rows.to.tag $0.cell.titleLabel.text = Rows.to.localized - + if let transaction = self?.transaction { if let recipientName = self?.recipientName?.checkAndReplaceSystemWallets() { $0.value = DoubleDetail(first: recipientName, second: transaction.recipientAddress) @@ -393,7 +397,7 @@ class TransactionDetailsViewControllerBase: FormViewController { } else { $0.value = nil } - + let height = self?.recipientName != nil ? DoubleDetailsTableViewCell.fullHeight : DoubleDetailsTableViewCell.compactHeight $0.cell.height = { height } $0.cell.secondDetailsLabel?.textAlignment = .right @@ -407,18 +411,18 @@ class TransactionDetailsViewControllerBase: FormViewController { guard let value = row.value else { return } - + let text: String if let address = value.second { text = address } else { text = value.first } - + self?.shareValue(text, from: cell) }.cellUpdate { [weak self] (cell, row) in cell.textLabel?.textColor = UIColor.adamant.textColor - + if let transaction = self?.transaction { if let recipientName = self?.recipientName?.checkAndReplaceSystemWallets() { row.value = DoubleDetail(first: recipientName, second: transaction.recipientAddress) @@ -432,15 +436,15 @@ class TransactionDetailsViewControllerBase: FormViewController { row.value = nil } } - + detailsSection.append(recipientRow) - + // MARK: Date let dateRow = LabelRow { $0.disabled = true $0.tag = Rows.date.tag $0.title = Rows.date.localized - + if let raw = transaction?.dateValue { $0.value = dateFormatter.string(from: raw) } else { @@ -462,9 +466,9 @@ class TransactionDetailsViewControllerBase: FormViewController { row.value = TransactionDetailsViewControllerBase.awaitingValueString } } - + detailsSection.append(dateRow) - + // MARK: Amount let amountRow = LabelRow { $0.disabled = true @@ -490,9 +494,9 @@ class TransactionDetailsViewControllerBase: FormViewController { row.value = TransactionDetailsViewControllerBase.awaitingValueString } } - + detailsSection.append(amountRow) - + // MARK: Fee let feeRow = LabelRow { $0.disabled = true @@ -510,21 +514,21 @@ class TransactionDetailsViewControllerBase: FormViewController { cell.textLabel?.textColor = UIColor.adamant.textColor row.value = self?.getFeeValue() } - + detailsSection.append(feeRow) - + // MARK: Confirmations let confirmationsRow = LabelRow { $0.disabled = true $0.tag = Rows.confirmations.tag $0.title = Rows.confirmations.localized - + if let value = transaction?.confirmationsValue, value != "0" { $0.value = value } else { $0.value = TransactionDetailsViewControllerBase.awaitingValueString } - + }.cellSetup { (cell, _) in cell.selectionStyle = .gray cell.textLabel?.textColor = UIColor.adamant.textColor @@ -540,17 +544,18 @@ class TransactionDetailsViewControllerBase: FormViewController { row.value = TransactionDetailsViewControllerBase.awaitingValueString } } - + detailsSection.append(confirmationsRow) - + // MARK: Block let blockRow = LabelRow { $0.disabled = true $0.tag = Rows.block.tag $0.title = Rows.block.localized - + if let value = transaction?.blockValue, - !value.isEmpty { + !value.isEmpty + { $0.value = value } else { $0.value = TransactionDetailsViewControllerBase.awaitingValueString @@ -566,15 +571,16 @@ class TransactionDetailsViewControllerBase: FormViewController { }.cellUpdate { [weak self] (cell, row) in cell.textLabel?.textColor = UIColor.adamant.textColor if let value = self?.transaction?.blockValue, - !value.isEmpty { + !value.isEmpty + { row.value = value } else { row.value = TransactionDetailsViewControllerBase.awaitingValueString } } - + detailsSection.append(blockRow) - + // MARK: Status let statusRow = LabelRow { $0.tag = Rows.status.tag @@ -589,24 +595,26 @@ class TransactionDetailsViewControllerBase: FormViewController { } }.cellUpdate { [weak self] (cell, row) in cell.textLabel?.textColor = UIColor.adamant.textColor - cell.detailTextLabel?.textColor = self?.transactionStatus?.color ?? UIColor.adamant.textColor - + cell.detailTextLabel?.textColor = self?.transactionStatus?.color ?? UIColor.adamant.textColor + if let value = self?.transactionStatus?.localized, - !value.isEmpty { + !value.isEmpty + { row.value = value } else { - row.value = TransactionDetailsViewControllerBase.awaitingValueString + row.value = TransactionStatus.registered.localized + cell.detailTextLabel?.textColor = .adamant.attention } } - + detailsSection.append(statusRow) - + // MARK: Current Fiat let currentFiatRow = LabelRow { $0.disabled = true $0.tag = Rows.currentFiat.tag $0.title = Rows.currentFiat.localized - + if let amount = transaction?.amountValue, let symbol = currencySymbol, let rate = currencyInfo.getRate(for: symbol) { let value = amount * rate $0.value = fiatFormatter.string(from: value) @@ -623,25 +631,26 @@ class TransactionDetailsViewControllerBase: FormViewController { }.cellUpdate { [weak self] (cell, row) in cell.textLabel?.textColor = UIColor.adamant.textColor if let amount = self?.transaction?.amountValue, - let symbol = self?.currencySymbol, - let rate = self?.currencyInfo.getRate(for: symbol), - let value = self?.fiatFormatter.string(from: amount * rate) { + let symbol = self?.currencySymbol, + let rate = self?.currencyInfo.getRate(for: symbol), + let value = self?.fiatFormatter.string(from: amount * rate) + { row.value = value } else { row.value = TransactionDetailsViewControllerBase.awaitingValueString } } - + detailsSection.append(currentFiatRow) - + form.append(detailsSection) - + // MARK: History Fiat let fiatRow = LabelRow { $0.disabled = true $0.tag = Rows.historyFiat.tag $0.title = Rows.historyFiat.localized - + $0.value = TransactionDetailsViewControllerBase.awaitingValueString }.cellSetup { (cell, _) in cell.selectionStyle = .gray @@ -653,35 +662,38 @@ class TransactionDetailsViewControllerBase: FormViewController { }.cellUpdate { (cell, _) in cell.textLabel?.textColor = UIColor.adamant.textColor } - + detailsSection.append(fiatRow) - + // MARK: Tx blockchain comment let txBlockchainComment = LabelRow { $0.disabled = true $0.tag = Rows.txBlockchainComment.tag $0.title = Rows.txBlockchainComment.localized - + if let value = transaction?.txBlockchainComment { $0.value = value } else { $0.value = TransactionDetailsViewControllerBase.awaitingValueString } - + $0.cell.detailTextLabel?.textAlignment = .right $0.cell.detailTextLabel?.lineBreakMode = .byTruncatingMiddle - - $0.hidden = Condition.function([], { [weak self] _ -> Bool in - guard let value = self?.transaction?.txBlockchainComment else { + + $0.hidden = Condition.function( + [], + { [weak self] _ -> Bool in + guard let value = self?.transaction?.txBlockchainComment else { + return false + } + + if value.isEmpty { + return true + } return false } - - if value.isEmpty { - return true - } - return false - }) - + ) + }.cellSetup { (cell, _) in cell.selectionStyle = .gray cell.textLabel?.textColor = UIColor.adamant.textColor @@ -697,18 +709,18 @@ class TransactionDetailsViewControllerBase: FormViewController { row.value = TransactionDetailsViewControllerBase.awaitingValueString } } - + if showTxBlockchainComment { detailsSection.append(txBlockchainComment) } - + // MARK: Comments section - + if let comment = comment { let commentSection = Section(Sections.comment.localized) { $0.tag = Sections.comment.tag } - + let row = TextAreaRow(Rows.comment.tag) { $0.textAreaHeight = .dynamic(initialTextViewHeight: 44) $0.value = comment @@ -724,24 +736,27 @@ class TransactionDetailsViewControllerBase: FormViewController { self?.shareValue(text, from: cell) } } - + commentSection.append(row) - + form.append(commentSection) } - + // MARK: Inconsistent Reason - + let inconsistentReasonSection = Section(Sections.inconsistentReason.localized) { $0.tag = Sections.inconsistentReason.tag - $0.hidden = Condition.function([], { [weak self] _ -> Bool in - if case .inconsistent = self?.transactionStatus { - return false + $0.hidden = Condition.function( + [], + { [weak self] _ -> Bool in + if case .inconsistent = self?.transactionStatus { + return false + } + return true } - return true - }) + ) } - + let inconsistentReasonRow = TextAreaRow(Rows.inconsistentReason.tag) { $0.textAreaHeight = .dynamic(initialTextViewHeight: 44) $0.value = transactionStatus?.descriptionLocalized @@ -758,29 +773,35 @@ class TransactionDetailsViewControllerBase: FormViewController { self?.shareValue(text, from: cell) } } - + inconsistentReasonSection.append(inconsistentReasonRow) form.append(inconsistentReasonSection) - + // MARK: Actions section - + let actionsSection = Section(Sections.actions.localized) { $0.tag = Sections.actions.tag - $0.hidden = Condition.function([], { [weak self] _ -> Bool in - return self?.transaction == nil - }) + $0.hidden = Condition.function( + [], + { [weak self] _ -> Bool in + return self?.transaction == nil + } + ) } - + // MARK: Open in explorer let explorerRow = LabelRow(Rows.openInExplorer.tag) { - $0.hidden = Condition.function([], { [weak self] _ -> Bool in - if let transaction = self?.transaction { - return self?.explorerUrl(for: transaction) == nil - } else { - return true + $0.hidden = Condition.function( + [], + { [weak self] _ -> Bool in + if let transaction = self?.transaction { + return self?.explorerUrl(for: transaction) == nil + } else { + return true + } } - }) - + ) + $0.title = Rows.openInExplorer.localized $0.cell.imageView?.image = Rows.openInExplorer.image }.cellSetup { (cell, _) in @@ -793,106 +814,111 @@ class TransactionDetailsViewControllerBase: FormViewController { guard let transaction = self?.transaction, let url = self?.explorerUrl(for: transaction) else { return } - + let safari = SFSafariViewController(url: url) safari.preferredControlTintColor = UIColor.adamant.primary safari.modalPresentationStyle = .overFullScreen self?.present(safari, animated: true, completion: nil) } - + actionsSection.append(explorerRow) - + form.append(actionsSection) - + // Get fiat value self.updateFiat() - + setColors() - + checkAddressesIfNeeded() } - + deinit { refreshTask?.cancel() } - + func updateFiat() { guard let date = transaction?.dateValue, - let currencySymbol = currencySymbol, - let amount = transaction?.amountValue + let currencySymbol = currencySymbol, + let amount = transaction?.amountValue else { return } - + let currentFiat = currencyInfo.currentCurrency.rawValue - + Task { var tickers = try await currencyInfo.getHistory( for: currencySymbol, date: date ).get() - + isFiatSet = true - + guard - var ticker = tickers[.init( - crypto: currencySymbol, - fiat: currentFiat - )] + var ticker = tickers[ + .init( + crypto: currencySymbol, + fiat: currentFiat + ) + ] else { return } - + let totalFiat = amount * ticker valueAtTimeTxn = fiatFormatter.string(from: totalFiat) - + guard let fee = transaction?.feeValue else { return } - + if let feeCurrencySymbol = feeCurrencySymbol, - feeCurrencySymbol != currencySymbol { + feeCurrencySymbol != currencySymbol + { tickers = try await currencyInfo.getHistory( for: feeCurrencySymbol, date: date ).get() - + guard - let feeTicker = tickers[.init( - crypto: feeCurrencySymbol, - fiat: currentFiat - )] + let feeTicker = tickers[ + .init( + crypto: feeCurrencySymbol, + fiat: currentFiat + ) + ] else { return } - + ticker = feeTicker } - + let totalFeeFiat = fee * ticker feeAtTimeTxn = fiatFormatter.string(from: totalFeeFiat) } } - + func updateIncosinstentRowIfNeeded() { guard case .inconsistent = transactionStatus, - let section = form.sectionBy(tag: Sections.inconsistentReason.tag) + let section = form.sectionBy(tag: Sections.inconsistentReason.tag) else { return } - + section.evaluateHidden() - + checkAddressesIfNeeded() } - + func updateTxDataRow() { let row = form.rowBy(tag: Rows.txBlockchainComment.tag) row?.evaluateHidden() } - + // MARK: - Other - + private func setColors() { view.backgroundColor = UIColor.adamant.secondBackgroundColor tableView.backgroundColor = .clear } - + func updateTransactionStatus() { guard let transaction = transaction, - let richTransaction = richTransaction + let richTransaction = richTransaction else { return } - + let failedTransaction = SimpleTransactionDetails( txId: transaction.txId, senderAddress: transaction.senderAddress, @@ -906,7 +932,7 @@ class TransactionDetailsViewControllerBase: FormViewController { transactionStatus: richTransaction.transactionStatus, nonceRaw: transaction.nonceRaw ) - + self.transaction = failedTransaction tableView.reloadData() updateIncosinstentRowIfNeeded() @@ -916,157 +942,162 @@ class TransactionDetailsViewControllerBase: FormViewController { private func updateValueAtTimeRowValue() { guard let row: LabelRow = form.rowBy(tag: Rows.historyFiat.tag) else { return } - + row.value = valueAtTimeTxn row.updateCell() } - + @MainActor private func updateFeeAtTimeRowValue() { guard let row: LabelRow = form.rowBy(tag: Rows.fee.tag) else { return } - + row.value = getFeeValue() row.updateCell() } - + func getFeeValue() -> String { guard let value = transaction?.feeValue else { return TransactionDetailsViewControllerBase.awaitingValueString } - + let feeValueRaw = feeFormatter.string(from: value) ?? "" - + if let feeAtTimeTxn = feeAtTimeTxn { return "\(feeValueRaw) ~\(feeAtTimeTxn)" } - + return feeValueRaw } - + @MainActor func checkAddressesIfNeeded() { Task { guard let senderAddress = senderId, - let recipientAddress = recipientId, - transactionStatus?.isInconsistent == true + let recipientAddress = recipientId, + transactionStatus?.isInconsistent == true else { return } - + let realSenderAddress = try? await walletService?.core.getWalletAddress( byAdamantAddress: senderAddress ) - + let realRecipientAddress = try? await walletService?.core.getWalletAddress( byAdamantAddress: recipientAddress ) - + if realSenderAddress != transaction?.senderAddress { senderName = nil } else { senderName = getName(by: senderId) } - + if realRecipientAddress != transaction?.recipientAddress { recipientName = nil } else { recipientName = getName(by: recipientId) } - + tableView.reloadData() } } - + func getName(by adamantAddress: String?) -> String? { guard let id = adamantAddress, - let address = accountService.account?.address - else { return nil } - + let address = accountService.account?.address + else { return nil } + if id.caseInsensitiveCompare(address) == .orderedSame { return String.adamant.transactionDetails.yourAddress } - + return addressBookService.getName(for: id) } - + // MARK: - Actions - + @objc func share(_ sender: UIBarButtonItem) { guard let transaction = transaction else { return } - + let alert = UIAlertController( title: nil, message: nil, preferredStyleSafe: .actionSheet, source: .barButtonItem(sender) ) - + alert.addAction(UIAlertAction(title: String.adamant.alert.cancel, style: .cancel, handler: nil)) - + if let url = explorerUrl(for: transaction) { // URL - alert.addAction(UIAlertAction(title: String.adamant.alert.exportUrlButton, style: .default) { [weak self] _ in - let alert = UIActivityViewController(activityItems: [url], applicationActivities: nil) - alert.modalPresentationStyle = .overFullScreen - self?.present(alert, animated: true, completion: nil) - }) + alert.addAction( + UIAlertAction(title: String.adamant.alert.exportUrlButton, style: .default) { [weak self] _ in + let alert = UIActivityViewController(activityItems: [url], applicationActivities: nil) + alert.modalPresentationStyle = .overFullScreen + self?.present(alert, animated: true, completion: nil) + } + ) } // Description if let summary = summary(for: transaction) { - alert.addAction(UIAlertAction(title: String.adamant.alert.exportSummaryButton, style: .default) { [weak self] _ in - let text = summary - let alert = UIActivityViewController(activityItems: [text], applicationActivities: nil) - alert.modalPresentationStyle = .overFullScreen - self?.present(alert, animated: true, completion: nil) - }) + alert.addAction( + UIAlertAction(title: String.adamant.alert.exportSummaryButton, style: .default) { [weak self] _ in + let text = summary + let alert = UIActivityViewController(activityItems: [text], applicationActivities: nil) + alert.modalPresentationStyle = .overFullScreen + self?.present(alert, animated: true, completion: nil) + } + ) } - + present(alert, animated: true, completion: nil) } - + // MARK: - Tools - + func shareValue(_ value: String, from: UIView) { - dialogService.presentShareAlertFor(string: value, types: [.copyToPasteboard, .share], excludedActivityTypes: nil, animated: true, from: from) { [weak self] in + dialogService.presentShareAlertFor(string: value, types: [.copyToPasteboard, .share], excludedActivityTypes: nil, animated: true, from: from) { + [weak self] in guard let tableView = self?.tableView else { return } - + if let indexPath = tableView.indexPathForSelectedRow { tableView.deselectRow(at: indexPath, animated: true) } } } - + // MARK: - To override - + var currencySymbol: String? - + // MARK: - Fix this later var senderName: String? var recipientName: String? var comment: String? - + func explorerUrl(for transaction: TransactionDetails) -> URL? { return nil } - + func summary(for transaction: TransactionDetails) -> String? { guard let amount = transaction.amountValue, - let symbol = currencySymbol, - let rate = currencyInfo.getRate(for: symbol), - !transaction.recipientAddress.isEmpty + let symbol = currencySymbol, + let rate = currencyInfo.getRate(for: symbol), + !transaction.recipientAddress.isEmpty else { return nil } - + let value = amount * rate let currentValue = fiatFormatter.string(from: value) - + return transaction.summary( with: explorerUrl(for: transaction)?.absoluteString, currentValue: currentValue, diff --git a/Adamant/Modules/Wallets/TransactionTableViewCell.swift b/Adamant/Modules/Wallets/TransactionTableViewCell.swift index 06669c173..eefdc960e 100644 --- a/Adamant/Modules/Wallets/TransactionTableViewCell.swift +++ b/Adamant/Modules/Wallets/TransactionTableViewCell.swift @@ -6,13 +6,13 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit import CommonKit +import UIKit final class TransactionTableViewCell: UITableViewCell { enum TransactionType { case income, outcome, myself - + var imageTop: UIImage { switch self { case .income: return .asset(named: "transfer-in_top") ?? .init() @@ -20,7 +20,7 @@ final class TransactionTableViewCell: UITableViewCell { case .myself: return .asset(named: "transfer-in_top")?.withTintColor(.lightGray) ?? .init() } } - + var imageBottom: UIImage { switch self { case .income: return .asset(named: "transfer-in_bot") ?? .init() @@ -28,7 +28,7 @@ final class TransactionTableViewCell: UITableViewCell { case .myself: return .asset(named: "transfer-self_bot") ?? .init() } } - + var bottomTintColor: UIColor { switch self { case .income: return UIColor.adamant.transferIncomeIconBackground @@ -37,26 +37,26 @@ final class TransactionTableViewCell: UITableViewCell { } } } - + // MARK: - Constants - + static let cellHeightCompact: CGFloat = 90.0 static let cellFooterLoadingCompact: CGFloat = 30.0 static let cellHeightFull: CGFloat = 100.0 - + // MARK: - IBOutlets - + private let topImageView = UIImageView(image: UIImage(named: "transfer-in_top")) private let bottomImageView = UIImageView(image: UIImage(named: "transfer-in_bot")) private lazy var amountLabel = UILabel() private lazy var dateLabel = UILabel() - + private lazy var accountLabel: UILabel = { let text = UILabel() text.font = .systemFont(ofSize: 17) return text }() - + private lazy var addressLabel: UILabel = { let text = UILabel() let font = UIFont.preferredFont(forTextStyle: .footnote) @@ -64,12 +64,12 @@ final class TransactionTableViewCell: UITableViewCell { text.textColor = .lightGray return text }() - + private lazy var contactInfoView: UIView = { let view = UIView() view.addSubview(accountLabel) view.addSubview(addressLabel) - + accountLabel.snp.makeConstraints { make in make.leading.centerY.equalToSuperview() make.trailing.equalTo(addressLabel.snp.leading).offset(-5) @@ -87,22 +87,22 @@ final class TransactionTableViewCell: UITableViewCell { } return view }() - + private lazy var informationStackView: UIStackView = { let stackView = UIStackView() stackView.axis = .vertical stackView.alignment = .leading stackView.spacing = 3 - + stackView.addArrangedSubview(contactInfoView) stackView.addArrangedSubview(amountLabel) stackView.addArrangedSubview(dateLabel) - + return stackView }() - + // MARK: - Properties - + var transactionType: TransactionType = .income { didSet { topImageView.image = transactionType.imageTop @@ -110,64 +110,65 @@ final class TransactionTableViewCell: UITableViewCell { bottomImageView.tintColor = transactionType.bottomTintColor } } - + var currencySymbol: String? - + var transaction: SimpleTransactionDetails? { didSet { updateUI() } } - + // MARK: - Initializers - + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: .subtitle, reuseIdentifier: reuseIdentifier) setupView() } - + required init?(coder: NSCoder) { super.init(coder: coder) setupView() } - + override func awakeFromNib() { Task { @MainActor in transactionType = .income } } - + private func setupView() { addSubview(informationStackView) addSubview(bottomImageView) addSubview(topImageView) - + bottomImageView.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalToSuperview().offset(16) make.size.equalTo(37) } - + topImageView.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalToSuperview().offset(16) make.size.equalTo(37) } - + informationStackView.snp.makeConstraints { make in make.top.equalToSuperview().offset(8) make.leading.equalToSuperview().offset(70) make.trailing.equalToSuperview().offset(-30) } } - + func updateUI() { guard let transaction = transaction else { return } - - let partnerId = transaction.isOutgoing - ? transaction.recipientAddress - : transaction.senderAddress - + + let partnerId = + transaction.isOutgoing + ? transaction.recipientAddress + : transaction.senderAddress + let transactionType: TransactionTableViewCell.TransactionType if transaction.recipientAddress == transaction.senderAddress { transactionType = .myself @@ -176,15 +177,15 @@ final class TransactionTableViewCell: UITableViewCell { } else { transactionType = .income } - + self.transactionType = transactionType - + backgroundColor = .clear accountLabel.tintColor = UIColor.adamant.primary amountLabel.tintColor = UIColor.adamant.primary - + dateLabel.textColor = transaction.transactionStatus?.color ?? .adamant.secondary - + switch transaction.transactionStatus { case .success, .inconsistent: if let date = transaction.dateValue { @@ -192,22 +193,20 @@ final class TransactionTableViewCell: UITableViewCell { } else { dateLabel.text = nil } - case .notInitiated: - dateLabel.text = TransactionDetailsViewControllerBase.awaitingValueString case .failed: dateLabel.text = TransactionStatus.failed.localized - case .pending, .registered: + case .pending, .registered, .notInitiated: dateLabel.text = TransactionStatus.pending.localized default: dateLabel.text = TransactionDetailsViewControllerBase.awaitingValueString } - + if let partnerName = transaction.partnerName { accountLabel.text = partnerName addressLabel.text = partnerId accountLabel.lineBreakMode = .byTruncatingTail addressLabel.lineBreakMode = .byTruncatingMiddle - + if addressLabel.isHidden { addressLabel.isHidden = false } @@ -218,7 +217,7 @@ final class TransactionTableViewCell: UITableViewCell { } else { accountLabel.text = partnerId accountLabel.lineBreakMode = .byTruncatingMiddle - + if !addressLabel.isHidden { addressLabel.isHidden = true } @@ -227,15 +226,15 @@ final class TransactionTableViewCell: UITableViewCell { make.width.equalTo(0) } } - + let amount = transaction.amountValue ?? .zero amountLabel.text = AdamantBalanceFormat.full.format(amount, withCurrencySymbol: currencySymbol) } } // MARK: - TransactionStatus UI -private extension TransactionStatus { - var color: UIColor { +extension TransactionStatus { + fileprivate var color: UIColor { switch self { case .failed: return .adamant.warning diff --git a/Adamant/Modules/Wallets/TransactionsListViewControllerBase.swift b/Adamant/Modules/Wallets/TransactionsListViewControllerBase.swift index 366a7d1a0..0d9269e68 100644 --- a/Adamant/Modules/Wallets/TransactionsListViewControllerBase.swift +++ b/Adamant/Modules/Wallets/TransactionsListViewControllerBase.swift @@ -6,10 +6,10 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import CoreData -import CommonKit import Combine +import CommonKit +import CoreData +import UIKit extension String.adamant { enum transactionList { @@ -37,7 +37,7 @@ private typealias TransactionsDiffableDataSource = UITableViewDiffableDataSource class TransactionsListViewControllerBase: UIViewController { let cellIdentifierFull = "cf" let cellIdentifierCompact = "cc" - + internal lazy var refreshControl: UIRefreshControl = { let refreshControl = UIRefreshControl() refreshControl.tintColor = .adamant.primary @@ -48,48 +48,48 @@ class TransactionsListViewControllerBase: UIViewController { ) return refreshControl }() - + // MARK: - Dependencies - + let walletService: WalletService let dialogService: DialogService let reachabilityMonitor: ReachabilityMonitor let screensFactory: ScreensFactory - + // MARK: - Proprieties - + var taskManager = TaskManager() - + var isNeedToLoadMoore = true { didSet { guard !isNeedToLoadMoore else { return } stopBottomIndicator() } } - + var isBusy = false - + var subscriptions = Set() var transactions: [SimpleTransactionDetails] = [] private(set) lazy var loadingView = LoadingView() private lazy var bottomActivityIndicatorView = makeBottomIndicatorView() - + private var limit = 25 private var offset = 0 - + private lazy var dataSource = TransactionsDiffableDataSource( tableView: tableView, cellProvider: { [weak self] in self?.makeCell(tableView: $0, indexPath: $1, model: $2) } ) - + var currencySymbol: String { walletService.core.tokenSymbol } - + // MARK: - IBOutlets @IBOutlet weak var tableView: UITableView! @IBOutlet weak var emptyLabel: UILabel! - + // MARK: - Init - + init( walletService: WalletService, dialogService: DialogService, @@ -100,23 +100,23 @@ class TransactionsListViewControllerBase: UIViewController { self.dialogService = dialogService self.reachabilityMonitor = reachabilityMonitor self.screensFactory = screensFactory - + super.init(nibName: String(describing: TransactionsListViewControllerBase.self), bundle: nil) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: - Lifecycle - + override func viewDidLoad() { super.viewDidLoad() navigationItem.largeTitleDisplayMode = .never navigationItem.title = String.adamant.transactionList.title emptyLabel.text = String.adamant.transactionList.noTransactionYet - + update(walletService.core.getLocalTransactionHistory()) configureTableView() setColors() @@ -124,20 +124,20 @@ class TransactionsListViewControllerBase: UIViewController { addObservers() handleRefresh() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.emptyLabel.isHidden = true - + if let indexPath = tableView.indexPathForSelectedRow { tableView.deselectRow(at: indexPath, animated: animated) } - + if tableView.isEditing { tableView.setEditing(false, animated: false) } } - + func addObservers() { NotificationCenter.default .notifications(named: .AdamantAddressBookService.addressBookUpdated, object: nil) @@ -145,35 +145,35 @@ class TransactionsListViewControllerBase: UIViewController { self?.reloadData() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedOut, object: nil) .sink { @MainActor [weak self] _ in self?.reloadData() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedIn, object: nil) .sink { @MainActor [weak self] _ in self?.reloadData() } .store(in: &subscriptions) - + walletService.core.transactionsPublisher .receive(on: OperationQueue.main) .sink { [weak self] transactions in self?.update(transactions) } .store(in: &subscriptions) - + walletService.core.hasMoreOldTransactionsPublisher.values .sink { @MainActor [weak self] isNeedToLoadMoore in self?.isNeedToLoadMoore = isNeedToLoadMoore } .store(in: &subscriptions) } - + func configureTableView() { tableView.register(TransactionTableViewCell.self, forCellReuseIdentifier: cellIdentifierFull) tableView.register(TransactionTableViewCell.self, forCellReuseIdentifier: cellIdentifierCompact) @@ -181,16 +181,16 @@ class TransactionsListViewControllerBase: UIViewController { tableView.refreshControl = refreshControl tableView.tableHeaderView = UIView() } - + @MainActor func update(_ transactions: [TransactionDetails]) { let transactions = transactions.map { SimpleTransactionDetails($0) } - + update(transactions) } - + @MainActor func update(_ transactions: [SimpleTransactionDetails]) { self.transactions = transactions.sorted( @@ -203,32 +203,32 @@ class TransactionsListViewControllerBase: UIViewController { snapshot.appendItems(list) snapshot.reconfigureItems(list) dataSource.apply(snapshot, animatingDifferences: false) - + guard !isBusy else { return } self.updateLoadingView(isHidden: true) } - + // MARK: - Other - + private func setColors() { view.backgroundColor = UIColor.adamant.backgroundColor tableView.backgroundColor = .clear } - + func configureLayout() { view.addSubview(loadingView) loadingView.isHidden = true - + loadingView.snp.makeConstraints { $0.directionalEdges.equalToSuperview() } } - + func presentLoadingViewIfNeeded() { guard transactions.count == 0 else { return } updateLoadingView(isHidden: false) } - + func updateLoadingView(isHidden: Bool) { loadingView.isHidden = isHidden if !isHidden { @@ -237,16 +237,16 @@ class TransactionsListViewControllerBase: UIViewController { loadingView.stopAnimating() } } - + @MainActor func loadData(silent: Bool) { loadData(offset: offset, silent: true) } - + @MainActor func loadData(offset: Int, silent: Bool) { guard !isBusy else { return } - + guard reachabilityMonitor.connection else { dialogService.showError( withMessage: .adamant.sharedErrors.networkError, @@ -255,7 +255,7 @@ class TransactionsListViewControllerBase: UIViewController { ) return } - + isBusy = true Task { do { @@ -268,57 +268,59 @@ class TransactionsListViewControllerBase: UIViewController { } catch { isNeedToLoadMoore = false emptyLabel.isHidden = self.transactions.count > 0 - + if !silent { dialogService.showError( withMessage: error.localizedDescription, supportEmail: false, - error: error) + error: error + ) emptyLabel.isHidden = true } } - + isBusy = false refreshControl.endRefreshing() updateLoadingView(isHidden: true) }.stored(in: taskManager) } - + // MARK: - To override - + func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { return UIView() } - + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return TransactionTableViewCell.cellHeightCompact } - + private func makeCell( tableView: UITableView, indexPath: IndexPath, model: SimpleTransactionDetails ) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifierCompact, for: indexPath) as! TransactionTableViewCell - + cell.accessoryType = .disclosureIndicator - cell.separatorInset = indexPath.row == transactions.count - 1 - ? .zero - : UITableView.defaultTransactionsSeparatorInset - + cell.separatorInset = + indexPath.row == transactions.count - 1 + ? .zero + : UITableView.defaultTransactionsSeparatorInset + cell.currencySymbol = currencySymbol cell.transaction = model return cell } - + func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { return TransactionTableViewCell.cellFooterLoadingCompact } - + func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { guard !isBusy, - isNeedToLoadMoore, - tableView.numberOfRows(inSection: .zero) - indexPath.row < 3 + isNeedToLoadMoore, + tableView.numberOfRows(inSection: .zero) - indexPath.row < 3 else { if tableView.tableFooterView == nil, isBusy { bottomActivityIndicatorView.startAnimating() @@ -329,15 +331,15 @@ class TransactionsListViewControllerBase: UIViewController { bottomActivityIndicatorView.startAnimating() loadData(silent: true) } - + @objc func handleRefresh() { presentLoadingViewIfNeeded() emptyLabel.isHidden = true loadData(offset: .zero, silent: false) } - + func reloadData() { - + } } @@ -345,11 +347,11 @@ class TransactionsListViewControllerBase: UIViewController { extension TransactionsListViewControllerBase: UITableViewDelegate { func makeBottomIndicatorView() -> UIActivityIndicatorView { var activityIndicatorView = UIActivityIndicatorView() - + guard tableView.tableFooterView == nil else { return activityIndicatorView } - + let indicatorFrame = CGRect( x: .zero, y: .zero, @@ -361,20 +363,20 @@ extension TransactionsListViewControllerBase: UITableViewDelegate { activityIndicatorView.style = .medium activityIndicatorView.color = .lightGray activityIndicatorView.hidesWhenStopped = true - + tableView.tableFooterView = activityIndicatorView - + return activityIndicatorView } - + private func stopBottomIndicator() { bottomActivityIndicatorView.stopAnimating() } } // MARK: - TransactionStatus UI -private extension TransactionStatus { - var color: UIColor { +extension TransactionStatus { + fileprivate var color: UIColor { switch self { case .failed: return .adamant.warning case .notInitiated, .inconsistent, .pending, .registered: diff --git a/Adamant/Modules/Wallets/TransferViewControllerBase+Alert.swift b/Adamant/Modules/Wallets/TransferViewControllerBase+Alert.swift index cf0e89641..1687e9a04 100644 --- a/Adamant/Modules/Wallets/TransferViewControllerBase+Alert.swift +++ b/Adamant/Modules/Wallets/TransferViewControllerBase+Alert.swift @@ -6,41 +6,41 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import SnapKit import CommonKit +import SnapKit +import UIKit extension TransferViewControllerBase { - + // MARK: - Progress view func showProgressView(animated: Bool) { if let alertView = alertView { hideView(alertView, animated: animated) } - + guard progressView == nil else { return } - + let view = UIView() progressView = view view.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6) - + self.view.addSubview(view) view.snp.makeConstraints { $0.directionalEdges.equalToSuperview() } - + let indicator = UIActivityIndicatorView(style: .whiteLarge) view.addSubview(indicator) indicator.snp.makeConstraints { $0.center.equalToSuperview() } - + indicator.startAnimating() - + guard animated else { return } - + DispatchQueue.onMainAsync { view.alpha = 0 UIView.animate(withDuration: 0.2) { @@ -48,75 +48,79 @@ extension TransferViewControllerBase { } } } - + func hideProgress(animated: Bool) { guard let progressView = progressView else { return } - + hideView(progressView, animated: animated) } - + // MARK: - Alert view - + func showAlertView(message: String, animated: Bool) { if let progressView = progressView { hideView(progressView, animated: animated) } - + if let alertView = alertView { hideView(alertView, animated: animated) } - + let callback: @MainActor () -> Void = { let alert = FullscreenAlertView() alert.message = message - + self.view.addSubview(alert) alert.snp.makeConstraints { $0.directionalEdges.equalToSuperview() } - + if animated { alert.alpha = 0 alert.transform = CGAffineTransform(scaleX: 1.2, y: 1.2) - + UIView.animate(withDuration: 0.2) { alert.alpha = 1 alert.transform = CGAffineTransform(scaleX: 1, y: 1) } } } - + DispatchQueue.onMainAsync(callback) } - + func hideAlert(animated: Bool) { guard let alertView = alertView else { return } - + hideView(alertView, animated: animated) } - + // MARK: - Tools private func hideView(_ view: UIView, animated: Bool) { let callback: @MainActor () -> Void - + if animated { callback = { - UIView.animate(withDuration: 0.2, animations: { - view.backgroundColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0) - }, completion: { _ in - view.removeFromSuperview() - }) + UIView.animate( + withDuration: 0.2, + animations: { + view.backgroundColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0) + }, + completion: { _ in + view.removeFromSuperview() + } + ) } } else { callback = { view.removeFromSuperview() } } - + DispatchQueue.onMainAsync(callback) } } diff --git a/Adamant/Modules/Wallets/TransferViewControllerBase+QR.swift b/Adamant/Modules/Wallets/TransferViewControllerBase+QR.swift index e41a2ae9d..ba5475d73 100644 --- a/Adamant/Modules/Wallets/TransferViewControllerBase+QR.swift +++ b/Adamant/Modules/Wallets/TransferViewControllerBase+QR.swift @@ -6,12 +6,12 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -@preconcurrency import QRCodeReader -import EFQRCode import AVFoundation -import Photos import CommonKit +import EFQRCode +import Photos +@preconcurrency import QRCodeReader +import UIKit // MARK: - QR extension TransferViewControllerBase { @@ -19,11 +19,11 @@ extension TransferViewControllerBase { switch AVCaptureDevice.authorizationStatus(for: .video) { case .authorized: qrReader.modalPresentationStyle = .overFullScreen - + DispatchQueue.onMainAsync { self.present(self.qrReader, animated: true, completion: nil) } - + case .notDetermined: AVCaptureDevice.requestAccess(for: .video) { [weak self] (granted: Bool) in DispatchQueue.onMainAsync { @@ -41,21 +41,23 @@ extension TransferViewControllerBase { DispatchQueue.onMainAsync { self.present(alert, animated: true, completion: nil) } - + case .denied: let alert = UIAlertController(title: nil, message: String.adamant.login.cameraNotAuthorized, preferredStyleSafe: .alert, source: nil) - - alert.addAction(UIAlertAction(title: String.adamant.alert.settings, style: .default) { _ in - DispatchQueue.main.async { - if let settingsURL = URL(string: UIApplication.openSettingsURLString) { - UIApplication.shared.open(settingsURL, options: [:], completionHandler: nil) + + alert.addAction( + UIAlertAction(title: String.adamant.alert.settings, style: .default) { _ in + DispatchQueue.main.async { + if let settingsURL = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(settingsURL, options: [:], completionHandler: nil) + } } } - }) - + ) + alert.addAction(UIAlertAction(title: String.adamant.alert.cancel, style: .cancel, handler: nil)) alert.modalPresentationStyle = .overFullScreen - + DispatchQueue.onMainAsync { self.present(alert, animated: true, completion: nil) } @@ -63,7 +65,7 @@ extension TransferViewControllerBase { break } } - + func loadQr() { let presenter: () -> Void = { [weak self] in let picker = UIImagePickerController() @@ -74,7 +76,7 @@ extension TransferViewControllerBase { picker.overrideUserInterfaceStyle = .light self?.present(picker, animated: true, completion: nil) } - + presenter() } } @@ -85,10 +87,10 @@ extension TransferViewControllerBase: ButtonsStripeViewDelegate { switch button { case .qrCameraReader: scanQr() - + case .qrPhotoReader: loadQr() - + default: return } @@ -97,20 +99,20 @@ extension TransferViewControllerBase: ButtonsStripeViewDelegate { // MARK: - UIImagePickerControllerDelegate extension TransferViewControllerBase: UINavigationControllerDelegate, UIImagePickerControllerDelegate { - func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { dismiss(animated: true, completion: nil) - + guard let image = info[.originalImage] as? UIImage, let cgImage = image.cgImage else { return } - + let codes = EFQRCode.recognize(cgImage) - + if codes.contains(where: { handleRawAddress($0) }) { vibroService.applyVibration(.medium) return } - + if codes.isEmpty { dialogService.showWarning(withMessage: String.adamant.login.noQrError) } else { @@ -134,7 +136,7 @@ extension TransferViewControllerBase: QRCodeReaderViewControllerDelegate { } } } - + nonisolated func readerDidCancel(_ reader: QRCodeReaderViewController) { Task { @MainActor in reader.dismiss(animated: true, completion: nil) diff --git a/Adamant/Modules/Wallets/TransferViewControllerBase.swift b/Adamant/Modules/Wallets/TransferViewControllerBase.swift index a4b82550a..cd9242bf1 100644 --- a/Adamant/Modules/Wallets/TransferViewControllerBase.swift +++ b/Adamant/Modules/Wallets/TransferViewControllerBase.swift @@ -6,17 +6,21 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit +import Combine +import CommonKit import Eureka import QRCodeReader -import CommonKit -import Combine +import UIKit // MARK: - Transfer Delegate Protocol @MainActor protocol TransferViewControllerDelegate: AnyObject { - func transferViewController(_ viewController: TransferViewControllerBase, didFinishWithTransfer transfer: TransactionDetails?, detailsViewController: UIViewController?) + func transferViewController( + _ viewController: TransferViewControllerBase, + didFinishWithTransfer transfer: TransactionDetails?, + detailsViewController: UIViewController? + ) } // MARK: - Localization @@ -73,22 +77,27 @@ extension String.adamant { } } -fileprivate extension String.adamant.alert { - static var confirmSendMessageFormat: String { String.localized("TransferScene.SendConfirmFormat", comment: "Transfer: Confirm transfer %1$@ tokens to %2$@ message. Note two variables: at runtime %1$@ will be amount (with ADM suffix), and %2$@ will be recipient address. You can use address before amount with this so called 'position tokens'.") +extension String.adamant.alert { + fileprivate static var confirmSendMessageFormat: String { + String.localized( + "TransferScene.SendConfirmFormat", + comment: + "Transfer: Confirm transfer %1$@ tokens to %2$@ message. Note two variables: at runtime %1$@ will be amount (with ADM suffix), and %2$@ will be recipient address. You can use address before amount with this so called 'position tokens'." + ) } - static func confirmSendMessage(formattedAmount amount: String, recipient: String) -> String { + fileprivate static func confirmSendMessage(formattedAmount amount: String, recipient: String) -> String { return String.localizedStringWithFormat(String.adamant.alert.confirmSendMessageFormat, "\(amount)", recipient) } - static var send: String { + fileprivate static var send: String { String.localized("TransferScene.Send", comment: "Transfer: Confirm transfer alert: Send tokens button") } } // MARK: - class TransferViewControllerBase: FormViewController { - + // MARK: - Rows - + enum BaseRows { case balance case amount @@ -102,7 +111,7 @@ class TransferViewControllerBase: FormViewController { case comments case sendButton case blockchainComments(coin: String) - + var tag: String { switch self { case .balance: return "balance" @@ -119,36 +128,43 @@ class TransferViewControllerBase: FormViewController { case .blockchainComments: return "blockchainComments" } } - + var localized: String { switch self { case .balance: return .localized("TransferScene.Row.Balance", comment: "Transfer: logged user balance.") case .amount: return .localized("TransferScene.Row.Amount", comment: "Transfer: amount of adamant to transfer.") case .fiat: return .localized("TransferScene.Row.Fiat", comment: "Transfer: fiat value of crypto-amout") - case .maxToTransfer: return .localized("TransferScene.Row.MaxToTransfer", comment: "Transfer: maximum amount to transfer: available account money substracting transfer fee") + case .maxToTransfer: + return .localized( + "TransferScene.Row.MaxToTransfer", + comment: "Transfer: maximum amount to transfer: available account money substracting transfer fee" + ) case .name: return .localized("TransferScene.Row.RecipientName", comment: "Transfer: recipient name") case .address: return .localized("TransferScene.Row.RecipientAddress", comment: "Transfer: recipient address") case .fee: return .localized("TransferScene.Row.TransactionFee", comment: "Transfer: transfer fee") case .total: return .localized("TransferScene.Row.Total", comment: "Transfer: total amount of transaction: money to transfer adding fee") case .comments: return .localized("TransferScene.Row.Comments", comment: "Transfer: transfer comment") case let .blockchainComments(coin): - return String.localizedStringWithFormat(.localized( - "TransferScene.Row.Blockchain.Comments", - comment: "Transfer: Blockchain transfer comment" - ), coin) + return String.localizedStringWithFormat( + .localized( + "TransferScene.Row.Blockchain.Comments", + comment: "Transfer: Blockchain transfer comment" + ), + coin + ) case .sendButton: return String.adamant.transfer.send case .increaseFee: return .localized("TransferScene.Row.IncreaseFee", comment: "Transfer: transfer increase fee") } } } - + enum Sections { case wallet case recipient case transferInfo case comments case blockchainComments(coin: String) - + var tag: String { switch self { case .wallet: return "wlt" @@ -158,7 +174,7 @@ class TransferViewControllerBase: FormViewController { case .blockchainComments: return "blockchainComments" } } - + var localized: String { switch self { case .wallet: return .localized("TransferScene.Section.YourWallet", comment: "Transfer: 'Your wallet' section") @@ -166,16 +182,19 @@ class TransferViewControllerBase: FormViewController { case .transferInfo: return .localized("TransferScene.Section.TransferInfo", comment: "Transfer: 'Transfer info' section") case .comments: return .localized("TransferScene.Row.Comments", comment: "Transfer: transfer comment") case let .blockchainComments(coin): - return String.localizedStringWithFormat(.localized( - "TransferScene.Row.Blockchain.Comments", - comment: "Transfer: Blockchain transfer comment" - ), coin) + return String.localizedStringWithFormat( + .localized( + "TransferScene.Row.Blockchain.Comments", + comment: "Transfer: Blockchain transfer comment" + ), + coin + ) } } } - + // MARK: - Dependencies - + let accountService: AccountService let accountsProvider: AccountsProvider let dialogService: DialogService @@ -188,41 +207,41 @@ class TransferViewControllerBase: FormViewController { let walletCore: WalletCoreProtocol let reachabilityMonitor: ReachabilityMonitor let apiServiceCompose: ApiServiceComposeProtocol - + // MARK: - Properties - + private var previousIsReadyToSend: Bool? private var subscriptions: Set = [] - + var commentsEnabled: Bool = false var rootCoinBalance: Decimal? var isNeedAddFee: Bool { true } var replyToMessageId: String? var blockchainCommentsEnabled: Bool { false } var maxBlockchainCommentLenght: Int { 64 } - + static let invalidCharacters: CharacterSet = { CharacterSet( charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" ).inverted }() - + weak var delegate: TransferViewControllerDelegate? - + var addAdditionalFee = false { didSet { updateFeeCell() } } - + var transactionFee: Decimal { let baseFee = walletCore.transactionFee let additionalyFee = walletCore.additionalFee return addAdditionalFee - ? baseFee + additionalyFee - : baseFee + ? baseFee + additionalyFee + : baseFee } - + var recipientAddress: String? { set { if let row: RowOf = form.rowBy(tag: BaseRows.address.tag) { @@ -236,26 +255,26 @@ class TransferViewControllerBase: FormViewController { return row?.value } } - + var recipientName: String? { didSet { guard let row: RowOf = form.rowBy(tag: BaseRows.name.tag) else { return } - + row.value = recipientName row.updateCell() row.evaluateHidden() } } - + var admReportRecipient: String? var amount: Decimal? - + var recipientIsReadonly = false - + var rate: Decimal? - + var maxToTransfer: Decimal { guard let balance = walletCore.wallet?.balance, @@ -263,51 +282,52 @@ class TransferViewControllerBase: FormViewController { else { return 0 } - - let fee = isNeedAddFee - ? transactionFee - : 0 - + + let fee = + isNeedAddFee + ? transactionFee + : 0 + let max = balance - fee - walletCore.minBalance - + return max >= 0 ? max : 0 } - + var minToTransfer: Decimal { get async throws { return walletCore.minAmount } } - + override var customNavigationAccessoryView: (UIView & NavigationAccessory)? { let accessory = NavigationAccessoryView() accessory.tintColor = UIColor.adamant.primary return accessory } - + private let inactiveBaseColor = UIColor.gray.withAlphaComponent(0.5) private let activeBaseColor = UIColor.adamant.primary - + // MARK: - QR Reader - + lazy var qrReader: QRCodeReaderViewController = { let builder = QRCodeReaderViewControllerBuilder { - $0.reader = QRCodeReader(metadataObjectTypes: [.qr ], captureDevicePosition: .back) + $0.reader = QRCodeReader(metadataObjectTypes: [.qr], captureDevicePosition: .back) $0.cancelButtonTitle = String.adamant.alert.cancel $0.showSwitchCameraButton = false } - + let vc = QRCodeReaderViewController(builder: builder) vc.delegate = self return vc }() - + // MARK: - Alert var progressView: UIView? var alertView: UIView? - + // MARK: - Init - + init( chatsProvider: ChatsProvider, accountService: AccountService, @@ -335,53 +355,53 @@ class TransferViewControllerBase: FormViewController { self.apiServiceCompose = apiServiceCompose super.init(style: .insetGrouped) } - + required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: - Lifecycle - + override func viewDidLoad() { super.viewDidLoad() - + // MARK: Fiat rate rate = currencyInfoService.getRate(for: currencyCode) - + // MARK: UI navigationItem.title = defaultSceneTitle() - + // MARK: Sections form.append(walletSection()) form.append(recipientSection()) form.append(transactionInfoSection()) - + if commentsEnabled { form.append(commentsSection()) } - + if blockchainCommentsEnabled { form.append(blockchainCommentsSection()) } - + // MARK: Button section form +++ Section() - <<< ButtonRow { - $0.title = BaseRows.sendButton.localized - $0.tag = BaseRows.sendButton.tag - }.onCellSelection { [weak self] (_, _) in - Task { await self?.confirmSendFunds() } - } - + <<< ButtonRow { + $0.title = BaseRows.sendButton.localized + $0.tag = BaseRows.sendButton.tag + }.onCellSelection { [weak self] (_, _) in + Task { await self?.confirmSendFunds() } + } + // MARK: Notifications - + addObservers() - + setColors() } - + // MARK: - Other - + private func addObservers() { NotificationCenter.default .notifications(named: walletCore.transactionFeeUpdated) @@ -389,14 +409,14 @@ class TransferViewControllerBase: FormViewController { self?.feeUpdated() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: walletCore.walletUpdatedNotification) .sink { @MainActor [weak self] _ in self?.reloadFormData() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantCurrencyInfoService.currencyRatesUpdated) .sink { @MainActor [weak self] _ in @@ -404,37 +424,37 @@ class TransferViewControllerBase: FormViewController { } .store(in: &subscriptions) } - + private func feeUpdated() { if let row: DoubleDetailsRow = form.rowBy(tag: BaseRows.fee.tag) { row.value = getCellFeeValue() row.updateCell() } - + if let row: SafeDecimalRow = form.rowBy(tag: BaseRows.maxToTransfer.tag) { row.value = maxToTransfer.doubleValue row.updateCell() } - + validateForm() } - + private func currencyRateUpdated() { rate = currencyInfoService.getRate(for: currencyCode) - + if let row: SafeDecimalRow = form.rowBy(tag: BaseRows.fiat.tag) { if let formatter = row.formatter as? NumberFormatter { formatter.currencyCode = currencyInfoService.currentCurrency.rawValue } - + row.updateCell() } } - + private func isReadyToSend() -> Bool { validateAddress() guard recipientAddress != nil, - amount != nil + amount != nil else { return false } @@ -446,7 +466,7 @@ class TransferViewControllerBase: FormViewController { } return true } - + private func navigationKeybordDone() { tableView?.endEditing(true) guard isReadyToSend() else { return } @@ -456,23 +476,24 @@ class TransferViewControllerBase: FormViewController { func updateToolbar(for row: BaseRow) { _ = inputAccessoryView(for: row) } - + override func inputAccessoryView(for row: BaseRow) -> UIView? { guard !isMacOS else { return nil } - + let view = super.inputAccessoryView(for: row) guard let view = view as? NavigationAccessoryView else { return view } - + view.doneClosure = { [weak self] in self?.navigationKeybordDone() } - + if let previousIsReadyToSend = previousIsReadyToSend, - previousIsReadyToSend == isReadyToSend() { + previousIsReadyToSend == isReadyToSend() + { return view } previousIsReadyToSend = isReadyToSend() - + let doneBtn = UIBarButtonItem(title: String.adamant.transfer.done, style: .done, target: view, action: view.doneButton.action) let sendBtn = UIBarButtonItem(title: String.adamant.transfer.send, style: .done, target: view, action: view.doneButton.action) view.doneButton = isReadyToSend() ? sendBtn : doneBtn @@ -482,96 +503,100 @@ class TransferViewControllerBase: FormViewController { } return view } - + private func setColors() { view.backgroundColor = UIColor.adamant.secondBackgroundColor tableView.backgroundColor = .clear } - + private func updateFeeCell() { let row: DoubleDetailsRow? = form.rowBy(tag: BaseRows.fee.tag) row?.value = getCellFeeValue() row?.updateCell() } - + // MARK: - Form constructors - + func walletSection() -> Section { let section = Section(Sections.wallet.localized) { $0.tag = Sections.wallet.tag } - + section.append(defaultRowFor(baseRow: BaseRows.balance)) section.append(defaultRowFor(baseRow: BaseRows.maxToTransfer)) - + return section } - + func recipientSection() -> Section { let section = Section(Sections.recipient.localized) { $0.tag = Sections.recipient.tag } - + section.header = { - var header = HeaderFooterView(.callback({ - let font = UIFont.preferredFont(forTextStyle: .footnote) - let label = UILabel() - label.text = " \(Sections.recipient.localized.uppercased())" - label.font = font - label.textColor = .adamant.primary - return label - })) - + var header = HeaderFooterView( + .callback({ + let font = UIFont.preferredFont(forTextStyle: .footnote) + let label = UILabel() + label.text = " \(Sections.recipient.localized.uppercased())" + label.font = font + label.textColor = .adamant.primary + return label + }) + ) + header.height = { 33 } return header }() - + // Address row section.append(defaultRowFor(baseRow: BaseRows.address)) - + if !recipientIsReadonly, let stripe = recipientStripe() { - var footer = HeaderFooterView(.callback { [weak self] in - let view = ButtonsStripeView.adamantConfigured() - view.stripe = stripe - view.delegate = self - return view - }) - + var footer = HeaderFooterView( + .callback { [weak self] in + let view = ButtonsStripeView.adamantConfigured() + view.stripe = stripe + view.delegate = self + return view + } + ) + footer.height = { ButtonsStripeView.adamantDefaultHeight } - + section.footer = footer } - + return section } - + func transactionInfoSection() -> Section { let section = Section(Sections.transferInfo.localized) { $0.tag = Sections.transferInfo.tag } - + section.append(defaultRowFor(baseRow: .amount)) section.append(defaultRowFor(baseRow: .fiat)) - + if walletCore.isSupportIncreaseFee { section.append(defaultRowFor(baseRow: .increaseFee)) } - + section.append(defaultRowFor(baseRow: .fee)) section.append(defaultRowFor(baseRow: .total)) - + return section } - + func commentsSection() -> Section { let section = Section(Sections.comments.localized) { $0.tag = Sections.comments.tag } - + section.append(defaultRowFor(baseRow: .comments)) - + return section } @@ -580,12 +605,12 @@ class TransferViewControllerBase: FormViewController { let section = Section(commentSection.localized) { $0.tag = commentSection.tag } - + section.append(defaultRowFor(baseRow: .blockchainComments(coin: walletCore.tokenName))) - + return section } - + // MARK: - Tools @discardableResult @@ -594,25 +619,25 @@ class TransferViewControllerBase: FormViewController { markAddres(isValid: false) return false } - + let isValid = validateRecipient(recipientAddress).isValid markAddres(isValid: isValid) return isValid } - + func validateForm(force: Bool = false) { guard let wallet = walletCore.wallet else { return } - + if let row: DoubleDetailsRow = form.rowBy(tag: BaseRows.fee.tag) { markRow(row, valid: isEnoughFee()) } - + if let row: SafeDecimalRow = form.rowBy(tag: BaseRows.maxToTransfer.tag) { markRow(row, valid: wallet.balance > transactionFee) } - + if let row: SafeDecimalRow = form.rowBy(tag: BaseRows.amount.tag) { // Eureka looses decimal precision when deserializing numbers by itself. // Try to get raw value and deserialize it @@ -627,14 +652,14 @@ class TransferViewControllerBase: FormViewController { if let localeSeparator = Locale.current.decimalSeparator { let replacingSeparator = localeSeparator == "." ? "," : "." let fixed = raw.replacingOccurrences(of: replacingSeparator, with: localeSeparator) - + if let amount = Decimal(string: fixed, locale: Locale.current) { self.amount = amount markRow(row, valid: validateAmount(amount)) gotValue = true } } - + if !gotValue { if let raw = row.value { let amount = Decimal(raw) @@ -645,24 +670,25 @@ class TransferViewControllerBase: FormViewController { markRow(row, valid: true) } } - } else if let raw = row.value { // We can't get raw value, let's try to get a value from row + } else if let raw = row.value { // We can't get raw value, let's try to get a value from row let amount = Decimal(raw) self.amount = amount - + markRow(row, valid: validateAmount(amount)) - } else { // No value at all + } else { // No value at all amount = nil markRow(row, valid: true) } } else { amount = nil } - + if let row: SafeDecimalRow = form.rowBy(tag: BaseRows.total.tag) { if let amount = amount { - row.value = isNeedAddFee - ? (amount + transactionFee).doubleValue - : amount.doubleValue + row.value = + isNeedAddFee + ? (amount + transactionFee).doubleValue + : amount.doubleValue row.updateCell() markRow(row, valid: validateAmount(amount)) } else { @@ -671,21 +697,22 @@ class TransferViewControllerBase: FormViewController { row.updateCell() } } - + validateAddress() } - + func markRow(_ row: BaseRowType, valid: Bool) { - row.baseCell.textLabel?.textColor = valid - ? getBaseColor(for: row.tag) - : UIColor.adamant.attention + row.baseCell.textLabel?.textColor = + valid + ? getBaseColor(for: row.tag) + : UIColor.adamant.attention } - + func getBaseColor(for tag: String?) -> UIColor { guard let tag = tag, - tag == BaseRows.fee.tag + tag == BaseRows.fee.tag else { return activeBaseColor } - + return inactiveBaseColor } @@ -697,11 +724,11 @@ class TransferViewControllerBase: FormViewController { else { return } - + if let label = recipientSection.header?.viewForSection(recipientSection, type: .header), let label = label as? UILabel { label.textColor = isValid ? UIColor.adamant.primary : UIColor.adamant.attention } - + recipientRow.cell.textField.textColor = isValid ? UIColor.adamant.primary : UIColor.adamant.attention recipientRow.cell.textField.leftView?.subviews.forEach { view in guard let label = view as? UILabel else { return } @@ -714,38 +741,38 @@ class TransferViewControllerBase: FormViewController { row.value = getCellFeeValue() row.updateCell() } - + if let row: SafeDecimalRow = form.rowBy(tag: BaseRows.maxToTransfer.tag) { row.value = maxToTransfer.doubleValue row.updateCell() } - + if let row: SafeDecimalRow = form.rowBy(tag: BaseRows.balance.tag) { if let wallet = walletCore.wallet { row.value = wallet.balance.doubleValue } else { row.value = 0 } - + row.updateCell() } - + validateForm() } - + // MARK: - Send Actions - + @MainActor private func confirmSendFunds() async { dialogService.showProgress(withMessage: nil, userInteractionEnable: true) validateAddress() validateForm(force: true) - + guard let recipientAddress = recipientAddress else { dialogService.showWarning(withMessage: .adamant.transfer.addressValidationError) return } - + let validationResult = validateRecipient(recipientAddress) guard validationResult.isValid else { dialogService.showWarning( @@ -754,19 +781,19 @@ class TransferViewControllerBase: FormViewController { ) return } - + guard let amount = amount, - amount > 0 + amount > 0 else { dialogService.showWarning(withMessage: String.adamant.transfer.amountZeroError) return } - + guard amount <= maxToTransfer else { dialogService.showWarning(withMessage: String.adamant.transfer.amountTooHigh) return } - + do { guard try await amount >= minToTransfer else { dialogService.showWarning(withMessage: .adamant.transfer.amountZeroError) @@ -776,26 +803,26 @@ class TransferViewControllerBase: FormViewController { dialogService.showWarning(withMessage: error.localizedDescription) return } - + guard isEnoughFee() else { dialogService.showWarning(withMessage: String.adamant.transfer.notEnoughFeeError) return } - + guard walletCore.isTransactionFeeValid else { return } - + if admReportRecipient != nil, let account = accountService.account, account.balance < 0.001 { dialogService.showWarning(withMessage: String.adamant.transfer.notEnoughAdmToSendTransfer) return } - + guard reachabilityMonitor.connection else { dialogService.showWarning(withMessage: .adamant.sharedErrors.networkError) return } - + guard apiServiceCompose.get(.adm)?.hasEnabledNode == true || admReportRecipient == nil else { @@ -806,7 +833,7 @@ class TransferViewControllerBase: FormViewController { ) return } - + for group in walletCore.nodeGroups { guard apiServiceCompose.get(group)?.hasEnabledNode == true else { dialogService.showWarning( @@ -817,38 +844,37 @@ class TransferViewControllerBase: FormViewController { return } } - - let recipient: String - if let recipientName = recipientName { + + var recipient: String = recipientAddress + + if let recipientName, recipientName != recipientAddress { recipient = "\(recipientName) \(recipientAddress)" - } else { - recipient = recipientAddress } - + let formattedAmount = balanceFormatter.string(from: amount as NSDecimalNumber)! let title = String.adamant.alert.confirmSendMessage(formattedAmount: formattedAmount, recipient: recipient) - + let alert = UIAlertController(title: title, message: String.adamant.transfer.cantUndo, preferredStyleSafe: .alert, source: nil) - let cancelAction = UIAlertAction(title: String.adamant.alert.cancel , style: .cancel, handler: nil) + let cancelAction = UIAlertAction(title: String.adamant.alert.cancel, style: .cancel, handler: nil) let sendAction = UIAlertAction(title: String.adamant.alert.send, style: .default) { [weak self] _ in self?.sendFunds() } - + dialogService.dismissProgress() alert.addAction(cancelAction) alert.addAction(sendAction) alert.modalPresentationStyle = .overFullScreen present(alert, animated: true, completion: nil) } - + // MARK: - 'Virtual' methods with basic implementation - + /// Currency code, used to get fiat rates /// Default implementation tries to get currency symbol from service. If no service present - its fails var currencyCode: String { walletCore.tokenSymbol } - + /// Override this to provide custom balance formatter var balanceFormatter: NumberFormatter { AdamantBalanceFormat.currencyFormatter( @@ -860,33 +886,33 @@ class TransferViewControllerBase: FormViewController { var feeBalanceFormatter: NumberFormatter { return balanceFormatter } - + var fiatFormatter: NumberFormatter { return AdamantBalanceFormat.fiatFormatter(for: currencyInfoService.currentCurrency) } - + /// Override this to provide custom validation logic /// Default - positive number, amount + fee less than or equal to wallet balance func validateAmount(_ amount: Decimal, withFee: Bool = true) -> Bool { guard amount > 0 else { return false } - + guard let balance = walletCore.wallet?.balance else { return false } - + let minAmount = walletCore.minAmount - + guard minAmount <= amount else { return false } - + let total = withFee ? amount + transactionFee : amount - + return balance >= total } - + func formIsValid() -> Bool { guard let wallet = walletCore.wallet, @@ -906,28 +932,28 @@ class TransferViewControllerBase: FormViewController { return true } - + func isEnoughFee() -> Bool { guard let wallet = walletCore.wallet, - wallet.balance > transactionFee, - walletCore.isTransactionFeeValid + wallet.balance > transactionFee, + walletCore.isTransactionFeeValid else { return false } return true } - + /// Recipient section footer. You can override this to provide custom set of elements. /// You can also override ButtonsStripeViewDelegate implementation /// nil for no stripe func recipientStripe() -> Stripe? { return [.qrCameraReader, .qrPhotoReader] } - + func defaultSceneTitle() -> String? { return WalletViewControllerBase.BaseRows.send.localized } - + /// User loaded address from QR (camera or library) /// /// - Parameter address: raw readed address @@ -937,18 +963,18 @@ class TransferViewControllerBase: FormViewController { uri: address, qqPrefix: walletCore.qqPrefix ) - + guard let parsedAddress = parsedAddress, - case .valid = walletCore.validate(address: parsedAddress.address) + case .valid = walletCore.validate(address: parsedAddress.address) else { return false } - + recipientAddress = parsedAddress.address - + parsedAddress.params?.forEach { param in switch param { case .amount(let amount): let row: SafeDecimalRow? = form.rowBy(tag: BaseRows.amount.tag) - row?.value = Double(amount) + row?.value = Double(amount) row?.updateCell() case .recipient: break @@ -959,9 +985,9 @@ class TransferViewControllerBase: FormViewController { row?.updateCell() } } - + return true - } + } /// Report transfer func reportTransferTo( @@ -971,9 +997,9 @@ class TransferViewControllerBase: FormViewController { hash: String ) async throws { let richMessageType = walletCore.dynamicRichMessageType - + let message: AdamantMessage - + if let replyToMessageId = replyToMessageId { let payload = RichTransferReply( replyto_id: replyToMessageId, @@ -992,32 +1018,32 @@ class TransferViewControllerBase: FormViewController { ) message = AdamantMessage.richMessage(payload: payload) } - + chatsProvider.removeChatPositon(for: admAddress) _ = try await chatsProvider.sendMessage(message, recipientId: admAddress) } - + // MARK: - Abstract - + /// Send funds to recipient after validations /// You must override this method /// Don't forget to call delegate.transferViewControllerDidFinishTransfer(self) after successfull transfer func sendFunds() { fatalError("You must implement sending logic") } - + /// Build recipient address row /// You must override this method func recipientRow() -> BaseRow { fatalError("You must implement recipient row") } - + /// Validate recipient's address func validateRecipient(_ address: String) -> AddressValidationResult { walletCore.validate(address: address) } - - func checkForAdditionalFee() { } + + func checkForAdditionalFee() {} } // MARK: - Default rows @@ -1030,33 +1056,36 @@ extension TransferViewControllerBase { $0.tag = BaseRows.balance.tag $0.disabled = true $0.formatter = self?.balanceFormatter - + if let wallet = self?.walletCore.wallet { $0.value = wallet.balance.doubleValue } else { $0.value = 0 } } - + case .name: let row = LabelRow { [weak self] in $0.title = BaseRows.name.localized $0.tag = BaseRows.name.tag $0.value = self?.recipientName - $0.hidden = Condition.function([], { form in - if let row: RowOf = form.rowBy(tag: BaseRows.name.tag), row.value != nil { - return false - } else { - return true + $0.hidden = Condition.function( + [], + { form in + if let row: RowOf = form.rowBy(tag: BaseRows.name.tag), row.value != nil { + return false + } else { + return true + } } - }) + ) } - + return row - + case .address: return recipientRow() - + case .maxToTransfer: let row = SafeDecimalRow { [weak self] in $0.title = BaseRows.maxToTransfer.localized @@ -1064,7 +1093,7 @@ extension TransferViewControllerBase { $0.disabled = true $0.formatter = self?.balanceFormatter $0.cell.selectionStyle = .gray - + if let maxToTransfer = self?.maxToTransfer { $0.value = maxToTransfer.doubleValue } @@ -1073,9 +1102,9 @@ extension TransferViewControllerBase { row.deselect(animated: true) return } - + let alert = UIAlertController(title: String.adamant.transfer.useMaxToTransfer, message: nil, preferredStyleSafe: .alert, source: nil) - let cancelAction = UIAlertAction(title: String.adamant.alert.cancel , style: .cancel, handler: nil) + let cancelAction = UIAlertAction(title: String.adamant.alert.cancel, style: .cancel, handler: nil) let confirmAction = UIAlertAction(title: String.adamant.alert.ok, style: .default) { [weak self] _ in guard let amountRow: SafeDecimalRow = self?.form.rowBy(tag: BaseRows.amount.tag) else { return @@ -1084,7 +1113,7 @@ extension TransferViewControllerBase { amountRow.updateCell() self?.validateForm() } - + alert.addAction(cancelAction) alert.addAction(confirmAction) alert.modalPresentationStyle = .overFullScreen @@ -1092,16 +1121,21 @@ extension TransferViewControllerBase { row.deselect(animated: true) } } - + return row - + case .amount: return SafeDecimalRow { [weak self] row in row.title = BaseRows.amount.localized row.placeholder = String.adamant.transfer.amountPlaceholder row.tag = BaseRows.amount.tag row.formatter = self?.balanceFormatter - + + if isMacOS { + row.cell._textField.caretInset = 1 + row.cell.adjustHuggingPriority() + } + if let amount = self?.amount { row.value = amount.doubleValue } @@ -1112,26 +1146,26 @@ extension TransferViewControllerBase { } else { fiatRow.value = nil } - + fiatRow.updateCell() } - + self?.validateForm() self?.updateToolbar(for: row) } - + case .fiat: return SafeDecimalRow { [weak self] in $0.title = BaseRows.fiat.localized $0.tag = BaseRows.fiat.tag $0.disabled = true - + $0.formatter = self?.fiatFormatter - + if let rate = self?.rate, let amount = self?.amount { $0.value = amount.doubleValue * rate.doubleValue } - + $0.hidden = Condition.function([]) { [weak self] _ -> Bool in return self?.rate == nil } @@ -1143,29 +1177,32 @@ extension TransferViewControllerBase { $0.value = self?.walletCore.isIncreaseFeeEnabled ?? false }.cellUpdate { [weak self] (cell, row) in cell.switchControl.onTintColor = UIColor.adamant.active - cell.textLabel?.textColor = row.value == true - ? self?.activeBaseColor - : self?.inactiveBaseColor + cell.textLabel?.textColor = + row.value == true + ? self?.activeBaseColor + : self?.inactiveBaseColor }.onChange { [weak self] row in - guard let id = self?.walletCore.tokenUnicID, - let value = row.value + guard let id = self?.walletCore.tokenUniqueID, + let value = row.value else { return } - - row.cell.textLabel?.textColor = value - ? self?.activeBaseColor - : self?.inactiveBaseColor - + + row.cell.textLabel?.textColor = + value + ? self?.activeBaseColor + : self?.inactiveBaseColor + self?.increaseFeeService.setIncreaseFeeEnabled(for: id, value: value) self?.walletCore.update() } case .fee: return DoubleDetailsRow { [weak self] in - let estimateSymbol = self?.walletCore.isDynamicFee == true - ? " ~" - : .empty - + let estimateSymbol = + self?.walletCore.isDynamicFee == true + ? " ~" + : .empty + $0.tag = BaseRows.fee.tag $0.cell.titleLabel.text = "" $0.disabled = true @@ -1174,7 +1211,7 @@ extension TransferViewControllerBase { $0.cell.secondDetailsLabel.textColor = .adamant.attention $0.value = self?.getCellFeeValue() } - + case .total: return SafeDecimalRow { [weak self] in $0.tag = BaseRows.total.tag @@ -1182,12 +1219,12 @@ extension TransferViewControllerBase { $0.value = nil $0.disabled = true $0.formatter = self?.balanceFormatter - + if let balance = self?.walletCore.wallet?.balance { $0.add(rule: RuleSmallerOrEqualThan(max: balance.doubleValue)) } } - + case .comments: let row = TextAreaRow { $0.tag = BaseRows.comments.tag @@ -1197,9 +1234,9 @@ extension TransferViewControllerBase { }.cellUpdate { (cell, _) in cell.textView?.backgroundColor = UIColor.clear } - + return row - + case .blockchainComments: let row = TextAreaRow { $0.tag = BaseRows.blockchainComments(coin: walletCore.tokenName).tag @@ -1212,9 +1249,9 @@ extension TransferViewControllerBase { }.cellUpdate { (cell, _) in cell.textView?.backgroundColor = UIColor.clear } - + return row - + case .sendButton: return ButtonRow { $0.title = BaseRows.sendButton.localized @@ -1224,31 +1261,31 @@ extension TransferViewControllerBase { } } } - + private func getCellFeeValue() -> DoubleDetail { let fee = transactionFee let isWarningGasPrice = walletCore.isWarningGasPrice - + var fiat: Double = 0.0 - + let rate = currencyInfoService.getRate(for: walletCore.blockchainSymbol) if let rate = rate { fiat = fee.doubleValue * rate.doubleValue } - + let feeRaw = fee.doubleValue.format(with: feeBalanceFormatter) let fiatRaw = fiat.format(with: fiatFormatter) - + return DoubleDetail( first: "\(feeRaw) ~\(fiatRaw)", second: isWarningGasPrice - ? String.adamant.transfer.feeIsTooHigh - : nil + ? String.adamant.transfer.feeIsTooHigh + : nil ) } - + // MARK: - Tools - + func shareValue(_ value: String?, from: UIView) { guard let value = value, @@ -1258,53 +1295,56 @@ extension TransferViewControllerBase { return } - dialogService.presentShareAlertFor(string: value, types: [.copyToPasteboard, .share], excludedActivityTypes: nil, animated: true, from: from) { [weak self] in + dialogService.presentShareAlertFor(string: value, types: [.copyToPasteboard, .share], excludedActivityTypes: nil, animated: true, from: from) { + [weak self] in guard let tableView = self?.tableView else { return } - + if let indexPath = tableView.indexPathForSelectedRow { tableView.deselectRow(at: indexPath, animated: true) } } } - + func doesNotContainSendingTx() async -> Bool { var history = walletCore.getLocalTransactionHistory() - + if history.isEmpty { - history = (try? await walletCore.getTransactionsHistory( - offset: .zero, - limit: 2) - ) ?? [] + history = + (try? await walletCore.getTransactionsHistory( + offset: .zero, + limit: 2 + )) ?? [] } - + let havePending = history.contains { $0.transactionStatus == .pending || $0.transactionStatus == .registered || $0.transactionStatus == .notInitiated } - + return !havePending } - + func doesNotContainSendingTx( with nonce: String, senderAddress: String ) async -> Bool { var history = walletCore.getLocalTransactionHistory() - + if history.isEmpty { - history = (try? await walletCore.getTransactionsHistory( - offset: .zero, - limit: 2) - ) ?? [] + history = + (try? await walletCore.getTransactionsHistory( + offset: .zero, + limit: 2 + )) ?? [] } - + let nonces = history.filter { - $0.senderAddress == senderAddress - && $0.transactionStatus != .failed + $0.senderAddress == senderAddress + && $0.transactionStatus != .failed }.compactMap { $0.nonceRaw } - + return !nonces.contains(nonce) } - + func presentSendingError() { dialogService.dismissProgress() dialogService.showAlert( diff --git a/Adamant/Modules/Wallets/WalletAccount.swift b/Adamant/Modules/Wallets/WalletAccount.swift index 1fde4a374..3ec62adaf 100644 --- a/Adamant/Modules/Wallets/WalletAccount.swift +++ b/Adamant/Modules/Wallets/WalletAccount.swift @@ -15,6 +15,6 @@ protocol WalletAccount: Sendable { var address: String { get } var balance: Decimal { get } var isBalanceInitialized: Bool { get } - + var notifications: Int { get } } diff --git a/Adamant/Modules/Wallets/WalletViewControllerBase.swift b/Adamant/Modules/Wallets/WalletViewControllerBase.swift index b96f0a2b3..e59593496 100644 --- a/Adamant/Modules/Wallets/WalletViewControllerBase.swift +++ b/Adamant/Modules/Wallets/WalletViewControllerBase.swift @@ -6,10 +6,10 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import Eureka -import CommonKit import Combine +import CommonKit +import Eureka +import UIKit extension String.adamant { enum wallets { @@ -26,7 +26,7 @@ class WalletViewControllerBase: FormViewController, WalletViewController { // MARK: - Rows enum BaseRows { case address, balance, send - + var tag: String { switch self { case .address: return "a" @@ -34,7 +34,7 @@ class WalletViewControllerBase: FormViewController, WalletViewController { case .send: return "s" } } - + var localized: String { switch self { case .address: return .localized("AccountTab.Row.Address", comment: "Account tab: 'Address' row") @@ -43,46 +43,46 @@ class WalletViewControllerBase: FormViewController, WalletViewController { } } } - + private let cellIdentifier = "cell" - + // MARK: - Dependencies - + private let currencyInfoService: InfoServiceProtocol private let accountService: AccountService private let walletServiceCompose: WalletServiceCompose - + let dialogService: DialogService let screensFactory: ScreensFactory var service: WalletService? // MARK: - Properties, WalletViewController - + var viewController: UIViewController { return self } var height: CGFloat { return tableView.frame.origin.y + tableView.contentSize.height } - + weak var delegate: WalletViewControllerDelegate? - + private lazy var fiatFormatter: NumberFormatter = { return AdamantBalanceFormat.fiatFormatter(for: currencyInfoService.currentCurrency) }() - + private var subscriptions = Set() private let additionalSpace: CGFloat = 5 - + // MARK: - IBOutlets - + @IBOutlet weak var walletTitleLabel: UILabel! @IBOutlet weak var initiatingActivityIndicator: UIActivityIndicatorView! - + // MARK: Error view - + @IBOutlet weak var errorView: UIView! @IBOutlet weak var errorImageView: UIImageView! @IBOutlet weak var errorLabel: UILabel! - + // MARK: Init - + init( dialogService: DialogService, currencyInfoService: InfoServiceProtocol, @@ -99,36 +99,36 @@ class WalletViewControllerBase: FormViewController, WalletViewController { self.service = service super.init(nibName: "WalletViewControllerBase", bundle: nil) } - + required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: - Lifecycle - + override func viewDidLoad() { super.viewDidLoad() setTitle() addObservers() tableView.tableFooterView = UIView() - + let section = Section() // MARK: Address let addressRow = adressRow() - + section.append(addressRow) - + // MARK: Balance let balanceRow = BalanceRow { [weak self] in $0.cell.accessoryType = .disclosureIndicator $0.tag = BaseRows.balance.tag $0.cell.titleLabel.text = BaseRows.balance.localized - + $0.alertBackgroundColor = UIColor.adamant.primary $0.alertTextColor = UIColor.adamant.cellAlertTextColor $0.cell.backgroundColor = UIColor.adamant.cellColor let symbol = self?.service?.core.tokenSymbol ?? "" - + $0.value = self?.balanceRowValueFor( balance: self?.service?.core.wallet?.balance ?? 0, symbol: symbol, @@ -139,7 +139,7 @@ class WalletViewControllerBase: FormViewController, WalletViewController { let row = $0 $0.cell.height = { row.value?.fiat != nil ? BalanceTableViewCell.fullHeight : BalanceTableViewCell.compactHeight } } - + balanceRow.cell.selectionStyle = .gray balanceRow.cellUpdate { (cell, _) in cell.titleLabel.text = BaseRows.balance.localized @@ -148,29 +148,30 @@ class WalletViewControllerBase: FormViewController, WalletViewController { let self = self, let service = service else { return } - - let vc = service.core.hasEnabledNode + + let vc = + service.core.hasEnabledNode ? screensFactory.makeTransferListVC(service: service) : makeNodesList() - + if let split = splitViewController { - let details = UINavigationController(rootViewController:vc) + let details = UINavigationController(rootViewController: vc) split.showDetailViewController(details, sender: self) } else { - navigationController?.pushViewController(vc, animated: true ) + navigationController?.pushViewController(vc, animated: true) } - + if let delegate = delegate { delegate.walletViewControllerSelectedRow(self) } } - + section.append(balanceRow) - + // MARK: Send - + let label = sendRowLocalizedLabel() - + let sendRow = LabelRow { $0.tag = BaseRows.send.tag var content = $0.cell.defaultContentConfiguration() @@ -180,33 +181,34 @@ class WalletViewControllerBase: FormViewController, WalletViewController { $0.cell.backgroundColor = UIColor.adamant.cellColor }.cellUpdate { [weak self] (cell, _) in cell.accessoryType = .disclosureIndicator - - cell.separatorInset = self?.service?.core is AdmWalletService - ? UITableView.defaultSeparatorInset - : .zero - + + cell.separatorInset = + self?.service?.core is AdmWalletService + ? UITableView.defaultSeparatorInset + : .zero + let label = self?.sendRowLocalizedLabel() var content = cell.defaultContentConfiguration() content.attributedText = label cell.contentConfiguration = content }.onCellSelection { [weak self] (_, _) in guard let self = self, let service = service else { return } - + let vc = screensFactory.makeTransferVC(service: service) vc.delegate = self if ERC20Token.supportedTokens.contains(where: { token in return token.symbol == service.core.tokenSymbol }) { - + let ethWallet = walletServiceCompose.getWallet( by: EthWalletService.richMessageType )?.core - + vc.rootCoinBalance = ethWallet?.wallet?.balance } - + if let split = splitViewController { - let details = UINavigationController(rootViewController:vc) + let details = UINavigationController(rootViewController: vc) split.showDetailViewController(details, sender: self) } else { if let nav = navigationController { @@ -216,33 +218,33 @@ class WalletViewControllerBase: FormViewController, WalletViewController { present(vc, animated: true) } } - + if let delegate = delegate { delegate.walletViewControllerSelectedRow(self) } } - + section.append(sendRow) - + form.append(section) - + if let state = service?.core.state { setUiToWalletServiceState(state) } else { setUiToWalletServiceState(.notInitiated) } - + setColors() } - + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - + if let indexPath = tableView.indexPathForSelectedRow { tableView.deselectRow(at: indexPath, animated: animated) } } - + override func viewDidLayoutSubviews() { NotificationCenter.default.post(name: Notification.Name.WalletViewController.heightUpdated, object: self) } @@ -250,21 +252,21 @@ class WalletViewControllerBase: FormViewController, WalletViewController { override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { return UIView() } - + // MARK: - To override - + func sendRowLocalizedLabel() -> NSAttributedString { return NSAttributedString(string: BaseRows.send.localized) } - + func encodeForQr(address: String) -> String? { return nil } - + func includeLogoInQR() -> Bool { return false } - + func adressRow() -> LabelRow { let addressRow = LabelRow { $0.tag = BaseRows.address.tag @@ -284,16 +286,17 @@ class WalletViewControllerBase: FormViewController, WalletViewController { guard let tableView = self?.tableView, let indexPath = tableView.indexPathForSelectedRow else { return } - + tableView.deselectRow(at: indexPath, animated: true) } - + if let address = self?.service?.core.wallet?.address, - let explorerAddress = self?.service?.core.explorerAddress, - let explorerAddressUrl = URL(string: explorerAddress + address) { + let explorerAddress = self?.service?.core.explorerAddress, + let explorerAddressUrl = URL(string: explorerAddress + address) + { let types: [ShareType] let withLogo = self?.includeLogoInQR() ?? false - + if let encodedAddress = self?.encodeForQr(address: address) { types = [ .copyToPasteboard, @@ -308,7 +311,7 @@ class WalletViewControllerBase: FormViewController, WalletViewController { } else { types = [.copyToPasteboard, .share] } - + self?.dialogService.presentShareAlertFor( string: address, types: types, @@ -321,39 +324,39 @@ class WalletViewControllerBase: FormViewController, WalletViewController { } return addressRow } - - func setTitle() { } - + + func setTitle() {} + // MARK: - Other - + private var currentUiState: WalletServiceState = .upToDate - + func setUiToWalletServiceState(_ state: WalletServiceState) { guard currentUiState != state else { return } - + switch state { case .upToDate, .updating: initiatingActivityIndicator.stopAnimating() tableView.isHidden = false errorView.isHidden = true - + case .notInitiated: initiatingActivityIndicator.startAnimating() tableView.isHidden = true errorView.isHidden = true - + case .initiationFailed(let reason): initiatingActivityIndicator.stopAnimating() tableView.isHidden = true errorView.isHidden = false errorLabel.text = reason } - + currentUiState = state } - + private func balanceRowValueFor( balance: Decimal, symbol: String?, @@ -363,13 +366,13 @@ class WalletViewControllerBase: FormViewController, WalletViewController { guard service?.core.hasEnabledNode == true else { return .init(crypto: .adamant.wallets.noEnabledNodes, fiat: nil, alert: nil) } - + guard isBalanceInitialized else { return .init(crypto: .adamant.account.updatingBalance, fiat: nil, alert: nil) } - + let cryptoString = AdamantBalanceFormat.full.format(balance, withCurrencySymbol: symbol) - + let fiatString: String? if balance > 0, let symbol = symbol, let rate = currencyInfoService.getRate(for: symbol) { let fiat = balance * rate @@ -377,29 +380,29 @@ class WalletViewControllerBase: FormViewController, WalletViewController { } else { fiatString = nil } - + if let alert = alert, alert > 0 { return BalanceRowValue(crypto: cryptoString, fiat: fiatString, alert: alert) } else { return BalanceRowValue(crypto: cryptoString, fiat: fiatString, alert: nil) } } - + private func stringFor(balance: Decimal, symbol: String?) -> String { var value = AdamantBalanceFormat.full.format(balance, withCurrencySymbol: symbol) - + if balance > 0, let symbol = symbol, let rate = currencyInfoService.getRate(for: symbol) { let fiat = balance * rate let fiatString = AdamantBalanceFormat.short.format(fiat, withCurrencySymbol: currencyInfoService.currentCurrency.symbol) - + value = "\(value) (\(fiatString))" } - + return value } - + // MARK: - Other - + func setColors() { view.backgroundColor = UIColor.adamant.secondBackgroundColor tableView.backgroundColor = .clear @@ -407,34 +410,34 @@ class WalletViewControllerBase: FormViewController, WalletViewController { } } -private extension WalletViewControllerBase { - func addObservers() { +extension WalletViewControllerBase { + fileprivate func addObservers() { guard let service = service else { return } - + NotificationCenter.default .notifications(named: service.core.serviceStateChanged) .sink { @MainActor [weak self] notification in guard let newState = notification.userInfo?[AdamantUserInfoKey.WalletService.walletState] as? WalletServiceState else { return } - + self?.setUiToWalletServiceState(newState) } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: service.core.walletUpdatedNotification) .sink { @MainActor [weak self] _ in self?.updateWalletUI() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantCurrencyInfoService.currencyRatesUpdated) .sink { @MainActor [weak self] _ in self?.updateWalletUI() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .LanguageStorageService.languageUpdated) .sink { @MainActor [weak self] _ in @@ -443,28 +446,28 @@ private extension WalletViewControllerBase { self?.updateWalletUI() } .store(in: &subscriptions) - + service.core.hasEnabledNodePublisher .sink { [weak self] _ in self?.updateWalletUI() } .store(in: &subscriptions) } - - func updateWalletUI() { + + fileprivate func updateWalletUI() { guard let service = service else { return } - + if let row: LabelRow = form.rowBy(tag: BaseRows.address.tag) { if let wallet = service.core.wallet { row.value = wallet.address row.updateCell() } } - + guard let wallet = service.core.wallet, - let row: BalanceRow = form.rowBy(tag: BaseRows.balance.tag) + let row: BalanceRow = form.rowBy(tag: BaseRows.balance.tag) else { return } - + fiatFormatter.currencyCode = currencyInfoService.currentCurrency.rawValue - + let symbol = service.core.tokenSymbol row.value = balanceRowValueFor( balance: wallet.balance, @@ -475,8 +478,8 @@ private extension WalletViewControllerBase { row.updateCell() row.reload() } - - func makeNodesList() -> UIViewController { + + fileprivate func makeNodesList() -> UIViewController { service?.core.nodeGroups.contains(.adm) == true ? screensFactory.makeNodesList() : screensFactory.makeCoinsNodesList(context: .menu) @@ -496,11 +499,11 @@ extension WalletViewControllerBase: TransferViewControllerDelegate { if let detailsViewController = detailsViewController { var viewControllers = nav.viewControllers viewControllers.removeLast() - + if let service = service { viewControllers.append(screensFactory.makeTransferListVC(service: service)) } - + viewControllers.append(detailsViewController) nav.setViewControllers(viewControllers, animated: true) } else { @@ -520,7 +523,7 @@ extension WalletViewControllerBase: TransferViewControllerDelegate { } } else if presentedViewController == viewController { dismiss(animated: true, completion: nil) - + if let detailsViewController = detailsViewController { detailsViewController.modalPresentationStyle = .overFullScreen present(detailsViewController, animated: true, completion: nil) diff --git a/Adamant/Modules/Wallets/WalletsService/WalletCoreProtocol.swift b/Adamant/Modules/Wallets/WalletsService/WalletCoreProtocol.swift index 595545925..6321fb33d 100644 --- a/Adamant/Modules/Wallets/WalletsService/WalletCoreProtocol.swift +++ b/Adamant/Modules/Wallets/WalletsService/WalletCoreProtocol.swift @@ -6,13 +6,15 @@ // Copyright © 2018 Adamant. All rights reserved. // +import Combine +import CommonKit import Foundation -import UIKit import Swinject -import CommonKit +import UIKit enum WalletServiceState: Equatable { - case notInitiated, updating, upToDate, initiationFailed(reason: String) + case notInitiated, updating, upToDate + case initiationFailed(reason: String) } enum WalletServiceSimpleResult { @@ -44,68 +46,74 @@ extension WalletServiceError: RichError { switch self { case .notLogged: return String.adamant.sharedErrors.userNotLogged - + case .notEnoughMoney: return String.adamant.sharedErrors.notEnoughMoney - + case .networkError: return String.adamant.sharedErrors.networkError - + case .accountNotFound: return String.adamant.transfer.accountNotFound - + case .walletNotInitiated: - return .localized("WalletServices.SharedErrors.WalletNotInitiated", comment: "Wallet Services: Shared error, user has not yet initiated a specific wallet.") - + return .localized( + "WalletServices.SharedErrors.WalletNotInitiated", + comment: "Wallet Services: Shared error, user has not yet initiated a specific wallet." + ) + case .remoteServiceError(let message, let error): return String.adamant.sharedErrors.remoteServerError(message: message) - + case .apiError(let error): return error.localizedDescription - + case .internalError(let message, _): return String.adamant.sharedErrors.internalError(message: message) - + case .invalidAmount(let amount): - return String.localizedStringWithFormat(.localized("WalletServices.SharedErrors.InvalidAmountFormat", comment: "Wallet Services: Shared error, invalid amount format. %@ for amount"), AdamantBalanceFormat.full.format(amount)) - + return String.localizedStringWithFormat( + .localized("WalletServices.SharedErrors.InvalidAmountFormat", comment: "Wallet Services: Shared error, invalid amount format. %@ for amount"), + AdamantBalanceFormat.full.format(amount) + ) + case .transactionNotFound: return .localized("WalletServices.SharedErrors.TransactionNotFound", comment: "Wallet Services: Shared error, transaction not found") - + case .requestCancelled: return String.adamant.sharedErrors.requestCancelled case .dustAmountError: return String.adamant.sharedErrors.dustError } } - + var internalError: Error? { switch self { case .internalError(_, let error): return error default: return nil } } - + var level: ErrorLevel { switch self { case .notLogged, .notEnoughMoney, .networkError, .accountNotFound, .invalidAmount, .walletNotInitiated, .transactionNotFound, .requestCancelled: return .warning - + case .dustAmountError, .remoteServiceError: return .error - + case .internalError: return .internalError - + case .apiError(let error): return error.level } } - + static func internalError(_ error: InternalAPIError) -> Self { .internalError(message: error.localizedDescription, error: error) } - + static func remoteServiceError(message: String? = nil, error: Error? = nil) -> Self { .remoteServiceError( message: message ?? error?.localizedDescription ?? .empty, @@ -123,11 +131,11 @@ extension WalletServiceError: HealthCheckableError { return false } } - + public static var noNetworkError: WalletServiceError { .apiError(.noNetworkError) } - + static func noEndpointsError(nodeGroupName: String) -> WalletServiceError { .apiError(.noEndpointsError(nodeGroupName: nodeGroupName)) } @@ -138,16 +146,16 @@ extension ApiServiceError { switch self { case .accountNotFound: return .accountNotFound - + case .networkError: return .networkError - + case .notLogged: return .notLogged - + case .requestCancelled: return .requestCancelled - + case .serverError, .internalError, .commonError, .noEndpointsAvailable: return .apiError(self) } @@ -159,40 +167,40 @@ extension ChatsProviderError { switch self { case .notLogged: return .notLogged - + case .messageNotValid: return .notLogged - + case .notEnoughMoneyToSend: return .notEnoughMoney - + case .networkError: return .networkError - + case .serverError(let e as ApiServiceError): return .apiError(e) - + case .serverError(let e): - return .internalError(message: self.message, error: e) - + return .internalError(message: self.message, error: e.wrappedError) + case .accountNotFound: return .accountNotFound - + case .dependencyError(let message): return .internalError(message: message, error: nil) - + case .transactionNotFound(let id): return .transactionNotFound(reason: "\(id)") - + case .internalError(let error): return .internalError(message: self.message, error: error) - + case .accountNotInitiated: return .walletNotInitiated - + case .requestCancelled: return .requestCancelled - + case .invalidTransactionStatus: return .internalError(message: "Invalid Transaction Status", error: nil) } @@ -204,7 +212,7 @@ extension AdamantUserInfoKey { struct WalletService { static let wallet = "Adamant.WalletService.wallet" static let walletState = "Adamant.WalletService.walletState" - + private init() {} } } @@ -213,7 +221,7 @@ extension AdamantUserInfoKey { extension Notification.Name { struct WalletViewController { static let heightUpdated = Notification.Name("adamant.walletViewController") - + private init() {} } } @@ -228,14 +236,10 @@ protocol WalletViewController { // MARK: - Wallet Service protocol WalletCoreProtocol: AnyObject, Sendable { // MARK: Currency - static var currencySymbol: String { get } - static var currencyLogo: UIImage { get } - static var qqPrefix: String { get } - var tokenSymbol: String { get } var tokenName: String { get } var tokenLogo: UIImage { get } - var tokenUnicID: String { get } + var tokenUniqueID: String { get } static var tokenNetworkSymbol: String { get } var consistencyMaxTime: Double { get } var tokenContract: String { get } @@ -249,52 +253,55 @@ protocol WalletCoreProtocol: AnyObject, Sendable { var nodeGroups: [NodeGroup] { get } var transferDecimals: Int { get } var explorerAddress: String { get } - + var transactionsPublisher: AnyObservable<[TransactionDetails]> { get } - + var hasMoreOldTransactionsPublisher: AnyObservable { get } - + /// Lowercased!! static var richMessageType: String { get } - + // MARK: Transactions fetch info - + var newPendingInterval: TimeInterval { get } var oldPendingInterval: TimeInterval { get } var registeredInterval: TimeInterval { get } var newPendingAttempts: Int { get } var oldPendingAttempts: Int { get } - + // MARK: Notifications - + /// Wallet updated. /// UserInfo contains new wallet at AdamantUserInfoKey.WalletService.wallet var walletUpdatedNotification: Notification.Name { get } - + /// Enabled state changed var serviceEnabledChanged: Notification.Name { get } - + /// State changed var serviceStateChanged: Notification.Name { get } - + // MARK: State var wallet: WalletAccount? { get } var state: WalletServiceState { get } var enabled: Bool { get } - + // MARK: Logic @MainActor var hasEnabledNode: Bool { get } - + @MainActor var hasEnabledNodePublisher: AnyObservable { get } - + + @MainActor + var walletUpdatePublisher: AnyObservable { get } + func update() - + // MARK: Tools func validate(address: String) -> AddressValidationResult func getWalletAddress(byAdamantAddress address: String) async throws -> String @@ -305,27 +312,27 @@ protocol WalletCoreProtocol: AnyObject, Sendable { func updateStatus(for id: String, status: TransactionStatus?) func isExist(address: String) async throws -> Bool func statusInfoFor(transaction: CoinTransaction) async -> TransactionStatusInfo - func initWallet(withPassphrase: String) async throws -> WalletAccount + func initWallet(withPassphrase: String, withPassword: String) async throws -> WalletAccount func setInitiationFailed(reason: String) func shortDescription(for transaction: RichMessageTransaction) -> NSAttributedString func getFee(comment: String) -> Decimal - + // MARK: Send - + var transactionFeeUpdated: Notification.Name { get } - + var qqPrefix: String { get } var blockchainSymbol: String { get } - var isDynamicFee : Bool { get } - var diplayTransactionFee : Decimal { get } - var transactionFee : Decimal { get } - var isWarningGasPrice : Bool { get } - var isTransactionFeeValid : Bool { get } + var isDynamicFee: Bool { get } + var diplayTransactionFee: Decimal { get } + var transactionFee: Decimal { get } + var isWarningGasPrice: Bool { get } + var isTransactionFeeValid: Bool { get } var commentsEnabledForRichMessages: Bool { get } var isSupportIncreaseFee: Bool { get } var isIncreaseFeeEnabled: Bool { get } var defaultIncreaseFee: Decimal { get } - var additionalFee : Decimal { get } + var additionalFee: Decimal { get } } extension WalletCoreProtocol { @@ -381,14 +388,14 @@ protocol WalletServiceSimpleSend: WalletCoreProtocol { protocol WalletServiceTwoStepSend: WalletCoreProtocol { associatedtype T: RawTransaction - + func createTransaction( recipient: String, amount: Decimal, fee: Decimal, comment: String? ) async throws -> T - + func sendTransaction(_ transaction: T) async throws } diff --git a/Adamant/Modules/Wallets/WalletsService/WalletService.swift b/Adamant/Modules/Wallets/WalletsService/WalletService.swift index 9d13690fb..bfda74281 100644 --- a/Adamant/Modules/Wallets/WalletsService/WalletService.swift +++ b/Adamant/Modules/Wallets/WalletsService/WalletService.swift @@ -6,12 +6,12 @@ // Copyright © 2023 Adamant. All rights reserved. // -import Foundation import CoreData +import Foundation final class WalletService: WalletServiceProtocol { let core: WalletCoreProtocol - + init(core: WalletCoreProtocol) { self.core = core } diff --git a/Adamant/Modules/Wallets/WalletsService/WalletServiceCompose.swift b/Adamant/Modules/Wallets/WalletsService/WalletServiceCompose.swift index 93a4812a7..c2623e297 100644 --- a/Adamant/Modules/Wallets/WalletsService/WalletServiceCompose.swift +++ b/Adamant/Modules/Wallets/WalletsService/WalletServiceCompose.swift @@ -15,17 +15,19 @@ protocol WalletServiceCompose: Sendable { struct AdamantWalletServiceCompose: WalletServiceCompose { private let wallets: [String: WalletService] - + init(wallets: [WalletCoreProtocol]) { - self.wallets = Dictionary(uniqueKeysWithValues: wallets.map { wallet in - (wallet.dynamicRichMessageType, WalletService(core: wallet)) - }) + self.wallets = Dictionary( + uniqueKeysWithValues: wallets.map { wallet in + (wallet.dynamicRichMessageType, WalletService(core: wallet)) + } + ) } - + func getWallet(by type: String) -> WalletService? { wallets[type] } - + func getWallets() -> [WalletService] { Array(wallets.values) } diff --git a/Adamant/Modules/Wallets/WalletsService/WalletStaticCoreProtocol.swift b/Adamant/Modules/Wallets/WalletsService/WalletStaticCoreProtocol.swift new file mode 100644 index 000000000..06b012b32 --- /dev/null +++ b/Adamant/Modules/Wallets/WalletsService/WalletStaticCoreProtocol.swift @@ -0,0 +1,145 @@ +import AdamantWalletsKit +import BigInt +import CommonKit +// +// WalletStaticCoreProtocol.swift +// Adamant +// +// Created by Владимир Клевцов on 17.1.25.. +// Copyright © 2025 Adamant. All rights reserved. +// +import Foundation +import UIKit + +protocol WalletStaticCoreProtocol { + static var currencySymbol: String { get } +} + +extension WalletStaticCoreProtocol { + static var coinInfo: CoinInfoDTO? { + CoinInfoProvider.storage?[currencySymbol] + } + + static var fixedFee: Decimal { + coinInfo?.fixedFee ?? coinInfo?.defaultFee ?? 0.0 + } + + static var currencyExponent: Int { + -(coinInfo?.decimals ?? 0) + } + + static var qqPrefix: String { + coinInfo?.qqPrefix ?? "" + } + static var healthCheckParameters: CoinHealthCheckParameters { + let coinInfoNH = coinInfo?.nodes?.healthCheck + let coinInfoSIH = coinInfo?.services?.infoService?.healthCheck + let coinInfoSNH = coinInfo?.services?.ipfsNode?.healthCheck + + if let coinInfoNH { + return CoinHealthCheckParameters( + normalUpdateInterval: TimeInterval(coinInfoNH.normalUpdateInterval / 1000), + crucialUpdateInterval: TimeInterval(coinInfoNH.crucialUpdateInterval / 1000), + onScreenUpdateInterval: TimeInterval(coinInfoNH.onScreenUpdateInterval / 1000), + + threshold: coinInfoNH.threshold ?? 0, + + normalServiceUpdateInterval: TimeInterval( + coinInfoSIH?.normalUpdateInterval ?? coinInfoSNH?.normalUpdateInterval ?? coinInfoNH.normalUpdateInterval / 1000 + ), + crucialServiceUpdateInterval: TimeInterval( + coinInfoSIH?.crucialUpdateInterval ?? coinInfoSNH?.crucialUpdateInterval ?? coinInfoNH.crucialUpdateInterval / 1000 + ), + onScreenServiceUpdateInterval: TimeInterval( + coinInfoSIH?.onScreenUpdateInterval ?? coinInfoSNH?.onScreenUpdateInterval ?? coinInfoNH.onScreenUpdateInterval / 1000 + ) + ) + } + print("error with healsCheck values") + return CoinHealthCheckParameters( + normalUpdateInterval: 300, + crucialUpdateInterval: 30, + onScreenUpdateInterval: 10, + threshold: 10, + normalServiceUpdateInterval: 300, + crucialServiceUpdateInterval: 30, + onScreenServiceUpdateInterval: 10 + ) + } + static var newPendingInterval: Int { + coinInfo?.txFetchInfo?.newPendingInterval ?? 0 + } + + static var oldPendingInterval: Int { + coinInfo?.txFetchInfo?.oldPendingInterval ?? 0 + } + + static var registeredInterval: Int { + coinInfo?.txFetchInfo?.registeredInterval ?? 0 + } + + static var newPendingAttempts: Int { + coinInfo?.txFetchInfo?.newPendingAttempts ?? 0 + } + + static var oldPendingAttempts: Int { + coinInfo?.txFetchInfo?.oldPendingAttempts ?? 0 + } + + var tokenName: String { + Self.coinInfo?.name ?? "" + } + + var consistencyMaxTime: Double { + Double(Self.coinInfo?.txConsistencyMaxTime ?? 0) / 1000.0 + } + + var minBalance: Decimal { + Self.coinInfo?.minBalance ?? 0.0 + } + + var minAmount: Decimal { + Decimal(Self.coinInfo?.minTransferAmount ?? 0.0) + } + + var defaultVisibility: Bool { + Self.coinInfo?.defaultVisibility ?? false + } + + var defaultOrdinalLevel: Int? { + Self.coinInfo?.defaultOrdinalLevel + } + + static var minNodeVersion: String? { + coinInfo?.nodes?.minVersion + } + + var transferDecimals: Int { + Self.coinInfo?.cryptoTransferDecimals ?? 0 + } + + static var explorerAddress: String { + coinInfo?.explorerAddress?.replacingOccurrences(of: "${ID}", with: "") ?? "" + } + static var explorerTx: String { + coinInfo?.explorerTx?.replacingOccurrences(of: "${ID}", with: "") ?? "" + } + + static var nodes: [Node] { + coinInfo?.nodes?.list.map { walletNode in + Node.makeDefaultNode( + url: URL(string: walletNode.url)!, + altUrl: walletNode.altIP.flatMap { URL(string: $0) } + ) + } ?? [] + } + + static var serviceNodes: [Node] { + coinInfo?.services?.infoService?.list.map { serviceNode in + Node.makeDefaultNode( + url: URL(string: serviceNode.url)!, + altUrl: serviceNode.altIP.flatMap { URL(string: $0) } + ) + } ?? [] + } +} diff --git a/Adamant/ServiceProtocols/AccountService.swift b/Adamant/ServiceProtocols/AccountService.swift index edf5c780c..051de5219 100644 --- a/Adamant/ServiceProtocols/AccountService.swift +++ b/Adamant/ServiceProtocols/AccountService.swift @@ -6,43 +6,43 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation // MARK: - Notifications extension Notification.Name { struct AdamantAccountService { /// Raised when user has successfully logged in. See AdamantUserInfoKey.AccountService static let userLoggedIn = Notification.Name("adamant.accountService.userHasLoggedIn") - + /// Raised when user has logged out. static let userLoggedOut = Notification.Name("adamant.accountService.userHasLoggedOut") - + /// Raised when user is about to log out. Save your data. static let userWillLogOut = Notification.Name("adamant.accountService.userWillLogOut") - + /// Raised on account info (balance) updated. static let accountDataUpdated = Notification.Name("adamant.accountService.accountDataUpdated") - + /// Raised on account info (balance) updated. static let forceUpdateBalance = Notification.Name("adamant.accountService.forceUpdateBalance") - + /// Raised on account info (balance) updated. static let forceUpdateAllBalances = Notification.Name("adamant.accountService.forceUpdateAllBalances") - + /// Raised when user changed Stay In option. /// /// UserInfo: /// - Adamant.AccountService.newStayInState with new state static let stayInChanged = Notification.Name("adamant.accountService.stayInChanged") - + /// Raised when wallets collection updated /// /// UserInfo: /// - AdamantUserInfoKey.AccountService.updatedWallet: wallet object /// - AdamantUserInfoKey.AccountService.updatedWalletIndex: wallet index in AccountService.wallets collection static let walletUpdated = Notification.Name("adamant.accountService.walletUpdated") - + private init() {} } } @@ -54,7 +54,10 @@ extension String.adamant { String.localized("AccountService.update.v12.title", comment: "AccountService: Alert title. Changes in version 1.2") } static var updateAlertMessageV12: String { - String.localized("AccountService.update.v12.message", comment: "AccountService: Alert message. Changes in version 1.2, notify user that he needs to relogin to initiate eth & lsk wallets") + String.localized( + "AccountService.update.v12.message", + comment: "AccountService: Alert message. Changes in version 1.2, notify user that he needs to relogin to initiate eth & lsk wallets" + ) } static var reloginToInitiateWallets: String { String.localized("AccountService.reloginToInitiateWallets", comment: "AccountService: User must relogin into app to initiate wallets") @@ -62,7 +65,6 @@ extension String.adamant { } } - /// - loggedAccountAddress: Newly logged account's address extension AdamantUserInfoKey { struct AccountService { @@ -70,7 +72,7 @@ extension AdamantUserInfoKey { static let newStayInState = "adamant.accountService.stayIn" static let updatedWallet = "adamant.accountService.updatedWallet" static let updatedWalletIndex = "adamant.accountService.updatedWalletIndex" - + private init() {} } } @@ -96,21 +98,21 @@ enum AccountServiceError: Error { case wrongPassphrase case apiError(error: ApiServiceError) case internalError(message: String, error: Error?) - + var localized: String { switch self { case .userNotLogged: return String.adamant.sharedErrors.userNotLogged - + case .invalidPassphrase: return .localized("AccountServiceError.InvalidPassphrase", comment: "Login: user typed in invalid passphrase") - + case .wrongPassphrase: return .localized("AccountServiceError.WrongPassphrase", comment: "Login: user typed in wrong passphrase") - + case .apiError(let error): return error.localizedDescription - + case .internalError(let message, _): return String.adamant.sharedErrors.internalError(message: message) } @@ -121,25 +123,25 @@ extension AccountServiceError: RichError { var message: String { return localized } - + var internalError: Error? { switch self { case .apiError(let error as Error?), .internalError(_, let error): return error - + default: return nil } } - + var level: ErrorLevel { switch self { case .wrongPassphrase, .userNotLogged, .invalidPassphrase: return .warning - + case .apiError(let error): return error.level - + case .internalError: return .internalError } @@ -147,53 +149,54 @@ extension AccountServiceError: RichError { } // MARK: - Protocol +// sourcery: AutoMockable protocol AccountService: AnyObject, Sendable { // MARK: State - + var state: AccountServiceState { get } var isBalanceExpired: Bool { get } var account: AdamantAccount? { get } var keypair: Keypair? { get } - + // MARK: Account functions - + /// Update logged account info func update() func update(_ completion: (@Sendable (AccountServiceResult) -> Void)?) - + /// Login into Adamant using passphrase. - func loginWith(passphrase: String) async throws -> AccountServiceResult - + func loginWith(passphrase: String, password: String) async throws -> AccountServiceResult + /// Login into Adamant using previously logged account func loginWithStoredAccount() async throws -> AccountServiceResult - + /// Logout func logout() - + /// Reload current wallets state - func reloadWallets() - + func reloadWallets() async + // MARK: Stay in functions - + /// There is a stored account information in secured store var hasStayInAccount: Bool { get } - + /// Use TouchID or FaceID to log in var useBiometry: Bool { get } - + /// Save account data and use pincode to login /// /// - Parameters: /// - pin: pincode to login - /// - completion: completion handler - func setStayLoggedIn(pin: String, completion: @escaping @Sendable (AccountServiceResult) -> Void) - + /// - Returns:AccountServiceResult with either success or failure + func setStayLoggedIn(pin: String) -> AccountServiceResult + /// Remove stored data func dropSavedAccount() - + /// If we have stored data with pin, validate it. If no data saved, always returns false. func validatePin(_ pin: String) -> Bool - + /// Update use TouchID or FaceID to log in func updateUseBiometry(_ newValue: Bool) } diff --git a/Adamant/ServiceProtocols/AddressBookService.swift b/Adamant/ServiceProtocols/AddressBookService.swift index b7e5b1edd..11841cdfc 100644 --- a/Adamant/ServiceProtocols/AddressBookService.swift +++ b/Adamant/ServiceProtocols/AddressBookService.swift @@ -6,8 +6,8 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation // MARK: - Notifications @@ -15,7 +15,7 @@ extension Notification.Name { struct AdamantAddressBookService { /// Raised when user rename accounts in chat static let addressBookUpdated = Notification.Name("adamant.addressBookService.updated") - + private init() {} } } @@ -28,10 +28,10 @@ enum AddressBookChange { extension AdamantUserInfoKey { struct AddressBook { - + /// Array of AddressBookChangeType static let changes = "adamant.addressBook.changes" - + private init() {} } } @@ -55,15 +55,15 @@ extension AddressBookServiceError: RichError { switch self { case .notLogged: return String.adamant.sharedErrors.userNotLogged - + case .notEnoughMoney: return .localized("AddressBookService.Error.notEnoughMoney", comment: "AddressBookService: Not enought money to save address into blockchain") - + case .apiServiceError(let error): return error.message case .internalError(let message, _): return message } } - + var internalError: Error? { switch self { case .notLogged, .notEnoughMoney: return nil @@ -71,7 +71,7 @@ extension AddressBookServiceError: RichError { case .internalError(_, let error): return error } } - + var level: ErrorLevel { switch self { case .notLogged, .notEnoughMoney: return .warning @@ -87,10 +87,10 @@ protocol AddressBookService: AnyObject, Sendable { func set(name: String, for: String) async func getName(for key: String) -> String? func getName(for partner: BaseAccount?) -> String? - + // MARK: Updating & saving - func update() async -> AddressBookServiceResult? - + func update() async -> AddressBookServiceResult? + var hasChanges: Bool { get } func saveIfNeeded() async } diff --git a/Adamant/ServiceProtocols/ApiServiceComposeProtocol.swift b/Adamant/ServiceProtocols/ApiServiceComposeProtocol.swift index 5f506279e..6858bad34 100644 --- a/Adamant/ServiceProtocols/ApiServiceComposeProtocol.swift +++ b/Adamant/ServiceProtocols/ApiServiceComposeProtocol.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation protocol ApiServiceComposeProtocol { func get(_ group: NodeGroup) -> ApiServiceProtocol? diff --git a/Adamant/ServiceProtocols/AvatarService.swift b/Adamant/ServiceProtocols/AvatarService.swift index 168b91d35..562ab87fa 100644 --- a/Adamant/ServiceProtocols/AvatarService.swift +++ b/Adamant/ServiceProtocols/AvatarService.swift @@ -6,12 +6,12 @@ // Copyright © 2018 Adamant. All rights reserved. // +import CommonKit import Foundation import UIKit -import CommonKit protocol AvatarService: Sendable { - func avatar(for key:String, size: Double) -> UIImage + func avatar(for key: String, size: Double) -> UIImage } extension AdamantAvatarService: AvatarService {} diff --git a/Adamant/ServiceProtocols/CellFactory.swift b/Adamant/ServiceProtocols/CellFactory.swift index ffd2eb47d..0b011e173 100644 --- a/Adamant/ServiceProtocols/CellFactory.swift +++ b/Adamant/ServiceProtocols/CellFactory.swift @@ -12,7 +12,7 @@ struct SharedCell: Equatable, Hashable { let cellIdentifier: String let defaultXibName: String let defaultRowHeight: CGFloat - + init(cellIdentifier: String, xibName: String, rowHeight: CGFloat) { self.cellIdentifier = cellIdentifier self.defaultXibName = xibName diff --git a/Adamant/ServiceProtocols/ChatFileProtocol.swift b/Adamant/ServiceProtocols/ChatFileProtocol.swift index bf9e8d02e..4cdfb9b50 100644 --- a/Adamant/ServiceProtocols/ChatFileProtocol.swift +++ b/Adamant/ServiceProtocols/ChatFileProtocol.swift @@ -6,11 +6,11 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation -import CommonKit import Combine -import UIKit +import CommonKit import FilesStorageKit +import Foundation +import UIKit struct FileUpdateProperties { let id: String @@ -29,7 +29,7 @@ protocol ChatFileProtocol: Sendable { var uploadingFiles: [String] { get } var filesLoadingProgress: [String: Int] { get } var updateFileFields: AnyObservable { get } - + func sendFile( text: String?, chatroom: Chatroom?, @@ -37,7 +37,7 @@ protocol ChatFileProtocol: Sendable { replyMessage: MessageModel?, saveEncrypted: Bool ) async throws - + func downloadFile( file: ChatFile, chatroom: Chatroom?, @@ -45,7 +45,7 @@ protocol ChatFileProtocol: Sendable { previewDownloadAllowed: Bool, fullMediaDownloadAllowed: Bool ) async throws - + func autoDownload( file: ChatFile, chatroom: Chatroom?, @@ -54,13 +54,13 @@ protocol ChatFileProtocol: Sendable { fullMediaDownloadPolicy: DownloadPolicy, saveEncrypted: Bool ) async - + func getDecodedData( file: FilesStorageKit.File, nonce: String, chatroom: Chatroom? ) throws -> Data - + func resendMessage( with id: String, text: String?, @@ -68,15 +68,17 @@ protocol ChatFileProtocol: Sendable { replyMessage: MessageModel?, saveEncrypted: Bool ) async throws - + func isDownloadPreviewLimitReached(for fileId: String) -> Bool - + + func cancelUpload(messageId: String, fileId: String) async + func isPreviewAutoDownloadAllowedByPolicy( hasPartnerName: Bool, isFromCurrentSender: Bool, downloadPolicy: DownloadPolicy ) -> Bool - + func isOriginalAutoDownloadAllowedByPolicy( hasPartnerName: Bool, isFromCurrentSender: Bool, diff --git a/Adamant/ServiceProtocols/ChatPreservation.swift b/Adamant/ServiceProtocols/ChatPreservation.swift index 3f51c413f..e12387f5a 100644 --- a/Adamant/ServiceProtocols/ChatPreservation.swift +++ b/Adamant/ServiceProtocols/ChatPreservation.swift @@ -6,16 +6,17 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation -import CommonKit import Combine +import CommonKit +import Foundation final class ChatPreservation: ChatPreservationProtocol, @unchecked Sendable { @Atomic private var preservedMessages: [String: String] = [:] @Atomic private var preservedReplayMessage: [String: MessageModel] = [:] @Atomic private var preservedFiles: [String: [FileResult]] = [:] @Atomic private var notificationsSet: Set = [] - + + var updateNotifier = ObservableSender() init() { NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedOut) @@ -24,63 +25,76 @@ final class ChatPreservation: ChatPreservationProtocol, @unchecked Sendable { } .store(in: ¬ificationsSet) } - + // MARK: Notification actions - + private func clearPreservedMessages() { preservedMessages = [:] preservedReplayMessage = [:] preservedFiles = [:] + + updateNotifier.send() } - - func preserveMessage(_ message: String, forAddress address: String) { - preservedMessages[address] = message + + func preserveChatState( + message: String?, + replyMessage: MessageModel?, + files: [FileResult]?, + forAddress address: String + ) { + var shouldNotify = false + + if let message = message, !message.isEmpty { + preservedMessages[address] = message + shouldNotify = true + } else if preservedMessages[address] != nil { + preservedMessages.removeValue(forKey: address) + shouldNotify = true + } + + if let replyMessage = replyMessage { + preservedReplayMessage[address] = replyMessage + shouldNotify = true + } else if preservedReplayMessage[address] != nil { + preservedReplayMessage.removeValue(forKey: address) + shouldNotify = true + } + + if let files = files { + preservedFiles[address] = files + shouldNotify = true + + } else if preservedFiles[address] != nil { + preservedFiles.removeValue(forKey: address) + shouldNotify = true + } + + if shouldNotify { + updateNotifier.send() + } } - - func getPreservedMessageFor(address: String, thenRemoveIt: Bool) -> String? { + + func getPreservedMessageFor(address: String) -> String? { guard let message = preservedMessages[address] else { return nil } - if thenRemoveIt { - preservedMessages.removeValue(forKey: address) - } - return message } - - func setReplyMessage(_ message: MessageModel?, forAddress address: String) { - preservedReplayMessage[address] = message - } - - func getReplyMessage(address: String, thenRemoveIt: Bool) -> MessageModel? { - guard let replayMessage = preservedReplayMessage[address] else { + + func getReplyMessage(address: String) -> MessageModel? { + guard let replyMessage = preservedReplayMessage[address] else { return nil } - - if thenRemoveIt { - preservedMessages.removeValue(forKey: address) - } - - return replayMessage - } - - func preserveFiles(_ files: [FileResult]?, forAddress address: String) { - preservedFiles[address] = files + + return replyMessage } - - func getPreservedFiles( - for address: String, - thenRemoveIt: Bool - ) -> [FileResult]? { + + func getPreservedFiles(for address: String) -> [FileResult]? { guard let files = preservedFiles[address] else { return nil } - if thenRemoveIt { - preservedFiles.removeValue(forKey: address) - } - return files } } diff --git a/Adamant/ServiceProtocols/ChatTransactionService.swift b/Adamant/ServiceProtocols/ChatTransactionService.swift index d45c3c533..128ef5248 100644 --- a/Adamant/ServiceProtocols/ChatTransactionService.swift +++ b/Adamant/ServiceProtocols/ChatTransactionService.swift @@ -6,17 +6,18 @@ // Copyright © 2022 Adamant. All rights reserved. // -import Foundation -import CoreData import CommonKit +import CoreData +import Foundation // - MARK: SocketService +// sourcery: AutoMockable protocol ChatTransactionService: AnyObject, Actor { - + /// Make operations serial func addOperations(_ op: Operation) - + /// Parse raw transaction into CoreData chat transaction /// /// - Parameters: @@ -28,14 +29,22 @@ protocol ChatTransactionService: AnyObject, Actor { /// - privateKey: logged account private key /// - context: context to insert parsed transaction to /// - Returns: New parsed transaction - func chatTransaction(from transaction: Transaction, isOutgoing: Bool, publicKey: String, privateKey: String, partner: BaseAccount, removedMessages: [String], context: NSManagedObjectContext) -> ChatTransaction? - + func chatTransaction( + from transaction: CommonKit.Transaction, + isOutgoing: Bool, + publicKey: String, + privateKey: String, + partner: BaseAccount, + removedMessages: [String], + context: NSManagedObjectContext + ) -> ChatTransaction? + /// Search transaction in local storage /// /// - Parameter id: Transacton ID /// - Returns: Transaction, if found func getTransfer(id: String, context: NSManagedObjectContext) -> TransferTransaction? - + /// Create a transaction /// /// - Parameters: @@ -44,6 +53,11 @@ protocol ChatTransactionService: AnyObject, Actor { /// - partner: Partner account /// - context: context to insert parsed transaction to /// - Returns: Transaction - func transferTransaction(from transaction: Transaction, isOut: Bool, partner: BaseAccount?, context: NSManagedObjectContext) -> TransferTransaction - + func transferTransaction( + from transaction: CommonKit.Transaction, + isOut: Bool, + partner: BaseAccount?, + context: NSManagedObjectContext + ) -> TransferTransaction + } diff --git a/Adamant/ServiceProtocols/CoinStorage.swift b/Adamant/ServiceProtocols/CoinStorage.swift index 912281324..9687db05b 100644 --- a/Adamant/ServiceProtocols/CoinStorage.swift +++ b/Adamant/ServiceProtocols/CoinStorage.swift @@ -13,7 +13,7 @@ protocol CoinStorageService: AnyObject { var transactionsPublisher: any Observable<[TransactionDetails]> { get } - + func append(_ transaction: TransactionDetails) func append(_ transactions: [TransactionDetails]) func clear() diff --git a/Adamant/ServiceProtocols/DataProviders/AccountsProvider.swift b/Adamant/ServiceProtocols/DataProviders/AccountsProvider.swift index f640eba21..6c12db420 100644 --- a/Adamant/ServiceProtocols/DataProviders/AccountsProvider.swift +++ b/Adamant/ServiceProtocols/DataProviders/AccountsProvider.swift @@ -6,9 +6,9 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation -import CoreData import CommonKit +import CoreData +import Foundation enum AccountsProviderError: Error { case dummy(BaseAccount) @@ -17,25 +17,28 @@ enum AccountsProviderError: Error { case notInitiated(address: String) case serverError(Error) case networkError(Error) - + var localized: String { switch self { case .dummy: return "Dummy" - + case .notFound(let address): return String.adamant.sharedErrors.accountNotFound(address) - + case .invalidAddress(let address): - return String.localizedStringWithFormat(.localized("AccountsProvider.Error.AddressNotValidFormat", comment: "AccountsProvider: Address not valid error, %@ for address"), address) - + return String.localizedStringWithFormat( + .localized("AccountsProvider.Error.AddressNotValidFormat", comment: "AccountsProvider: Address not valid error, %@ for address"), + address + ) + case .notInitiated: return String.adamant.sharedErrors.accountNotInitiated - + case .serverError(let error): return ApiServiceError.serverError(error: error.localizedDescription) .localizedDescription - + case .networkError: return String.adamant.sharedErrors.networkError } @@ -48,28 +51,23 @@ enum AccountsProviderDummyAccountError: Error { case internalError(Error) } +// sourcery: AutoMockable @MainActor protocol AccountsProvider: Sendable { - + /// Search for fetched account, if not found, asks server for account. /// /// - Returns: Account, if found, created in main viewContext func getAccount(byAddress address: String) async throws -> CoreDataAccount - + /// Search for fetched account, if not found try to create or asks server for account. /// /// - Returns: Account, if found, created in main viewContext func getAccount(byAddress address: String, publicKey: String) async throws -> CoreDataAccount - - /* That one bugged. Will be fixed later. Maybe. */ - /// Search for fetched account, if not found, asks server for account. - /// - /// - Returns: Account, if found, created in main viewContext -// func getAccount(byPublicKey publicKey: String, completion: @escaping (AccountsProviderResult) -> Void) - + /// Check locally if has account with specified address func hasAccount(address: String) async -> Bool - + /// Request Dummy account, if account wasn't found or initiated func getDummyAccount(for address: String) async throws -> DummyAccount } @@ -81,109 +79,135 @@ struct SystemMessage { } extension AdamantContacts { - static func messagesFor(address: String) -> [String:SystemMessage]? { + static func messagesFor(address: String) -> [String: SystemMessage]? { switch address { case AdamantContacts.adamantBountyWallet.address, - AdamantContacts.adamantBountyWallet.name, - AdamantContacts.adamantNewBountyWallet.address, - AdamantContacts.adamantNewBountyWallet.name: + AdamantContacts.adamantBountyWallet.name, + AdamantContacts.adamantNewBountyWallet.address, + AdamantContacts.adamantNewBountyWallet.name: return AdamantContacts.adamantBountyWallet.messages - + case AdamantContacts.adamantIco.address, AdamantContacts.adamantIco.name: return AdamantContacts.adamantIco.messages - + case AdamantContacts.adamantExchange.address, AdamantContacts.adamantExchange.name: return AdamantContacts.adamantExchange.messages - + case AdamantContacts.betOnBitcoin.address, AdamantContacts.betOnBitcoin.name: return AdamantContacts.betOnBitcoin.messages - + case AdamantContacts.adelina.address, AdamantContacts.adelina.name: return AdamantContacts.adelina.messages - + default: return nil } } - + var welcomeMessage: SystemMessage? { - if - let regionCode = Locale.current.regionCode, + if let regionCode = Locale.current.regionCode, prohibitedRegions.contains(regionCode) - { return nil } + { + return nil + } return messages["chats.welcome_message"] } - + var messages: [String: SystemMessage] { switch self { case .adamantBountyWallet, .adamantNewBountyWallet: - return ["chats.welcome_message": SystemMessage(message: AdamantMessage.markdownText(.localized("Chats.WelcomeMessage", comment: "Known contacts: Adamant welcome message. Markdown supported.")), - silentNotification: true)] + return [ + "chats.welcome_message": SystemMessage( + message: AdamantMessage.markdownText( + .localized("Chats.WelcomeMessage", comment: "Known contacts: Adamant welcome message. Markdown supported.") + ), + silentNotification: true + ) + ] case .adamantWelcomeWallet: - return ["chats.welcome_message": SystemMessage(message: AdamantMessage.markdownText(.localized("Chats.WelcomeMessage", comment: "Known contacts: Adamant welcome message. Markdown supported.")), - silentNotification: true)] + return [ + "chats.welcome_message": SystemMessage( + message: AdamantMessage.markdownText( + .localized("Chats.WelcomeMessage", comment: "Known contacts: Adamant welcome message. Markdown supported.") + ), + silentNotification: true + ) + ] case .adamantIco: return [ - "chats.preico_message": SystemMessage(message: AdamantMessage.text(.localized("Chats.PreIcoMessage", comment: "Known contacts: Adamant pre ICO message")), - silentNotification: true), - "chats.ico_message": SystemMessage(message: AdamantMessage.markdownText(.localized("Chats.IcoMessage", comment: "Known contacts: Adamant ICO message. Markdown supported.")), - silentNotification: true) + "chats.preico_message": SystemMessage( + message: AdamantMessage.text(.localized("Chats.PreIcoMessage", comment: "Known contacts: Adamant pre ICO message")), + silentNotification: true + ), + "chats.ico_message": SystemMessage( + message: AdamantMessage.markdownText(.localized("Chats.IcoMessage", comment: "Known contacts: Adamant ICO message. Markdown supported.")), + silentNotification: true + ) ] - + case .adamantSupport, .pwaBountyBot: return [:] - + case .donate: - return ["chats.welcome_message": SystemMessage( - message: AdamantMessage.markdownText( - .localized( - "Chats.Donate.WelcomeMessage", - comment: "Known contacts: Adamant donate welcome message. Markdown supported." - ) - ), - silentNotification: true - )] - + return [ + "chats.welcome_message": SystemMessage( + message: AdamantMessage.markdownText( + .localized( + "Chats.Donate.WelcomeMessage", + comment: "Known contacts: Adamant donate welcome message. Markdown supported." + ) + ), + silentNotification: true + ) + ] + case .adamantExchange: - return ["chats.welcome_message": SystemMessage( - message: AdamantMessage.markdownText( - .localized( - "Chats.Exchange.WelcomeMessage", - comment: "Known contacts: Adamant welcome message. Markdown supported." - ) - ), - silentNotification: true - )] - + return [ + "chats.welcome_message": SystemMessage( + message: AdamantMessage.markdownText( + .localized( + "Chats.Exchange.WelcomeMessage", + comment: "Known contacts: Adamant welcome message. Markdown supported." + ) + ), + silentNotification: true + ) + ] + case .betOnBitcoin: - return ["chats.welcome_message": SystemMessage( - message: AdamantMessage.markdownText( - .localized( - "Chats.BetOnBitcoin.WelcomeMessage", - comment: "Known contacts: Adamant welcome message. Markdown supported." - ) - ), - silentNotification: true - )] + return [ + "chats.welcome_message": SystemMessage( + message: AdamantMessage.markdownText( + .localized( + "Chats.BetOnBitcoin.WelcomeMessage", + comment: "Known contacts: Adamant welcome message. Markdown supported." + ) + ), + silentNotification: true + ) + ] case .adelina: - return ["chats.welcome_message": SystemMessage( - message: AdamantMessage.markdownText( - .localized( - "Chats.Adelina.WelcomeMessage", - comment: "Known contacts: Adamant welcome message. Markdown supported." - ) - ), - silentNotification: true - )] + return [ + "chats.welcome_message": SystemMessage( + message: AdamantMessage.markdownText( + .localized( + "Chats.Adelina.WelcomeMessage", + comment: "Known contacts: Adamant welcome message. Markdown supported." + ) + ), + silentNotification: true + ) + ] } } - + var prohibitedRegions: [String] { switch self { case .adelina: return ["CN"] - case .adamantBountyWallet, .adamantNewBountyWallet, .adamantExchange, .betOnBitcoin, .donate, .pwaBountyBot, .adamantIco, .adamantWelcomeWallet, .adamantSupport: + case .adamantBountyWallet, .adamantNewBountyWallet, .adamantExchange, .betOnBitcoin, .donate, .pwaBountyBot, .adamantIco, .adamantWelcomeWallet, + .adamantSupport: return [] } } diff --git a/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift b/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift index 62ca6ce81..2c86090fb 100644 --- a/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift +++ b/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift @@ -6,9 +6,9 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation -import CoreData import CommonKit +import CoreData +import Foundation // MARK: - Callbacks @@ -33,7 +33,7 @@ enum ChatsProviderError: Error { case messageNotValid(ValidateMessageResult) case notEnoughMoneyToSend case networkError - case serverError(Error) + case serverError(ServerError) case accountNotFound(String) case accountNotInitiated(String) case dependencyError(String) @@ -43,75 +43,115 @@ enum ChatsProviderError: Error { case invalidTransactionStatus } +extension ChatsProviderError { + enum ServerError { + case timestampIsInTheFuture + case unknown(Error) + + var wrappedError: Error? { + switch self { + case let .unknown(error): error + default: nil + } + } + + var localizedDescription: String { + switch self { + case .timestampIsInTheFuture: + .adamant.alert.timeAheadError + case let .unknown(error): + error.localizedDescription + } + } + + init(from error: Error) { + switch error.localizedDescription { + case error.localizedDescription + where error.localizedDescription.contains( + "Timestamp is in the future" + ): self = .timestampIsInTheFuture + + default: self = .unknown(error) + } + } + } +} + extension ChatsProviderError: RichError { var message: String { switch self { case .invalidTransactionStatus: return String.adamant.chat.cancelError - + case .notLogged: return String.adamant.sharedErrors.userNotLogged - + case .messageNotValid(let result): return result.localized - + case .notEnoughMoneyToSend: return .localized("ChatsProvider.Error.notEnoughMoney", comment: "ChatsProvider: Notify user that he doesn't have money to pay a message fee") - + case .networkError: return String.adamant.sharedErrors.networkError - + case .serverError(let error): return ApiServiceError.serverError(error: error.localizedDescription) .localizedDescription - + case .accountNotFound(let address): return AccountsProviderError.notFound(address: address).localized - + case .dependencyError(let message): return String.adamant.sharedErrors.internalError(message: message) - + case .transactionNotFound(let id): - return String.localizedStringWithFormat(.localized("ChatsProvider.Error.TransactionNotFoundFormat", comment: "ChatsProvider: Transaction not found error. %@ for transaction's ID"), id) - + return String.localizedStringWithFormat( + .localized("ChatsProvider.Error.TransactionNotFoundFormat", comment: "ChatsProvider: Transaction not found error. %@ for transaction's ID"), + id + ) + case .internalError(let error): return String.adamant.sharedErrors.internalError(message: error.localizedDescription) - + case .accountNotInitiated: return String.adamant.sharedErrors.accountNotInitiated - + case .requestCancelled: return String.adamant.sharedErrors.requestCancelled } } - + var internalError: Error? { switch self { - case .internalError(let error), .serverError(let error): + case .internalError(let error): return error - + + case .serverError(let error): + return error.wrappedError + default: return nil } } - + var level: ErrorLevel { switch self { case .accountNotFound, - .messageNotValid, - .networkError, - .notEnoughMoneyToSend, - .accountNotInitiated, - .requestCancelled, - .invalidTransactionStatus, - .notLogged: + .messageNotValid, + .networkError, + .notEnoughMoneyToSend, + .accountNotInitiated, + .requestCancelled, + .invalidTransactionStatus, + .notLogged: return .warning - + case .serverError, .transactionNotFound: return .error - + case .dependencyError, - .internalError: + .internalError: return .internalError } } @@ -121,14 +161,14 @@ enum ValidateMessageResult { case isValid case empty case tooLong - + var localized: String { switch self { case .isValid: return .localized("ChatsProvider.Validation.Passed", comment: "ChatsProvider: Validation passed, message is valid") - + case .empty: return .localized("ChatsProvider.Validation.MessageIsEmpty", comment: "ChatsProvider: Validation error: Message is empty") - + case .tooLong: return .localized("ChatsProvider.Validation.MessageTooLong", comment: "ChatsProvider: Validation error: Message is too long") } @@ -144,7 +184,7 @@ extension Notification.Name { static let initiallySyncedChanged = Notification.Name("adamant.chatsProvider.initialSyncChanged") static let initiallyLoadedMessages = Notification.Name("adamant.chatsProvider.initiallyLoadedMessages") - + private init() {} } } @@ -156,14 +196,14 @@ extension AdamantUserInfoKey { static let newChatroomAddress = "adamant.chatsProvider.newChatroom.address" /// lastMessageHeight: new lastMessageHeight static let lastMessageHeight = "adamant.chatsProvider.newMessage.lastHeight" - + static let initiallySynced = "adamant.chatsProvider.initiallySynced" - + private init() {} } } -// MARK: - SecuredStore keys +// MARK: - SecureStore keys extension StoreKey { struct chatProvider { static let address = "chatProvider.address" @@ -171,27 +211,28 @@ extension StoreKey { static let readedLastHeight = "chatProvider.readedLastHeight" static let notifiedLastHeight = "chatProvider.notifiedLastHeight" static let notifiedMessagesCount = "chatProvider.notifiedCount" + static let markedChatsAsUnread = "adamant.chatsProvider.markedChatsAsUnread" } } -// MARK: - Protocol +// sourcery: AutoMockable protocol ChatsProvider: DataProvider, Actor { // MARK: - Properties var receivedLastHeight: Int64? { get } var readedLastHeight: Int64? { get } var isInitiallySynced: Bool { get } var blockList: [String] { get } - + var roomsMaxCount: Int? { get } var roomsLoadedCount: Int? { get } - + var chatLoadingStatusPublisher: AnyObservable<[String: ChatRoomLoadingStatus]> { get } - + var chatMaxMessages: [String: Int] { get } var chatLoadedMessages: [String: Int] { get } - + // MARK: - Getting chats and messages func getChatroom(for adm: String) -> Chatroom? func getChatroomsController() -> NSFetchedResultsController @@ -200,63 +241,67 @@ protocol ChatsProvider: DataProvider, Actor { func getChatMessages(with addressRecipient: String, offset: Int?) async func isChatLoading(with addressRecipient: String) -> Bool func isChatLoaded(with addressRecipient: String) -> Bool - + func loadTransactionsUntilFound( _ transactionId: String, recipient: String - ) async throws - + ) async throws + /// Unread messages controller. Sections by chatroom. func getUnreadMessagesController() -> NSFetchedResultsController - + // ForceUpdate chats func update(notifyState: Bool) async -> ChatsProviderResult? - + func setManualMarkChatAsUnread(chatroomId: String) + func removeManualMarkChatAsUnread(chatroomId: String) + func getMarkAdressesFromChain() -> Set + // MARK: - Sending messages func sendMessage(_ message: AdamantMessage, recipientId: String) async throws -> ChatTransaction func sendMessage(_ message: AdamantMessage, recipientId: String, from chatroom: Chatroom?) async throws -> ChatTransaction func retrySendMessage(_ message: ChatTransaction) async throws - + func sendFileMessageLocally( _ message: AdamantMessage, recipientId: String, from chatroom: Chatroom? ) async throws -> String - + func sendFileMessage( _ message: AdamantMessage, recipientId: String, transactionLocalyId: String, from chatroom: Chatroom? ) async throws -> ChatTransaction - + func updateTxMessageContent( txId: String, richMessage: RichMessage ) throws - + func setTxMessageStatus( txId: String, status: MessageStatus ) throws - + // MARK: - Delete local message func cancelMessage(_ message: ChatTransaction) async throws func isMessageDeleted(id: String) -> Bool - + // MARK: - Tools func validateMessage(_ message: AdamantMessage) -> ValidateMessageResult func blockChat(with address: String) func removeMessage(with id: String) func markChatAsRead(chatroom: Chatroom) - + func markMessageAsRead(chatroom: Chatroom, message: String) + @MainActor func removeChatPositon(for address: String) @MainActor func setChatPositon(for address: String, position: Double?) @MainActor func getChatPositon(for address: String) -> Double? - + // MARK: - Unconfirmed Transaction func addUnconfirmed(transactionId: UInt64, managedObjectId: NSManagedObjectID) - + func fakeReceived( message: AdamantMessage, senderId: String, @@ -265,7 +310,7 @@ protocol ChatsProvider: DataProvider, Actor { silent: Bool, showsChatroom: Bool ) async throws -> ChatTransaction - + // MARK: - Search func getMessages(containing text: String, in chatroom: Chatroom?) -> [MessageTransaction]? func isTransactionUnique(_ transaction: RichMessageTransaction) -> Bool diff --git a/Adamant/ServiceProtocols/DataProviders/CoreDataStack.swift b/Adamant/ServiceProtocols/DataProviders/CoreDataStack.swift index 86ffd7e65..4d9c9a247 100644 --- a/Adamant/ServiceProtocols/DataProviders/CoreDataStack.swift +++ b/Adamant/ServiceProtocols/DataProviders/CoreDataStack.swift @@ -6,9 +6,10 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation import CoreData +import Foundation protocol CoreDataStack: Sendable { var container: NSPersistentContainer { get } + func clearCoreData() } diff --git a/Adamant/ServiceProtocols/DataProviders/DataProvider.swift b/Adamant/ServiceProtocols/DataProviders/DataProvider.swift index 395453c24..69a78919c 100644 --- a/Adamant/ServiceProtocols/DataProviders/DataProvider.swift +++ b/Adamant/ServiceProtocols/DataProviders/DataProvider.swift @@ -6,37 +6,44 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation -enum State { +enum DataProviderState { case empty case updating case upToDate case failedToUpdate(Error) + + var isUpdating: Bool { + switch self { + case .updating: true + case .failedToUpdate, .upToDate, .empty: false + } + } } protocol DataProvider: AnyObject, Actor { - var state: State { get } - var stateObserver: AnyObservable { get } + var state: DataProviderState { get } + var stateObserver: AnyObservable { get } var isInitiallySynced: Bool { get } - + func reload() async func reset() } // MARK: - Status Equatable -extension State: Equatable { - +extension DataProviderState: Equatable { + /// Simple equatable function. Does not checks associated values. - static func ==(lhs: State, rhs: State) -> Bool { + static func == (lhs: DataProviderState, rhs: DataProviderState) -> Bool { switch (lhs, rhs) { case (.empty, .empty): return true case (.updating, .updating): return true case (.upToDate, .upToDate): return true - + case (.failedToUpdate, .failedToUpdate): return true - + default: return false } } diff --git a/Adamant/ServiceProtocols/DataProviders/TransfersProvider.swift b/Adamant/ServiceProtocols/DataProviders/TransfersProvider.swift index 70f4a5c70..733e381c2 100644 --- a/Adamant/ServiceProtocols/DataProviders/TransfersProvider.swift +++ b/Adamant/ServiceProtocols/DataProviders/TransfersProvider.swift @@ -6,9 +6,9 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation -import CoreData import CommonKit +import CoreData +import Foundation enum TransfersProviderError: Error { case notLogged @@ -37,69 +37,75 @@ extension TransfersProviderError: RichError { switch self { case .notLogged: return String.adamant.sharedErrors.userNotLogged - + case .serverError(let error): return ApiServiceError.serverError(error: error.localizedDescription) .localizedDescription - + case .accountNotFound(let address): return AccountsProviderError.notFound(address: address).localized - + case .internalError(let message, _): return String.adamant.sharedErrors.internalError(message: message) - + case .transactionNotFound(let id): - return String.localizedStringWithFormat(.localized("TransfersProvider.Error.TransactionNotFoundFormat", comment: "TransfersProvider: Transaction not found error. %@ for transaction's ID"), id) - + return String.localizedStringWithFormat( + .localized( + "TransfersProvider.Error.TransactionNotFoundFormat", + comment: "TransfersProvider: Transaction not found error. %@ for transaction's ID" + ), + id + ) + case .dependencyError(let message): return String.adamant.sharedErrors.internalError(message: message) - + case .networkError: return String.adamant.sharedErrors.networkError - + case .notEnoughMoney: return String.adamant.sharedErrors.notEnoughMoney - + case .requestCancelled: return String.adamant.sharedErrors.requestCancelled } } - + var internalError: Error? { switch self { case .serverError(let error): return error - + default: return nil } } - + var level: ErrorLevel { switch self { case .notLogged, .accountNotFound, .transactionNotFound, .networkError, .notEnoughMoney, .requestCancelled: return .warning - + case .serverError: return .error - + case .internalError, .dependencyError: return .internalError } } - + } extension Notification.Name { struct AdamantTransfersProvider { /// userInfo contains 'newTransactions' element. See AdamantUserInfoKey.TransfersProvider static let newTransactions = Notification.Name("adamant.transfersProvider.newTransactions") - + /// userInfo contains newState element. See AdamantUserInfoKey.TransfersProvider static let stateChanged = Notification.Name("adamant.transfersProvider.stateChanged") - + static let initialSyncFinished = Notification.Name("adamant.transfersProvider.initialSyncFinished") - + private init() {} } } @@ -108,16 +114,16 @@ extension AdamantUserInfoKey { struct TransfersProvider { /// New provider state static let newState = "transfersNewState" - + /// Previous provider state, if avaible static let prevState = "transfersPrevState" - + // New received transactions static let newTransactions = "transfersNewTransactions" - + /// lastMessageHeight: new lastMessageHeight static let lastTransactionHeight = "adamant.transfersProvider.newTransactions.lastHeight" - + private init() {} } } @@ -135,23 +141,23 @@ extension StoreKey { protocol TransfersProvider: DataProvider, Actor { // MARK: - Constants static var transferFee: Decimal { get } - + // MARK: - Properties var receivedLastHeight: Int64? { get } var readedLastHeight: Int64? { get } var isInitiallySynced: Bool { get } var hasTransactions: Bool { get } var offsetTransactions: Int { get set } - + // MARK: Controller func transfersController() -> NSFetchedResultsController func unreadTransfersController() -> NSFetchedResultsController - + func transfersController(for account: CoreDataAccount) -> NSFetchedResultsController // Force update transactions func update() async -> TransfersProviderResult? - + // MARK: - Sending funds func transferFunds( toAddress recipient: String, @@ -159,11 +165,11 @@ protocol TransfersProvider: DataProvider, Actor { comment: String?, replyToMessageId: String? ) async throws -> AdamantTransactionDetails - + // MARK: - Transactions func getTransfer(id: String) -> TransferTransaction? func refreshTransfer(id: String) async throws - + /// Load moore transactions func getTransactions( forAccount account: String, @@ -172,6 +178,6 @@ protocol TransfersProvider: DataProvider, Actor { limit: Int, orderByTime: Bool ) async throws -> Int - + func updateOffsetTransactions(_ value: Int) } diff --git a/Adamant/ServiceProtocols/DialogService.swift b/Adamant/ServiceProtocols/DialogService.swift index 85676546b..b08659fc0 100644 --- a/Adamant/ServiceProtocols/DialogService.swift +++ b/Adamant/ServiceProtocols/DialogService.swift @@ -6,8 +6,8 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit import CommonKit +import UIKit extension String.adamant.alert { static var copyToPasteboard: String { @@ -17,19 +17,22 @@ extension String.adamant.alert { String.localized("Shared.Share", comment: "Shared alert 'Share' button. Used anywhere for presenting standart iOS 'Share' menu.") } static var generateQr: String { - String.localized("Shared.GenerateQRCode", comment: "Shared alert 'Generate QR' button. Used to generate QR codes with addresses and passphrases. Used with sharing and saving, anywhere.") + String.localized( + "Shared.GenerateQRCode", + comment: "Shared alert 'Generate QR' button. Used to generate QR codes with addresses and passphrases. Used with sharing and saving, anywhere." + ) } static var saveToPhotolibrary: String { String.localized("Shared.SaveToPhotolibrary", comment: "Shared alert 'Save to Photos'. Used with saving images to photolibrary") } - + static var renameContact: String { String.localized("Shared.RenameContact", comment: "Partner screen 'Rename contact'") } - + static var renameContactInitial: String { String.localized("Shared.RenameContactInitial", comment: "Partner screen 'Give contact a name' at first") - } + } static var sendTokens: String { String.localized("Shared.SendTokens", comment: "Shared alert 'Send tokens'") @@ -43,21 +46,25 @@ extension String.adamant.alert { static var openInExplorer: String { String.localized("TransactionDetailsScene.Row.Explorer", comment: "Transaction details: 'Open transaction in explorer' row.") } + static var timeAheadError: String { + String.localized("Chat.Timestamp.InFuture.Error", comment: "Timestamp error. Used for chat when the user's time is in the future") + + } } enum AddressChatShareType { - case chat - case send - - var localized: String { - switch self { - case .chat: - return .localized("Shared.ChatWith", comment: "Shared alert 'Chat With' button. Used to chat with recipient") - case .send: - return .localized("Shared.SendAdmTo", comment: "Shared alert 'Send ADM To' button. Used to send ADM to recipient") - } - } - } + case chat + case send + + var localized: String { + switch self { + case .chat: + return .localized("Shared.ChatWith", comment: "Shared alert 'Chat With' button. Used to chat with recipient") + case .send: + return .localized("Shared.SendAdmTo", comment: "Shared alert 'Send ADM To' button. Used to send ADM to recipient") + } + } +} enum ShareType { case copyToPasteboard @@ -69,30 +76,30 @@ enum ShareType { case sendTokens case uploadMedia case uploadFile - + var localized: String { switch self { case .copyToPasteboard: return String.adamant.alert.copyToPasteboard - + case .share: return String.adamant.alert.share - + case .generateQr, .partnerQR: return String.adamant.alert.generateQr - + case .openInExplorer: return String.adamant.alert.openInExplorer - + case .saveToPhotolibrary: return String.adamant.alert.saveToPhotolibrary - + case .sendTokens: return String.adamant.alert.sendTokens - + case .uploadMedia: return String.adamant.alert.uploadMedia - + case .uploadFile: return String.adamant.alert.uploadFile } @@ -102,31 +109,35 @@ enum ShareType { enum ShareContentType { case passphrase case address - + var excludedActivityTypes: [UIActivity.ActivityType]? { switch self { case .passphrase: - var types: [UIActivity.ActivityType] = [.postToFacebook, - .postToTwitter, - .postToWeibo, - .message, - .mail, - .assignToContact, - .saveToCameraRoll, - .addToReadingList, - .postToFlickr, - .postToVimeo, - .postToTencentWeibo, - .airDrop, - .openInIBooks] - + var types: [UIActivity.ActivityType] = [ + .postToFacebook, + .postToTwitter, + .postToWeibo, + .message, + .mail, + .assignToContact, + .saveToCameraRoll, + .addToReadingList, + .postToFlickr, + .postToVimeo, + .postToTencentWeibo, + .airDrop, + .openInIBooks + ] + types.append(.markupAsPDF) return types - + case .address: - return [.assignToContact, - .addToReadingList, - .openInIBooks] + return [ + .assignToContact, + .addToReadingList, + .openInIBooks + ] } } } @@ -162,17 +173,17 @@ struct AdamantAlertAction { @MainActor protocol DialogService: AnyObject { func setup(window: UIWindow) - + func getTopmostViewController() -> UIViewController? - + /// Present view controller modally func present(_ viewController: UIViewController, animated: Bool, completion: (() -> Void)?) - + // MARK: - Toast messages /// Show pop-up message func showToastMessage(_ message: String) func dismissToast() - + // MARK: - Indicators func showProgress(withMessage: String?, userInteractionEnable: Bool) func dismissProgress() @@ -183,16 +194,47 @@ protocol DialogService: AnyObject { func showRichError(error: Error) func showNoConnectionNotification() func dissmisNoConnectionNotification() - + // MARK: - Notifications func showNotification(title: String?, message: String?, image: UIImage?, tapHandler: (() -> Void)?) func dismissNotification() - + // MARK: - ActivityControllers - func presentShareAlertFor(adm: String, name: String, types: [AddressChatShareType], animated: Bool, from: UIView?, completion: (() -> Void)?, didSelect: ((AddressChatShareType) -> Void)?) - func presentShareAlertFor(string: String, types: [ShareType], excludedActivityTypes: [UIActivity.ActivityType]?, animated: Bool, from: UIView?, completion: (() -> Void)?) - func presentShareAlertFor(stringForPasteboard: String, stringForShare: String, stringForQR: String, types: [ShareType], excludedActivityTypes: [UIActivity.ActivityType]?, animated: Bool, from: UIView?, completion: (() -> Void)?) - func presentShareAlertFor(string: String, types: [ShareType], excludedActivityTypes: [UIActivity.ActivityType]?, animated: Bool, from: UIBarButtonItem?, completion: (() -> Void)?) + func presentShareAlertFor( + adm: String, + name: String, + types: [AddressChatShareType], + animated: Bool, + from: UIView?, + completion: (() -> Void)?, + didSelect: ((AddressChatShareType) -> Void)? + ) + func presentShareAlertFor( + string: String, + types: [ShareType], + excludedActivityTypes: [UIActivity.ActivityType]?, + animated: Bool, + from: UIView?, + completion: (() -> Void)? + ) + func presentShareAlertFor( + stringForPasteboard: String, + stringForShare: String, + stringForQR: String, + types: [ShareType], + excludedActivityTypes: [UIActivity.ActivityType]?, + animated: Bool, + from: UIView?, + completion: (() -> Void)? + ) + func presentShareAlertFor( + string: String, + types: [ShareType], + excludedActivityTypes: [UIActivity.ActivityType]?, + animated: Bool, + from: UIBarButtonItem?, + completion: (() -> Void)? + ) func presentShareAlertFor( string: String, types: [ShareType], @@ -202,23 +244,23 @@ protocol DialogService: AnyObject { completion: (() -> Void)?, didSelect: ((ShareType) -> Void)? ) - + func presentGoToSettingsAlert(title: String?, message: String?) - + func presentDummyAlert( for adm: String, from: UIView?, canSend: Bool, sendCompletion: ((UIAlertAction) -> Void)? ) - + func presentDummyChatAlert( for adm: String, from: UIView?, canSend: Bool, sendCompletion: ((UIAlertAction) -> Void)? ) - + func presentDummyAlert( for adm: String, from: UIView?, @@ -226,10 +268,18 @@ protocol DialogService: AnyObject { message: String, sendCompletion: ((UIAlertAction) -> Void)? ) - + // MARK: - Alerts func showAlert(title: String?, message: String?, style: UIAlertController.Style, actions: [UIAlertAction]?, from: UIAlertController.SourceView?) func showAlert(title: String?, message: String?, style: AdamantAlertStyle, actions: [AdamantAlertAction]?, from: UIAlertController.SourceView?) - + func makeRenameAlert( + titleFormat: String, + initialText: String?, + isEnoughMoney: Bool, + url: String?, + showVC: @escaping () -> Void, + onRename: @escaping (String) -> Void + ) -> UIAlertController func selectAllTextFields(in alert: UIAlertController) + func showFreeTokenAlert(url: String?, type: FreeTokensAlertType, showVC: @escaping () -> Void) } diff --git a/Adamant/ServiceProtocols/FileApiServiceProtocol.swift b/Adamant/ServiceProtocols/FileApiServiceProtocol.swift index aed483dfe..368f26d42 100644 --- a/Adamant/ServiceProtocols/FileApiServiceProtocol.swift +++ b/Adamant/ServiceProtocols/FileApiServiceProtocol.swift @@ -6,15 +6,15 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation protocol FileApiServiceProtocol: ApiServiceProtocol { func uploadFile( data: Data, uploadProgress: @escaping @Sendable (Progress) -> Void ) async -> FileApiServiceResult - + func downloadFile( id: String, downloadProgress: @escaping @Sendable (Progress) -> Void diff --git a/Adamant/ServiceProtocols/FilesNetworkManagerProtocol.swift b/Adamant/ServiceProtocols/FilesNetworkManagerProtocol.swift index 267958565..65cfc1253 100644 --- a/Adamant/ServiceProtocols/FilesNetworkManagerProtocol.swift +++ b/Adamant/ServiceProtocols/FilesNetworkManagerProtocol.swift @@ -14,7 +14,7 @@ protocol FilesNetworkManagerProtocol { type: NetworkFileProtocolType, uploadProgress: @escaping @Sendable (Progress) -> Void ) async -> FileApiServiceResult - + func downloadFile( _ id: String, type: String, diff --git a/Adamant/ServiceProtocols/FilesStorageProprietiesProtocol.swift b/Adamant/ServiceProtocols/FilesStorageProprietiesProtocol.swift index 8a35e0c4c..394803a39 100644 --- a/Adamant/ServiceProtocols/FilesStorageProprietiesProtocol.swift +++ b/Adamant/ServiceProtocols/FilesStorageProprietiesProtocol.swift @@ -6,14 +6,14 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation @MainActor protocol FilesStorageProprietiesProtocol: Sendable { var autoDownloadPreviewPolicyPublisher: AnyObservable { get } var autoDownloadFullMediaPolicyPublisher: AnyObservable { get } - + func autoDownloadPreviewPolicy() -> DownloadPolicy func setAutoDownloadPreview(_ value: DownloadPolicy) func autoDownloadFullMediaPolicy() -> DownloadPolicy diff --git a/Adamant/ServiceProtocols/IncreaseFeeService.swift b/Adamant/ServiceProtocols/IncreaseFeeService.swift index eb7bd9357..5f1788654 100644 --- a/Adamant/ServiceProtocols/IncreaseFeeService.swift +++ b/Adamant/ServiceProtocols/IncreaseFeeService.swift @@ -8,7 +8,8 @@ import Foundation +// sourcery: AutoMockable protocol IncreaseFeeService: AnyObject, Sendable { - func isIncreaseFeeEnabled(for tokenUnicID: String) -> Bool - func setIncreaseFeeEnabled(for tokenUnicID: String, value: Bool) + func isIncreaseFeeEnabled(for tokenUniqueID: String) -> Bool + func setIncreaseFeeEnabled(for tokenUniqueID: String, value: Bool) } diff --git a/Adamant/ServiceProtocols/LanguageStorageProtocol.swift b/Adamant/ServiceProtocols/LanguageStorageProtocol.swift index 629566766..7db147866 100644 --- a/Adamant/ServiceProtocols/LanguageStorageProtocol.swift +++ b/Adamant/ServiceProtocols/LanguageStorageProtocol.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation protocol LanguageStorageProtocol { func getLanguage() -> Language diff --git a/Adamant/ServiceProtocols/LocalAuthentication.swift b/Adamant/ServiceProtocols/LocalAuthentication.swift index 11f32ea2a..f92ba9a1a 100644 --- a/Adamant/ServiceProtocols/LocalAuthentication.swift +++ b/Adamant/ServiceProtocols/LocalAuthentication.swift @@ -10,7 +10,7 @@ import Foundation enum BiometryType { case none, touchID, faceID - + var localized: String { switch self { case .none: return "None" @@ -22,6 +22,7 @@ enum BiometryType { enum AuthenticationResult { case success + case biometryLockout case cancel case fallback case failed @@ -29,6 +30,6 @@ enum AuthenticationResult { protocol LocalAuthentication: AnyObject { var biometryType: BiometryType { get } - - func authorizeUser(reason: String, completion: @escaping (AuthenticationResult) -> Void) + + func authorizeUser(reason: String) async -> AuthenticationResult } diff --git a/Adamant/ServiceProtocols/LskApiServiceProtocol.swift b/Adamant/ServiceProtocols/LskApiServiceProtocol.swift deleted file mode 100644 index 3a94f7d33..000000000 --- a/Adamant/ServiceProtocols/LskApiServiceProtocol.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// LskApiServerProtocol.swift -// Adamant -// -// Created by Anton Boyarkin on 12/07/2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import Foundation -import Lisk - -// MARK: - Notifications -extension Notification.Name { - struct LskApiService { - /// Raised when user has logged out. - static let userLoggedOut = Notification.Name("adamant.lskApiService.userHasLoggedOut") - - /// Raised when user has successfully logged in. - static let userLoggedIn = Notification.Name("adamant.lskApiService.userHasLoggedIn") - - private init() {} - } -} - -protocol LskApiService: class { - - var account: LskAccount? { get } - - // MARK: - Accounts - func newAccount(byPassphrase passphrase: String, completion: @escaping (ApiServiceResult) -> Void) - - // MARK: - Transactions - func createTransaction(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult) -> Void) - func sendTransaction(transaction: LocalTransaction, completion: @escaping (ApiServiceResult) -> Void) - - func sendFunds(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult) -> Void) - func getTransactions(_ completion: @escaping (ApiServiceResult<[Transactions.TransactionModel]>) -> Void) - func getTransaction(byHash hash: String, completion: @escaping (ApiServiceResult) -> Void) - - // MARK: - Tools - func getBalance(_ completion: @escaping (ApiServiceResult) -> Void) - func getLskAddress(byAdamandAddress address: String, completion: @escaping (ApiServiceResult) -> Void) -} diff --git a/Adamant/ServiceProtocols/NotificationsService.swift b/Adamant/ServiceProtocols/NotificationsService.swift index a9259c57f..fabb41978 100644 --- a/Adamant/ServiceProtocols/NotificationsService.swift +++ b/Adamant/ServiceProtocols/NotificationsService.swift @@ -6,22 +6,22 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation enum NotificationsMode: Int { case disabled case backgroundFetch case push - + var localized: String { switch self { case .disabled: return .localized("Notifications.Mode.NotificationsDisabled", comment: "Notifications: Disable notifications") - + case .backgroundFetch: return .localized("Notifications.Mode.BackgroundFetch", comment: "Notifications: Use Background fetch notifications") - + case .push: return .localized("Notifications.Mode.ApplePush", comment: "Notifications: Use Apple Push notifications") } @@ -48,7 +48,7 @@ enum NotificationSound: String { case rebound case slide case welcome - + var tag: String { switch self { case .none: return "none" @@ -72,7 +72,7 @@ enum NotificationSound: String { case .welcome: return "welcome" } } - + var fileName: String { switch self { case .none: return "" @@ -96,7 +96,7 @@ enum NotificationSound: String { case .welcome: return "welcome.mp3" } } - + var localized: String { switch self { case .none: return "None" @@ -120,7 +120,7 @@ enum NotificationSound: String { case .welcome: return "Welcome" } } - + init?(fileName: String) { switch fileName { case "notification.mp3": self = .inputDefault @@ -156,28 +156,28 @@ enum AdamantNotificationType { case newMessages(count: Int) case newTransactions(count: Int) case custom(identifier: String, badge: Int?) - + var identifier: String { switch self { case .newMessages: return "newMessages" - + case .newTransactions: return "newTransactions" - + case .custom(let identifier, _): return identifier } } - + var badge: Int? { switch self { case .newMessages(let count): return count - + case .newTransactions(let count): return count - + case .custom(_, let badge): return badge } @@ -196,7 +196,7 @@ extension Notification.Name { extension AdamantUserInfoKey { struct NotificationsService { static let newNotificationsMode = "adamant.notificationsService.notificationsMode" - + private init() {} } } @@ -221,14 +221,14 @@ extension NotificationsServiceError: RichError { case .notStayedLoggedIn: return NotificationStrings.notStayedLoggedIn } } - + var internalError: Error? { switch self { case .notEnoughMoney, .notStayedLoggedIn: return nil case .denied(let error): return error } } - + var level: ErrorLevel { switch self { case .notEnoughMoney, .notStayedLoggedIn: return .warning @@ -245,24 +245,24 @@ protocol NotificationsService: AnyObject, Sendable { var inAppSound: Bool { get } var inAppVibrate: Bool { get } var inAppToasts: Bool { get } - + func setInAppSound(_ value: Bool) func setInAppVibrate(_ value: Bool) func setInAppToasts(_ value: Bool) - + func setNotificationSound( _ sound: NotificationSound, for target: NotificationTarget ) func setNotificationsMode(_ mode: NotificationsMode, completion: ((NotificationsServiceResult) -> Void)?) - + func showNotification(title: String, body: String, type: AdamantNotificationType) - + func setBadge(number: Int?) - + func removeAllPendingNotificationRequests() func removeAllDeliveredNotifications() - + // MARK: Background batch notifications func startBackgroundBatchNotifications() func stopBackgroundBatchNotifications() diff --git a/Adamant/ServiceProtocols/PushNotificationsTokenService.swift b/Adamant/ServiceProtocols/PushNotificationsTokenService.swift index 19bbe657e..1d013cfe6 100644 --- a/Adamant/ServiceProtocols/PushNotificationsTokenService.swift +++ b/Adamant/ServiceProtocols/PushNotificationsTokenService.swift @@ -6,8 +6,8 @@ // Copyright © 2022 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation extension StoreKey { enum PushNotificationsTokenService { diff --git a/Adamant/ServiceProtocols/ReachabilityMonitor.swift b/Adamant/ServiceProtocols/ReachabilityMonitor.swift index 274209f50..40884dcc0 100644 --- a/Adamant/ServiceProtocols/ReachabilityMonitor.swift +++ b/Adamant/ServiceProtocols/ReachabilityMonitor.swift @@ -6,13 +6,13 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation extension Notification.Name { struct AdamantReachabilityMonitor { static let reachabilityChanged = Notification.Name("adamant.reachabilityMonitor.reachabilityChanged") - + private init() {} } } @@ -21,7 +21,7 @@ extension AdamantUserInfoKey { struct ReachabilityMonitor { /// Contains Connection object static let connection = "adamant.reachability.connection" - + private init() {} } } @@ -29,7 +29,7 @@ extension AdamantUserInfoKey { protocol ReachabilityMonitor: Sendable { var connectionPublisher: AnyObservable { get } var connection: Bool { get } - + func start() func stop() } diff --git a/Adamant/ServiceProtocols/RichMessageProviderWithStatusCheck/RichMessageProviderWithStatusCheck.swift b/Adamant/ServiceProtocols/RichMessageProviderWithStatusCheck/RichMessageProviderWithStatusCheck.swift index 29536ae35..d80b0986c 100644 --- a/Adamant/ServiceProtocols/RichMessageProviderWithStatusCheck/RichMessageProviderWithStatusCheck.swift +++ b/Adamant/ServiceProtocols/RichMessageProviderWithStatusCheck/RichMessageProviderWithStatusCheck.swift @@ -6,8 +6,8 @@ // Copyright © 2023 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation struct TransactionStatusInfo { let sentDate: Date? diff --git a/Adamant/ServiceProtocols/SecretWalletsManagerProtocol.swift b/Adamant/ServiceProtocols/SecretWalletsManagerProtocol.swift new file mode 100644 index 000000000..a2f395400 --- /dev/null +++ b/Adamant/ServiceProtocols/SecretWalletsManagerProtocol.swift @@ -0,0 +1,26 @@ +// +// SecretWalletsManagerProtocol.swift +// Adamant +// +// Created by Dmitrij Meidus on 19.02.25. +// Copyright © 2025 Adamant. All rights reserved. +// + +import CommonKit + +protocol SecretWalletsManagerProtocol { + var statePublisher: AnyObservable { get } + + func createSecretWallet(withPassword password: String) + func removeSecretWallet(at index: Int) -> WalletStoreServiceProtocol? + func getCurrentWallet() -> WalletStoreServiceProtocol + func getSecretWallets() -> [WalletStoreServiceProtocol] + func activateSecretWallet(at index: Int) + func activateDefaultWallet() +} + +protocol SecretWalletsManagerStateProtocol { + var currentWallet: WalletStoreServiceProtocol { get set } + var defaultWallet: WalletStoreServiceProtocol { get } + var secretWallets: [WalletStoreServiceProtocol] { get set } +} diff --git a/Adamant/ServiceProtocols/SocketService.swift b/Adamant/ServiceProtocols/SocketService.swift index 7d6f10198..c1adc1fe4 100644 --- a/Adamant/ServiceProtocols/SocketService.swift +++ b/Adamant/ServiceProtocols/SocketService.swift @@ -6,8 +6,8 @@ // Copyright © 2022 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation // MARK: - Notifications @@ -21,7 +21,7 @@ extension Notification.Name { protocol SocketService: AnyObject, Sendable { var currentNode: Node? { get } - + func connect(address: String, handler: @escaping @Sendable (ApiServiceResult) -> Void) func disconnect() } diff --git a/Adamant/ServiceProtocols/TransactionDetailsProtocol.swift b/Adamant/ServiceProtocols/TransactionDetailsProtocol.swift deleted file mode 100644 index 45dba6998..000000000 --- a/Adamant/ServiceProtocols/TransactionDetailsProtocol.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// TransactionDetailsProtocol.swift -// Adamant -// -// Created by Anton Boyarkin on 26/06/2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import Foundation -import web3swift -import BigInt - -/// A standard protocol representing a Transaction details. -protocol TransactionDetails { - /// The identifier of the transaction. - var id: String { get } - - /// The sender of the transaction. - var senderAddress: String { get } - - /// The reciver of the transaction. - var recipientAddress: String { get } - - /// The date the transaction was sent. - var sentDate: Date { get } - - /// The amount of currency that was sent. - var amountValue: Decimal { get } - - /// The amount of fee that taken for transaction process. - var feeValue: Decimal { get } - - /// The confirmations of the transaction. - var confirmationsValue: String { get } - - /// The block of the transaction. - var block: String { get } - - /// The show go to button. - var chatroom: Chatroom? { get } - - /// The currency of the transaction. - var currencyCode: String { get } -} - -extension TransactionDetails { - func isOutgoing(_ address: String) -> Bool { - return senderAddress.lowercased() == address.lowercased() ? true : false - } - -// func getSummary() -> String { -// return """ -// Transaction #\(id) -// -// Summary -// Sender: \(senderAddress) -// Recipient: \(recipientAddress) -// Date: \(DateFormatter.localizedString(from: sentDate, dateStyle: .short, timeStyle: .medium)) -// Amount: \(formattedAmount()) -// Fee: \(formattedFee()) -// Confirmations: \(String(confirmationsValue)) -// Block: \(block) -// URL: \(explorerUrl?.absoluteString ?? "") -// """ -// } -} diff --git a/Adamant/ServiceProtocols/VibroService.swift b/Adamant/ServiceProtocols/VibroService.swift index 79913ab12..5d2e3db1f 100644 --- a/Adamant/ServiceProtocols/VibroService.swift +++ b/Adamant/ServiceProtocols/VibroService.swift @@ -12,7 +12,7 @@ import Foundation extension Notification.Name { struct AdamantVibroService { static let presentVibrationRow = Notification.Name("adamant.vibroService.presentVibrationRow") - + } } diff --git a/Adamant/ServiceProtocols/VisibleWalletsService.swift b/Adamant/ServiceProtocols/VisibleWalletsService.swift index bb53182f4..789cd017f 100644 --- a/Adamant/ServiceProtocols/VisibleWalletsService.swift +++ b/Adamant/ServiceProtocols/VisibleWalletsService.swift @@ -6,28 +6,18 @@ // Copyright © 2022 Adamant. All rights reserved. // +import CommonKit import Foundation -// MARK: - Notifications -extension Notification.Name { - struct AdamantVisibleWalletsService { - /// Raised when user has changed visible wallets - static let visibleWallets = Notification.Name("adamant.visibleWallets.update") - - } -} protocol VisibleWalletsService: AnyObject, Sendable { - func addToInvisibleWallets(_ wallet: WalletCoreProtocol) - func removeFromInvisibleWallets(_ wallet: WalletCoreProtocol) - func getInvisibleWallets() -> [String] - func isInvisible(_ wallet: WalletCoreProtocol) -> Bool - + var statePublisher: AnyObservable { get } + + func addToInvisibleWallets(_ walletID: String) + func removeFromInvisibleWallets(_ walletID: String) func getSortedWallets(includeInvisible: Bool) -> [String] + func isInvisible(_ walletID: String) -> Bool + func setIndexPositionWallets(_ indexes: [String], includeInvisible: Bool) - func getIndexPosition(for wallet: WalletCoreProtocol) -> Int? - func setIndexPositionWallets(_ wallets: [WalletCoreProtocol], includeInvisible: Bool) - + func reset() - - func sorted(includeInvisible: Bool) -> [WalletService] } diff --git a/Adamant/ServiceProtocols/WalletsStoreService.swift b/Adamant/ServiceProtocols/WalletsStoreService.swift new file mode 100644 index 000000000..b467b908c --- /dev/null +++ b/Adamant/ServiceProtocols/WalletsStoreService.swift @@ -0,0 +1,12 @@ +// +// WalletsStoreService.swift +// Adamant +// +// Created by Dmitrij Meidus on 08.02.25. +// Copyright © 2025 Adamant. All rights reserved. +// + +protocol WalletStoreServiceProtocol { + func sorted(includeInvisible: Bool) -> [WalletService] + func isInvisible(_ wallet: WalletService) -> Bool +} diff --git a/Adamant/Services/AdamantAccountService.swift b/Adamant/Services/AdamantAccountService.swift index 36872bdb3..6eddfb615 100644 --- a/Adamant/Services/AdamantAccountService.swift +++ b/Adamant/Services/AdamantAccountService.swift @@ -6,28 +6,28 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation -import UIKit import Combine import CommonKit +import Foundation +import UIKit final class AdamantAccountService: AccountService, @unchecked Sendable { - + // MARK: Dependencies - + private let apiService: AdamantApiServiceProtocol private let adamantCore: AdamantCore - private let dialogService: DialogService - private let securedStore: SecuredStore + private let SecureStore: SecureStore private let walletServiceCompose: WalletServiceCompose private let currencyInfoService: InfoServiceProtocol + private let coreDataStack: CoreDataStack weak var notificationsService: NotificationsService? weak var pushNotificationsTokenService: PushNotificationsTokenService? - weak var visibleWalletService: VisibleWalletsService? - + var walletsStoreService: WalletStoreServiceProtocol? + // MARK: Properties - + @Atomic private(set) var state: AccountServiceState = .notLogged @Atomic private(set) var isBalanceExpired = true @Atomic private(set) var account: AdamantAccount? @@ -38,31 +38,32 @@ final class AdamantAccountService: AccountService, @unchecked Sendable { @Atomic private var previousAppState: UIApplication.State? @Atomic private var subscriptions = Set() @Atomic private var balanceInvalidationSubscription: AnyCancellable? - + init( apiService: AdamantApiServiceProtocol, adamantCore: AdamantCore, - dialogService: DialogService, - securedStore: SecuredStore, + SecureStore: SecureStore, walletServiceCompose: WalletServiceCompose, currencyInfoService: InfoServiceProtocol, + coreDataStack: CoreDataStack, connection: AnyObservable ) { self.apiService = apiService self.adamantCore = adamantCore - self.dialogService = dialogService - self.securedStore = securedStore + self.SecureStore = SecureStore self.walletServiceCompose = walletServiceCompose self.currencyInfoService = currencyInfoService - + self.coreDataStack = coreDataStack + NotificationCenter.default.addObserver(forName: .AdamantAccountService.forceUpdateBalance, object: nil, queue: OperationQueue.main) { [weak self] _ in self?.update() } - - NotificationCenter.default.addObserver(forName: .AdamantAccountService.forceUpdateAllBalances, object: nil, queue: OperationQueue.main) { [weak self] _ in + + NotificationCenter.default.addObserver(forName: .AdamantAccountService.forceUpdateAllBalances, object: nil, queue: OperationQueue.main) { + [weak self] _ in self?.updateAll() } - + NotificationCenter.default .notifications(named: UIApplication.didBecomeActiveNotification, object: nil) .sink { @MainActor [weak self] _ in @@ -71,91 +72,97 @@ final class AdamantAccountService: AccountService, @unchecked Sendable { self?.update() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: UIApplication.willResignActiveNotification, object: nil) .sink { @MainActor [weak self] _ in self?.previousAppState = .background } .store(in: &subscriptions) - + connection.filter { $0 }.sink { [weak self] _ in self?.update() }.store(in: &subscriptions) - - setupSecuredStore() + + setupSecureStore() } } // MARK: - Saved data extension AdamantAccountService { - func setStayLoggedIn(pin: String, completion: @escaping @Sendable (AccountServiceResult) -> Void) { + func setStayLoggedIn(pin: String) -> AccountServiceResult { guard let account = account, let keypair = keypair else { - completion(.failure(.userNotLogged)) - return + return .failure(.userNotLogged) } - + if hasStayInAccount { - completion(.failure(.internalError(message: "Already has account", error: nil))) - return + return .failure(.internalError(message: "Already has account", error: nil)) } - - securedStore.set(pin, for: .pin) - + + SecureStore.set(pin, for: .pin) + if let passphrase = passphrase { - securedStore.set(passphrase, for: .passphrase) + SecureStore.set(passphrase, for: .passphrase) } else { - securedStore.set(keypair.publicKey, for: .publicKey) - securedStore.set(keypair.privateKey, for: .privateKey) + SecureStore.set(keypair.publicKey, for: .publicKey) + SecureStore.set(keypair.privateKey, for: .privateKey) } - + hasStayInAccount = true - NotificationCenter.default.post(name: Notification.Name.AdamantAccountService.stayInChanged, object: self, userInfo: [AdamantUserInfoKey.AccountService.newStayInState : true]) - completion(.success(account: account, alert: nil)) + NotificationCenter.default.post( + name: Notification.Name.AdamantAccountService.stayInChanged, + object: self, + userInfo: [AdamantUserInfoKey.AccountService.newStayInState: true] + ) + return .success(account: account, alert: nil) } - + func validatePin(_ pin: String) -> Bool { - guard let savedPin = securedStore.get(.pin) else { + guard let savedPin = SecureStore.get(.pin) else { return false } - + return pin == savedPin } - + private func getSavedKeypair() -> Keypair? { - if let publicKey = securedStore.get(.publicKey), let privateKey = securedStore.get(.privateKey) { + if let publicKey = SecureStore.get(.publicKey), let privateKey = SecureStore.get(.privateKey) { return Keypair(publicKey: publicKey, privateKey: privateKey) } - + return nil } - + private func getSavedPassphrase() -> String? { - return securedStore.get(.passphrase) + return SecureStore.get(.passphrase) } - + func dropSavedAccount() { useBiometry = false isBalanceExpired = true pushNotificationsTokenService?.removeCurrentToken() balanceInvalidationSubscription = nil - Key.allCases.forEach(securedStore.remove) - + Key.allCases.forEach(SecureStore.remove) + hasStayInAccount = false - NotificationCenter.default.post(name: Notification.Name.AdamantAccountService.stayInChanged, object: self, userInfo: [AdamantUserInfoKey.AccountService.newStayInState : false]) - + NotificationCenter.default.post( + name: Notification.Name.AdamantAccountService.stayInChanged, + object: self, + userInfo: [AdamantUserInfoKey.AccountService.newStayInState: false] + ) + Task { @MainActor in notificationsService?.setNotificationsMode(.disabled, completion: nil) } } - + private func markBalanceAsFresh() { isBalanceExpired = false - + balanceInvalidationSubscription = Task { [weak self] in try await Task.sleep( interval: AdmWalletService.balanceLifetime, pauseInBackground: true ) - + guard let self else { return } isBalanceExpired = true NotificationCenter.default.post( @@ -164,27 +171,29 @@ extension AdamantAccountService { ) }.eraseToAnyCancellable() } - - private func setupSecuredStore() { - if securedStore.get(.passphrase) != nil { + + private func setupSecureStore() { + if SecureStore.get(.passphrase) != nil { hasStayInAccount = true - useBiometry = securedStore.get(.useBiometry) != nil - } else if securedStore.get(.publicKey) != nil, - securedStore.get(.privateKey) != nil, - securedStore.get(.pin) != nil { + useBiometry = SecureStore.get(.useBiometry) != nil + } else if SecureStore.get(.publicKey) != nil, + SecureStore.get(.privateKey) != nil, + SecureStore.get(.pin) != nil + { hasStayInAccount = true - - useBiometry = securedStore.get(.useBiometry) != nil + + useBiometry = SecureStore.get(.useBiometry) != nil } else { hasStayInAccount = false useBiometry = false } - - NotificationCenter.default.addObserver(forName: Notification.Name.SecuredStore.securedStorePurged, object: securedStore, queue: OperationQueue.main) { [weak self] notification in - guard let store = notification.object as? SecuredStore else { + + NotificationCenter.default.addObserver(forName: Notification.Name.SecureStore.SecureStorePurged, object: SecureStore, queue: OperationQueue.main) { + [weak self] notification in + guard let store = notification.object as? SecureStore else { return } - + if store.get(.passphrase) != nil { self?.hasStayInAccount = true self?.useBiometry = store.get(.useBiometry) != nil @@ -194,15 +203,15 @@ extension AdamantAccountService { } } } - + func updateUseBiometry(_ newValue: Bool) { $useBiometry.mutate { $0 = newValue && hasStayInAccount - + if $0 { - securedStore.set(String($0), for: .useBiometry) + SecureStore.set(String($0), for: .useBiometry) } else { - securedStore.remove(.useBiometry) + SecureStore.remove(.useBiometry) } } } @@ -214,36 +223,36 @@ extension AdamantAccountService { func update() { self.update(nil) } - + func updateAll() { update(nil, updateOnlyVisible: false) } - + func update(_ completion: (@Sendable (AccountServiceResult) -> Void)?) { update(completion, updateOnlyVisible: true) } - + func update(_ completion: (@Sendable (AccountServiceResult) -> Void)?, updateOnlyVisible: Bool) { switch state { case .notLogged, .isLoggingIn, .updating: return - + case .loggedIn: break } - + let prevState = state state = .updating - + guard let loggedAccount = account, let publicKey = loggedAccount.publicKey else { return } - - let wallets = walletServiceCompose.getWallets().map { $0.core } - + + let wallets = walletServiceCompose.getWallets() + Task { @Sendable in let result = await apiService.getAccount(byPublicKey: publicKey) - + switch result { case .success(let account): guard let acc = self.account, acc.address == account.address else { @@ -251,35 +260,28 @@ extension AdamantAccountService { state = .notLogged return } - + markBalanceAsFresh() self.account = account - + NotificationCenter.default.post( name: .AdamantAccountService.accountDataUpdated, object: self ) - + state = .loggedIn completion?(.success(account: account, alert: nil)) - - if let adm = wallets.first(where: { $0 is AdmWalletService }) { - adm.update() - } - + case .failure(let error): completion?(.failure(.apiError(error: error))) + isBalanceExpired = true state = prevState } } - - if updateOnlyVisible { - for wallet in wallets.filter({ !($0 is AdmWalletService) }) where !(visibleWalletService?.isInvisible(wallet) ?? false) { - wallet.update() - } - } else { - for wallet in wallets.filter({ !($0 is AdmWalletService) }) { - wallet.update() + + for wallet in wallets { + if !updateOnlyVisible || !(walletsStoreService?.isInvisible(wallet) ?? false) { + wallet.core.update() } } } @@ -289,153 +291,150 @@ extension AdamantAccountService { extension AdamantAccountService { // MARK: Passphrase @MainActor - func loginWith(passphrase: String) async throws -> AccountServiceResult { + func loginWith(passphrase: String, password: String) async throws -> AccountServiceResult { guard AdamantUtilities.validateAdamantPassphrase(passphrase: passphrase) else { throw AccountServiceError.invalidPassphrase } - - guard let keypair = adamantCore.createKeypairFor(passphrase: passphrase) else { + + guard let keypair = adamantCore.createKeypairFor(passphrase: passphrase, password: password) else { throw AccountServiceError.internalError(message: "Failed to generate keypair for passphrase", error: nil) } - + let account = try await loginWith(keypair: keypair) - + // MARK: Drop saved accs if let storedPassphrase = self.getSavedPassphrase(), - storedPassphrase != passphrase { + storedPassphrase != passphrase + { dropSavedAccount() } - + if let storedKeypair = self.getSavedKeypair(), - storedKeypair != self.keypair { + storedKeypair != self.keypair + { dropSavedAccount() } - + // Update and initiate wallet services self.passphrase = passphrase - + _ = await initWallets() - + return .success(account: account, alert: nil) } - + // MARK: Pincode func loginWith(pincode: String) async throws -> AccountServiceResult { - guard let storePin = securedStore.get(.pin) else { + guard let storePin = SecureStore.get(.pin) else { throw AccountServiceError.invalidPassphrase } - + guard storePin == pincode else { throw AccountServiceError.invalidPassphrase } - + return try await loginWithStoredAccount() } - + // MARK: Biometry @MainActor func loginWithStoredAccount() async throws -> AccountServiceResult { if let passphrase = getSavedPassphrase() { - let account = try await loginWith(passphrase: passphrase) + let account = try await loginWith(passphrase: passphrase, password: .empty) return account } - + if let keypair = getSavedKeypair() { let account = try await loginWith(keypair: keypair) - + let alert: (title: String, message: String)? - if securedStore.get(.showedV12) != nil { + if SecureStore.get(.showedV12) != nil { alert = nil } else { - securedStore.set("1", for: .showedV12) - alert = (title: String.adamant.accountService.updateAlertTitleV12, - message: String.adamant.accountService.updateAlertMessageV12) + SecureStore.set("1", for: .showedV12) + alert = ( + title: String.adamant.accountService.updateAlertTitleV12, + message: String.adamant.accountService.updateAlertMessageV12 + ) } - + for wallet in walletServiceCompose.getWallets() { wallet.core.setInitiationFailed(reason: .adamant.accountService.reloginToInitiateWallets) } - + return .success(account: account, alert: alert) } - + throw AccountServiceError.invalidPassphrase } - + // MARK: Keypair private func loginWith(keypair: Keypair) async throws -> AdamantAccount { switch state { case .isLoggingIn: throw AccountServiceError.internalError(message: "Service is busy", error: nil) - case .updating: - fallthrough - + // Logout first - case .loggedIn: + case .updating, .loggedIn: logout() - + // Go login case .notLogged: break } - + state = .isLoggingIn - + do { let account = try await apiService.getAccount(byPublicKey: keypair.publicKey).get() self.account = account self.keypair = keypair markBalanceAsFresh() - + let userInfo = [AdamantUserInfoKey.AccountService.loggedAccountAddress: account.address] - + NotificationCenter.default.post( name: Notification.Name.AdamantAccountService.userLoggedIn, object: self, userInfo: userInfo ) - + self.state = .loggedIn return account - } catch let error as ApiServiceError { + } catch let error { self.state = .notLogged - + switch error { case .accountNotFound: throw AccountServiceError.wrongPassphrase - + default: throw AccountServiceError.apiError(error: error) } - } catch { - throw AccountServiceError.internalError(message: error.localizedDescription, error: error) } } - - func reloadWallets() { - Task { - _ = await initWallets() - } + + func reloadWallets() async { + _ = await initWallets() } - + func initWallets() async -> [WalletAccount?] { guard let passphrase = passphrase else { print("No passphrase found") return [] } - + return await withTaskGroup(of: WalletAccount?.self) { group in for wallet in walletServiceCompose.getWallets() { group.addTask { - let result = try? await wallet.core.initWallet( + try? await wallet.core.initWallet( withPassphrase: passphrase ) - return result } } - + var wallets: [WalletAccount?] = [] - + for await wallet in group { wallets.append(wallet) } @@ -451,15 +450,17 @@ extension AdamantAccountService { if account != nil { NotificationCenter.default.post(name: Notification.Name.AdamantAccountService.userWillLogOut, object: self) } - + dropSavedAccount() - + let wasLogged = account != nil account = nil keypair = nil passphrase = nil state = .notLogged - + apiService.cancelCurrentTasks() + coreDataStack.clearCoreData() + guard wasLogged else { return } NotificationCenter.default.post(name: .AdamantAccountService.userLoggedOut, object: self) } @@ -474,7 +475,7 @@ private enum Key: CaseIterable { case showedV12 case blockListKey case removedMessages - + var stringValue: String { switch self { case .publicKey: return StoreKey.accountService.publicKey @@ -489,16 +490,16 @@ private enum Key: CaseIterable { } } -private extension SecuredStore { - func set(_ value: String, for key: Key) { +extension SecureStore { + fileprivate func set(_ value: String, for key: Key) { set(value, for: key.stringValue) } - + func get(_ key: Key) -> String? { - return get(key.stringValue) + get(key.stringValue) } - - func remove(_ key: Key) { + + fileprivate func remove(_ key: Key) { remove(key.stringValue) } } diff --git a/Adamant/Services/AdamantAddressBookService.swift b/Adamant/Services/AdamantAddressBookService.swift index 5b346af36..4d53fab27 100644 --- a/Adamant/Services/AdamantAddressBookService.swift +++ b/Adamant/Services/AdamantAddressBookService.swift @@ -6,36 +6,36 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit import Clibsodium import Combine import CommonKit +import UIKit @MainActor final class AdamantAddressBookService: AddressBookService { let addressBookKey = "contact_list" - let waitTime: TimeInterval = 20.0 // in sec - + let waitTime: TimeInterval = 20.0 // in sec + // MARK: - Dependencies - + private let apiService: AdamantApiServiceProtocol private let adamantCore: AdamantCore private let accountService: AccountService private let dialogService: DialogService - + // MARK: - Properties - + var addressBook: [String: String] = [:] - + private(set) var hasChanges = false - - private var removedNames = [String:String]() - + + private var removedNames = [String: String]() + private var savingBookTaskId = UIBackgroundTaskIdentifier.invalid private var savingBookOnLogoutTaskId = UIBackgroundTaskIdentifier.invalid - + private var notificationsSet: Set = [] - + // MARK: - Lifecycle init( apiService: AdamantApiServiceProtocol, @@ -49,12 +49,12 @@ final class AdamantAddressBookService: AddressBookService { self.dialogService = dialogService addObservers() } - + // MARK: Observers - + private func addObservers() { // Update on login - + NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedIn) .sink { _ in @@ -63,9 +63,9 @@ final class AdamantAddressBookService: AddressBookService { } } .store(in: ¬ificationsSet) - + // Save on logout - + NotificationCenter.default .notifications(named: .AdamantAccountService.userWillLogOut) .sink { _ in @@ -74,9 +74,9 @@ final class AdamantAddressBookService: AddressBookService { } } .store(in: ¬ificationsSet) - + // Clean on logout - + NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedOut) .sink { _ in @@ -86,63 +86,63 @@ final class AdamantAddressBookService: AddressBookService { } .store(in: ¬ificationsSet) } - + // MARK: - Observer Actions - + private func userWillLogOut() async { guard hasChanges, let keypair = accountService.keypair else { return } - + savingBookOnLogoutTaskId = UIApplication.shared.beginBackgroundTask { [unowned self] in UIApplication.shared.endBackgroundTask(self.savingBookOnLogoutTaskId) self.savingBookOnLogoutTaskId = .invalid } - + _ = try? await saveAddressBook(self.addressBook, keypair: keypair) - + UIApplication.shared.endBackgroundTask(savingBookOnLogoutTaskId) savingBookOnLogoutTaskId = .invalid } - + private func userLoggedOut() async { hasChanges = false - + addressBook.removeAll() } - + // MARK: - Setting - + @MainActor func getName(for key: String) -> String? { return addressBook[key]?.checkAndReplaceSystemWallets() } - + @MainActor func getName(for partner: BaseAccount?) -> String? { guard let partenerAddress = partner?.address else { return nil } - + return getName(for: partenerAddress) ?? partner?.name?.checkAndReplaceSystemWallets() } - + func set(name: String, for address: String) async { guard addressBook[address] == nil || addressBook[address] != name else { return } - + let changes: [AddressBookChange] - + if name.count > 0 { if let prevName = addressBook[address] { if prevName == name { return } - + changes = [AddressBookChange.updated(address: address, name: name)] } else { changes = [AddressBookChange.newName(address: address, name: name)] } - + addressBook[address] = name } else if let prevName = addressBook[address] { addressBook.removeValue(forKey: address) @@ -151,36 +151,36 @@ final class AdamantAddressBookService: AddressBookService { } else { return } - + hasChanges = true - + NotificationCenter.default.post( name: Notification.Name.AdamantAddressBookService.addressBookUpdated, object: self, userInfo: [AdamantUserInfoKey.AddressBook.changes: changes] ) - + try? await Task.sleep(interval: waitTime) await saveIfNeeded() } - + // MARK: - Updating - + func update() async -> AddressBookServiceResult? { guard !hasChanges else { return nil } - + do { let book = try await getAddressBook() - + guard self.addressBook != book else { return .success } - + var localBook = self.addressBook var changes = [AddressBookChange]() - + for (address, name) in book { if let localName = localBook[address] { if localName != name { @@ -191,143 +191,151 @@ final class AdamantAddressBookService: AddressBookService { if let removedName = self.removedNames[address], removedName == name { continue } - + localBook[address] = name changes.append(AddressBookChange.newName(address: address, name: name)) } } - + self.addressBook = localBook - + NotificationCenter.default.post( name: Notification.Name.AdamantAddressBookService.addressBookUpdated, object: self, userInfo: [AdamantUserInfoKey.AddressBook.changes: changes] ) - + return .success } catch { return nil } } - + // MARK: - Saving - + func saveIfNeeded() async { guard hasChanges, let keypair = accountService.keypair else { return } - + // Background task savingBookTaskId = UIApplication.shared.beginBackgroundTask { UIApplication.shared.endBackgroundTask(self.savingBookTaskId) self.savingBookTaskId = .invalid } - + guard let id = try? await saveAddressBook(addressBook, keypair: keypair) else { return } - + var done: Bool = false - + // Hold updates until transaction passed on backend - + while !done { try? await Task.sleep(interval: 3.0) - - if let _ = try? await apiService.getTransaction(id: id) { + + if (try? await apiService.getTransaction(id: id)) != nil { done = true } } - + if done { self.hasChanges = false self.removedNames.removeAll() } - + UIApplication.shared.endBackgroundTask(self.savingBookTaskId) self.savingBookTaskId = .invalid } - + private func saveAddressBook(_ book: [String: String], keypair: Keypair) async throws -> UInt64 { guard let loggedAccount = accountService.account else { throw AddressBookServiceError.notLogged } - + guard loggedAccount.balance >= AdamantApiService.KvsFee else { throw AddressBookServiceError.notEnoughMoney } - + let address = loggedAccount.address - + // MARK: 1. Pack and ecode address book - + let packed = AdamantAddressBookService.packAddressBook(book: book) - - guard let encodeResult = adamantCore.encodeValue( - packed, - privateKey: keypair.privateKey - ) else { + + guard + let encodeResult = adamantCore.encodeValue( + packed, + privateKey: keypair.privateKey + ) + else { throw AddressBookServiceError.internalError(message: "Processing error", error: nil) } - + let value = AdamantUtilities.JSONStringify( - value: ["message": encodeResult.message, - "nonce": encodeResult.nonce] as AnyObject + value: [ + "message": encodeResult.message, + "nonce": encodeResult.nonce + ] as AnyObject ) - + // MARK: 2. Submit to KVS - + do { - let id = try await apiService.store(.init( - key: addressBookKey, - value: value, - keypair: keypair - )).get() - + let id = try await apiService.store( + .init( + key: addressBookKey, + value: value, + keypair: keypair + ), + date: AdmWalletService.correctedDate + ).get() + return id } catch let error { throw AddressBookServiceError.apiServiceError(error: error) } } - + // MARK: - Getting address book - + private func getAddressBook() async throws -> [String: String] { guard let loggedAccount = accountService.account, - let keypair = accountService.keypair + let keypair = accountService.keypair else { throw AddressBookServiceError.notLogged } - + let address = loggedAccount.address - + do { let rawValue = try await apiService.get(key: addressBookKey, sender: address).get() guard let value = rawValue, - let object = value.toDictionary(), - let message = object["message"] as? String, - let nonce = object["nonce"] as? String + let object = value.toDictionary(), + let message = object["message"] as? String, + let nonce = object["nonce"] as? String else { throw AddressBookServiceError.internalError(message: "Processing error", error: nil) } - + // MARK: Encoding - - guard let result = adamantCore.decodeValue( - rawMessage: message, - rawNonce: nonce, - privateKey: keypair.privateKey - ), - let value = result.matches(for: "\\{.*\\}").first, - let object = value.toDictionary(), - let rawAddressBook = object["payload"] as? [String: Any] + + guard + let result = adamantCore.decodeValue( + rawMessage: message, + rawNonce: nonce, + privateKey: keypair.privateKey + ), + let value = result.matches(for: "\\{.*\\}").first, + let object = value.toDictionary(), + let rawAddressBook = object["payload"] as? [String: Any] else { throw AddressBookServiceError.internalError(message: "Encoding error", error: nil) } - + let book = AdamantAddressBookService.processAddressBook(rawBook: rawAddressBook) - + return book } catch let error as ApiServiceError { throw AddressBookServiceError.apiServiceError(error: error) @@ -343,10 +351,10 @@ final class AdamantAddressBookService: AddressBookService { // MARK: - Tools extension AdamantAddressBookService { - private static func processAddressBook(rawBook: [String:Any]) -> [String:String] { - var processedBook = [String:String]() + private static func processAddressBook(rawBook: [String: Any]) -> [String: String] { + var processedBook = [String: String]() for key in rawBook.keys { - if let value = rawBook[key] as? [String:Any], let displayName = value["displayName"] as? String { + if let value = rawBook[key] as? [String: Any], let displayName = value["displayName"] as? String { if !displayName.trimmingCharacters(in: .whitespaces).isEmpty { processedBook[key] = displayName } @@ -354,9 +362,9 @@ extension AdamantAddressBookService { } return processedBook } - - private static func packAddressBook(book: [String:String]) -> [String:Any] { - var processedBook = [String:Any]() + + private static func packAddressBook(book: [String: String]) -> [String: Any] { + var processedBook = [String: Any]() for key in book.keys { if let value = book[key] { processedBook[key] = ["displayName": value] diff --git a/Adamant/Services/AdamantAuthentication.swift b/Adamant/Services/AdamantAuthentication.swift index d3aac41b2..ebb247be5 100644 --- a/Adamant/Services/AdamantAuthentication.swift +++ b/Adamant/Services/AdamantAuthentication.swift @@ -14,29 +14,19 @@ final class AdamantAuthentication: LocalAuthentication { let context = LAContext() var error: NSError? let available: Bool - - if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) { - available = true - } else if let errorCode = error?.code { - let lockoutCode = LAError.biometryLockout.rawValue - - if errorCode == lockoutCode { - available = true - } else { - available = false - } - } else { - available = false - } - - if available { + + available = context.canEvaluatePolicy( + .deviceOwnerAuthenticationWithBiometrics, + error: &error + ) + if available || error?.code == LAError.biometryLockout.rawValue { switch context.biometryType { case .none, .opticID: return .none - + case .touchID: return .touchID - + case .faceID: return .faceID @unknown default: @@ -46,49 +36,48 @@ final class AdamantAuthentication: LocalAuthentication { return .none } } - - func authorizeUser(reason: String, completion: @escaping (AuthenticationResult) -> Void) { + + func authorizeUser(reason: String) async -> AuthenticationResult { let context = LAContext() - context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { (success, error) in - if success { - completion(.success) - return - } - - guard let error = error as? LAError else { - completion(.failed) - return - } - - if error.code == LAError.userFallback { - completion(.fallback) - return - } - - if error.code == LAError.userCancel { - completion(.cancel) - return + let result = await authorizeUser( + context: context, + policy: .deviceOwnerAuthenticationWithBiometrics, + reason: reason + ) + if result == .biometryLockout { + return await authorizeUser( + context: context, + policy: .deviceOwnerAuthentication, + reason: reason + ) + } + return result + } + + private func authorizeUser( + context: LAContext, + policy: LAPolicy, + reason: String + ) async -> AuthenticationResult { + do { + let result = try await context.evaluatePolicy(policy, localizedReason: reason) + if result { + return .success } - - let tryDeviceOwner = error.code == LAError.biometryLockout - - if tryDeviceOwner { - context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { (success, error) in - let result: AuthenticationResult - - if success { - result = .success - } else if let error = error as? LAError, error.code == LAError.userCancel { - result = .cancel - } else { - result = .failed - } - - completion(result) - } - } else { - completion(.failed) + } catch let error as LAError { + switch error.code { + case .userFallback: + return .fallback + case .biometryLockout: + return .biometryLockout + case .userCancel: + return .cancel + default: + return .failed } + } catch { + return .failed } + return .failed } } diff --git a/Adamant/Services/AdamantCellFactory.swift b/Adamant/Services/AdamantCellFactory.swift index ac424a407..a16061b34 100644 --- a/Adamant/Services/AdamantCellFactory.swift +++ b/Adamant/Services/AdamantCellFactory.swift @@ -21,12 +21,12 @@ final class AdamantCellFactory: CellFactory { return UINib(nibName: sharedCell.defaultXibName, bundle: nil) } - + func cellInstance(for sharedCell: SharedCell) -> UITableViewCell? { guard let nib = nib(for: sharedCell) else { return nil } - + let cell = nib.instantiate(withOwner: nil, options: nil).first as? UITableViewCell return cell } diff --git a/Adamant/Services/AdamantCoinStorageService.swift b/Adamant/Services/AdamantCoinStorageService.swift index d39721797..060edc89a 100644 --- a/Adamant/Services/AdamantCoinStorageService.swift +++ b/Adamant/Services/AdamantCoinStorageService.swift @@ -6,51 +6,51 @@ // Copyright © 2023 Adamant. All rights reserved. // -import Foundation -import CoreData import Combine import CommonKit +import CoreData +import Foundation final class AdamantCoinStorageService: NSObject, CoinStorageService { - + // MARK: Proprieties - + private let blockchainType: String private let coinId: String private let coreDataStack: CoreDataStack private lazy var transactionController = getTransactionController() private var subscriptions = Set() - + @ObservableValue private var transactions: [TransactionDetails] = [] var transactionsPublisher: any Observable<[TransactionDetails]> { $transactions } - + // MARK: Init - + init(coinId: String, coreDataStack: CoreDataStack, blockchainType: String) { self.coinId = coinId self.coreDataStack = coreDataStack self.blockchainType = blockchainType super.init() - + try? transactionController.performFetch() transactions = transactionController.fetchedObjects ?? [] - + setupObserver() } - + func append(_ transaction: TransactionDetails) { append([transaction]) } - + func append(_ transactions: [TransactionDetails]) { let privateContext = coreDataStack.container.viewContext privateContext.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyObjectTrumpMergePolicyType) - + var coinTransactions: [CoinTransaction] = [] - + transactions.forEach { transaction in let isExist = self.transactions.contains { tx in tx.txId == transaction.txId @@ -59,7 +59,7 @@ final class AdamantCoinStorageService: NSObject, CoinStorageService { tx.txId == transaction.txId } guard !isExist, !isLocalExist else { return } - + let coinTransaction = CoinTransaction(context: privateContext) coinTransaction.amount = NSDecimalNumber(decimal: transaction.amountValue ?? 0) coinTransaction.date = (transaction.dateValue ?? Date()) as NSDate @@ -72,32 +72,34 @@ final class AdamantCoinStorageService: NSObject, CoinStorageService { coinTransaction.blockchainType = blockchainType coinTransaction.fee = NSDecimalNumber(decimal: transaction.feeValue ?? 0) coinTransaction.nonceRaw = transaction.nonceRaw - + coinTransactions.append(coinTransaction) } - + try? privateContext.save() } - + func updateStatus(for transactionId: String, status: TransactionStatus?) { let privateContext = coreDataStack.container.viewContext - - guard let transaction = getTransactionFromDB( - id: transactionId, - context: privateContext - ) else { return } - + + guard + let transaction = getTransactionFromDB( + id: transactionId, + context: privateContext + ) + else { return } + transaction.transactionStatus = status try? privateContext.save() } - + func clear() { transactions = [] } } -private extension AdamantCoinStorageService { - func setupObserver() { +extension AdamantCoinStorageService { + fileprivate func setupObserver() { NotificationCenter.default.publisher( for: .NSManagedObjectContextObjectsDidChange, object: coreDataStack.container.viewContext @@ -111,24 +113,25 @@ private extension AdamantCoinStorageService { } self?.transactions.append(contentsOf: filteredInserted) } - + if let updated = changes.updated, !updated.isEmpty { let filteredUpdated = updated.filter { $0.coinId == self?.coinId } - + filteredUpdated.forEach { coinTransaction in - guard let index = self?.transactions.firstIndex(where: { - $0.txId == coinTransaction.txId - }) + guard + let index = self?.transactions.firstIndex(where: { + $0.txId == coinTransaction.txId + }) else { return } - + self?.transactions[index] = coinTransaction } } } .store(in: &subscriptions) } - - func getTransactionController() -> NSFetchedResultsController { + + fileprivate func getTransactionController() -> NSFetchedResultsController { let request: NSFetchRequest = NSFetchRequest( entityName: CoinTransaction.entityCoinName ) @@ -139,7 +142,7 @@ private extension AdamantCoinStorageService { NSSortDescriptor(key: "date", ascending: true), NSSortDescriptor(key: "transactionId", ascending: true) ] - + return NSFetchedResultsController( fetchRequest: request, managedObjectContext: coreDataStack.container.viewContext, @@ -147,16 +150,16 @@ private extension AdamantCoinStorageService { cacheName: nil ) } - + /// Search transaction in local storage /// /// - Parameter id: Transacton ID /// - Returns: Transaction, if found - func getTransactionFromDB(id: String, context: NSManagedObjectContext) -> CoinTransaction? { + fileprivate func getTransactionFromDB(id: String, context: NSManagedObjectContext) -> CoinTransaction? { let request = NSFetchRequest(entityName: CoinTransaction.entityCoinName) request.predicate = NSPredicate(format: "transactionId == %@", String(id)) request.fetchLimit = 1 - + do { let result = try context.fetch(request) return result.first @@ -177,9 +180,10 @@ extension Notification { return ManagedObjectContextChanges( updated: objects(forKey: NSUpdatedObjectsKey), inserted: objects(forKey: NSInsertedObjectsKey), - deleted: objects(forKey: NSDeletedObjectsKey)) + deleted: objects(forKey: NSDeletedObjectsKey) + ) } - + private func objects(forKey key: String) -> Set? { guard let userInfo = userInfo else { assertionFailure() diff --git a/Adamant/Services/AdamantCrashlysticsService.swift b/Adamant/Services/AdamantCrashlysticsService.swift index e262cde14..a7996ad25 100644 --- a/Adamant/Services/AdamantCrashlysticsService.swift +++ b/Adamant/Services/AdamantCrashlysticsService.swift @@ -6,28 +6,28 @@ // Copyright © 2023 Adamant. All rights reserved. // -import Foundation import Combine -import Firebase import CommonKit +import Firebase +import Foundation @MainActor final class AdamantCrashlyticsService: CrashlyticsService { - + // MARK: Dependencies - - let securedStore: SecuredStore - + + let SecureStore: SecureStore + // MARK: Proprieties - + @Atomic private var notificationsSet: Set = [] private var isConfigured = false - + // MARK: Lifecycle - - init(securedStore: SecuredStore) { - self.securedStore = securedStore - + + init(SecureStore: SecureStore) { + self.SecureStore = SecureStore + NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedOut) .sink { [weak self] _ in @@ -35,38 +35,40 @@ final class AdamantCrashlyticsService: CrashlyticsService { } .store(in: ¬ificationsSet) } - + // MARK: Notification actions - + private func userLoggedOut() { - securedStore.remove(StoreKey.increaseFee.increaseFee) + SecureStore.remove(StoreKey.increaseFee.increaseFee) updateCrashlyticSDK(isEnabled: false) } - + // MARK: Update data - + func setCrashlyticsEnabled(_ value: Bool) { - securedStore.set(value, for: StoreKey.crashlytic.crashlyticEnabled) + SecureStore.set(value, for: StoreKey.crashlytic.crashlyticEnabled) updateCrashlyticSDK(isEnabled: value) } - + func isCrashlyticsEnabled() -> Bool { - guard let result: Bool = securedStore.get( - StoreKey.crashlytic.crashlyticEnabled - ) else { + guard + let result: Bool = SecureStore.get( + StoreKey.crashlytic.crashlyticEnabled + ) + else { return false } - + return result } - + func configureIfNeeded() { guard !isConfigured && isCrashlyticsEnabled() else { return } - + FirebaseApp.configure() isConfigured = true } - + private func updateCrashlyticSDK(isEnabled: Bool) { configureIfNeeded() Crashlytics.crashlytics().setCrashlyticsCollectionEnabled(isEnabled) diff --git a/Adamant/Services/AdamantEmojiService.swift b/Adamant/Services/AdamantEmojiService.swift index 0bb7ccabd..dbcc1f1a3 100644 --- a/Adamant/Services/AdamantEmojiService.swift +++ b/Adamant/Services/AdamantEmojiService.swift @@ -6,35 +6,35 @@ // Copyright © 2023 Adamant. All rights reserved. // -import Foundation import Combine import CommonKit +import Foundation final class AdamantEmojiService: EmojiService, @unchecked Sendable { // MARK: Dependencies - - let securedStore: SecuredStore - + + let SecureStore: SecureStore + // MARK: Proprieties - + @Atomic private var notificationsSet: Set = [] @Atomic private var defaultEmojis = ["😂": 3, "🔥": 3, "😁": 3, "👍": 2, "👌": 2, "❤️️️️️️️": 2, "🙂": 2, "🤔": 2, "👋": 2, "🙏": 2, "😳": 2, "🎉": 2] private let maxEmojiCount = 12 private let incCount = 4 private let decCount = 2 - + // MARK: Lifecycle - - init(securedStore: SecuredStore) { - self.securedStore = securedStore - + + init(SecureStore: SecureStore) { + self.SecureStore = SecureStore + NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedIn) .sink { [weak self] _ in self?.userLoggedIn() } .store(in: ¬ificationsSet) - + NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedOut) .sink { [weak self] _ in @@ -42,34 +42,35 @@ final class AdamantEmojiService: EmojiService, @unchecked Sendable { } .store(in: ¬ificationsSet) } - + // MARK: Notification actions - + private func userLoggedOut() { - securedStore.remove(StoreKey.emojis.emojis) + SecureStore.remove(StoreKey.emojis.emojis) } - + private func userLoggedIn() { setDefaultEmojiIfNeeded() } - + // MARK: Update data - + private func setDefaultEmojiIfNeeded() { - let emojis: [String: Int]? = securedStore.get( + let emojis: [String: Int]? = SecureStore.get( StoreKey.emojis.emojis ) - + guard emojis == nil else { return } - - securedStore.set(defaultEmojis, for: StoreKey.emojis.emojis) + + SecureStore.set(defaultEmojis, for: StoreKey.emojis.emojis) } - + func getFrequentlySelectedEmojis() -> [String] { - let storedEmojis: [String: Int] = securedStore.get( - StoreKey.emojis.emojis - ) ?? defaultEmojis - + let storedEmojis: [String: Int] = + SecureStore.get( + StoreKey.emojis.emojis + ) ?? defaultEmojis + let sortedEmojis = storedEmojis.sorted { (emoji1, emoji2) in if emoji1.value == emoji2.value { return emoji1.key > emoji2.key @@ -77,7 +78,7 @@ final class AdamantEmojiService: EmojiService, @unchecked Sendable { return emoji1.value > emoji2.value } } - + return Array(sortedEmojis.prefix(maxEmojiCount)).map { $0.key } } @@ -85,20 +86,22 @@ final class AdamantEmojiService: EmojiService, @unchecked Sendable { selectedEmoji: String, type: EmojiUpdateType ) { - var storedEmojis: [String: Int] = securedStore.get( - StoreKey.emojis.emojis - ) ?? defaultEmojis - - let value = type == .increment - ? incCount - : -decCount - + var storedEmojis: [String: Int] = + SecureStore.get( + StoreKey.emojis.emojis + ) ?? defaultEmojis + + let value = + type == .increment + ? incCount + : -decCount + if let count = storedEmojis[selectedEmoji] { storedEmojis[selectedEmoji] = count + value } else { storedEmojis[selectedEmoji] = value } - - securedStore.set(storedEmojis, for: StoreKey.emojis.emojis) + + SecureStore.set(storedEmojis, for: StoreKey.emojis.emojis) } } diff --git a/Adamant/Services/AdamantIncreaseFeeService.swift b/Adamant/Services/AdamantIncreaseFeeService.swift index 44839b411..8ecbbacb3 100644 --- a/Adamant/Services/AdamantIncreaseFeeService.swift +++ b/Adamant/Services/AdamantIncreaseFeeService.swift @@ -6,33 +6,33 @@ // Copyright © 2023 Adamant. All rights reserved. // -import Foundation import Combine import CommonKit +import Foundation final class AdamantIncreaseFeeService: IncreaseFeeService, @unchecked Sendable { - + // MARK: Dependencies - - let securedStore: SecuredStore - + + let SecureStore: SecureStore + // MARK: Proprieties - + @Atomic private var increaseFeeData: [String: Bool] = [:] @Atomic private var notificationsSet: Set = [] - + // MARK: Lifecycle - - init(securedStore: SecuredStore) { - self.securedStore = securedStore - + + init(SecureStore: SecureStore) { + self.SecureStore = SecureStore + NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedOut) .sink { [weak self] _ in self?.userLoggedOut() } .store(in: ¬ificationsSet) - + NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedIn) .sink { [weak self] _ in @@ -40,36 +40,36 @@ final class AdamantIncreaseFeeService: IncreaseFeeService, @unchecked Sendable { } .store(in: ¬ificationsSet) } - + // MARK: Notification actions - + private func userLoggedIn() { increaseFeeData = getIncreaseFeeDictionary() } - + private func userLoggedOut() { - securedStore.remove(StoreKey.increaseFee.increaseFee) + SecureStore.remove(StoreKey.increaseFee.increaseFee) increaseFeeData = [:] } - + // MARK: Check - - func isIncreaseFeeEnabled(for tokenUnicID: String) -> Bool { - return increaseFeeData[tokenUnicID] ?? false + + func isIncreaseFeeEnabled(for tokenUniqueID: String) -> Bool { + return increaseFeeData[tokenUniqueID] ?? false } - - func setIncreaseFeeEnabled(for tokenUnicID: String, value: Bool) { + + func setIncreaseFeeEnabled(for tokenUniqueID: String, value: Bool) { $increaseFeeData.mutate { - $0[tokenUnicID] = value - securedStore.set($0, for: StoreKey.increaseFee.increaseFee) + $0[tokenUniqueID] = value + SecureStore.set($0, for: StoreKey.increaseFee.increaseFee) } } - + private func getIncreaseFeeDictionary() -> [String: Bool] { - guard let result: [String: Bool] = securedStore.get(StoreKey.increaseFee.increaseFee) else { + guard let result: [String: Bool] = SecureStore.get(StoreKey.increaseFee.increaseFee) else { return [:] } - + return result } } diff --git a/Adamant/Services/AdamantNotificationService.swift b/Adamant/Services/AdamantNotificationService.swift index 965216615..eb98fbb36 100644 --- a/Adamant/Services/AdamantNotificationService.swift +++ b/Adamant/Services/AdamantNotificationService.swift @@ -6,24 +6,24 @@ // Copyright © 2018 Adamant. All rights reserved. // +import AVFoundation +import Combine +import CommonKit +@preconcurrency import CoreData import Foundation import UIKit import UserNotifications -import CommonKit -import Combine -@preconcurrency import CoreData -import AVFoundation extension NotificationsMode { func toRaw() -> String { return String(self.rawValue) } - + init?(string: String) { guard let int = Int(string: string), let mode = NotificationsMode(rawValue: int) else { return nil } - + self = mode } } @@ -31,7 +31,7 @@ extension NotificationsMode { enum NotificationTarget: CaseIterable { case baseMessage case reaction - + var storeId: String { switch self { case .baseMessage: @@ -45,18 +45,18 @@ enum NotificationTarget: CaseIterable { @MainActor final class AdamantNotificationsService: NSObject, NotificationsService { // MARK: Dependencies - private let securedStore: SecuredStore + private let SecureStore: SecureStore private let vibroService: VibroService weak var accountService: AccountService? weak var chatsProvider: ChatsProvider? - + // MARK: Properties private let defaultNotificationsSound: NotificationSound = .inputDefault private let defaultNotificationsReactionSound: NotificationSound = .none private let defaultInAppSound: Bool = false private let defaultInAppVibrate: Bool = true private let defaultInAppToasts: Bool = true - + private(set) var notificationsMode: NotificationsMode = .disabled private(set) var customBadgeNumber = 0 private(set) var notificationsSound: NotificationSound = .inputDefault @@ -64,51 +64,45 @@ final class AdamantNotificationsService: NSObject, NotificationsService { private(set) var inAppSound: Bool = false private(set) var inAppVibrate: Bool = true private(set) var inAppToasts: Bool = true - + private var isBackgroundSession = false private var backgroundNotifications = 0 private var subscriptions = Set() - + private var preservedBadgeNumber: Int? private var audioPlayer: AVAudioPlayer? private var unreadController: NSFetchedResultsController? - + // MARK: Lifecycle init( - securedStore: SecuredStore, + SecureStore: SecureStore, vibroService: VibroService ) { - self.securedStore = securedStore + self.SecureStore = SecureStore self.vibroService = vibroService super.init() - + NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedIn, object: nil) .sink { @MainActor [weak self] _ in self?.onUserLoggedIn() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedOut, object: nil) .sink { @MainActor [weak self] _ in self?.onUserLoggedOut() } .store(in: &subscriptions) - - NotificationCenter.default - .notifications(named: .AdamantAccountService.stayInChanged, object: nil) - .compactMap { $0.userInfo?[AdamantUserInfoKey.AccountService.newStayInState] as? Bool } - .sink { @MainActor [weak self] in self?.onStayInChanged($0) } - .store(in: &subscriptions) } - + func setInAppSound(_ value: Bool) { setValue(for: StoreKey.notificationsService.inAppSounds, value: value) inAppSound = value } - + func setInAppVibrate(_ value: Bool) { setValue(for: StoreKey.notificationsService.inAppVibrate, value: value) inAppVibrate = value } - + func setInAppToasts(_ value: Bool) { setValue(for: StoreKey.notificationsService.inAppToasts, value: value) inAppToasts = value @@ -127,12 +121,12 @@ extension AdamantNotificationsService { case .reaction: notificationsReactionSound = sound } - - securedStore.set( + + SecureStore.set( sound.fileName, for: target.storeId ) - + NotificationCenter.default.post( name: .AdamantNotificationService.notificationsSoundChanged, object: self, @@ -147,87 +141,92 @@ extension AdamantNotificationsService { switch mode { case .disabled: AdamantNotificationsService.configureUIApplicationFor(mode: mode) - securedStore.remove(StoreKey.notificationsService.notificationsMode) + SecureStore.remove(StoreKey.notificationsService.notificationsMode) notificationsMode = mode - - NotificationCenter.default.post(name: Notification.Name.AdamantNotificationService.notificationsModeChanged, - object: self, - userInfo: [AdamantUserInfoKey.NotificationsService.newNotificationsMode: mode]) - + + NotificationCenter.default.post( + name: Notification.Name.AdamantNotificationService.notificationsModeChanged, + object: self, + userInfo: [AdamantUserInfoKey.NotificationsService.newNotificationsMode: mode] + ) + completion?(.success) return - + case .push: guard let account = accountService?.account, account.balance > AdamantApiService.KvsFee else { completion?(.failure(error: .notEnoughMoney)) return } - + fallthrough - + case .backgroundFetch: guard accountService?.hasStayInAccount ?? false else { completion?(.failure(error: .notStayedLoggedIn)) return } - + authorizeNotifications { [weak self] (success, error) in guard success else { completion?(.failure(error: .denied(error: error))) return } - + Task { @MainActor in AdamantNotificationsService.configureUIApplicationFor(mode: mode) } - - self?.securedStore.set( + + self?.SecureStore.set( mode.toRaw(), for: StoreKey.notificationsService.notificationsMode ) - + self?.notificationsMode = mode - + NotificationCenter.default.post( name: .AdamantNotificationService.notificationsModeChanged, object: self, userInfo: [AdamantUserInfoKey.NotificationsService.newNotificationsMode: mode] ) - + completion?(.success) } } } - + private func authorizeNotifications(completion: @escaping (Bool, Error?) -> Void) { UNUserNotificationCenter.current().getNotificationSettings { settings in switch settings.authorizationStatus { case .authorized, .ephemeral: completion(true, nil) - + case .denied, .provisional: completion(false, nil) - + case .notDetermined: - UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound], completionHandler: { (granted, error) in - completion(granted, error) - }) + UNUserNotificationCenter.current().requestAuthorization( + options: [.alert, .badge, .sound], + completionHandler: { (granted, error) in + completion(granted, error) + } + ) @unknown default: completion(false, nil) } } } - + private static func configureUIApplicationFor(mode: NotificationsMode) { switch mode { case .disabled: UIApplication.shared.unregisterForRemoteNotifications() UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalNever) - + case .backgroundFetch: UIApplication.shared.unregisterForRemoteNotifications() UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum) - + case .push: UIApplication.shared.registerForRemoteNotifications() UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalNever) @@ -267,48 +266,37 @@ extension AdamantNotificationsService { } } } - + func setBadge(number: Int?) { - setBadge(number: number, force: false) - } - - private func setBadge(number: Int?, force: Bool) { - if !force { - guard let stayIn = accountService?.hasStayInAccount, stayIn else { - preservedBadgeNumber = number - return - } - } - let appIconBadgeNumber: Int - + if let number = number { customBadgeNumber = number appIconBadgeNumber = number - securedStore.set(String(number), for: StoreKey.notificationsService.customBadgeNumber) + SecureStore.set(String(number), for: StoreKey.notificationsService.customBadgeNumber) } else { customBadgeNumber = 0 appIconBadgeNumber = 0 - securedStore.remove(StoreKey.notificationsService.customBadgeNumber) + SecureStore.remove(StoreKey.notificationsService.customBadgeNumber) } - + DispatchQueue.onMainAsync { UIApplication.shared.applicationIconBadgeNumber = appIconBadgeNumber } } - + func removeAllPendingNotificationRequests() { UNUserNotificationCenter.current().removeAllPendingNotificationRequests() UIApplication.shared.applicationIconBadgeNumber = customBadgeNumber } - + func removeAllDeliveredNotifications() { UNUserNotificationCenter.current().removeAllDeliveredNotifications() UIApplication.shared.applicationIconBadgeNumber = customBadgeNumber } - + func setValue(for key: String, value: Bool) { - securedStore.set(value, for: key) + SecureStore.set(value, for: key) } } @@ -318,79 +306,72 @@ extension AdamantNotificationsService { isBackgroundSession = true backgroundNotifications = 0 } - + func stopBackgroundBatchNotifications() { isBackgroundSession = false backgroundNotifications = 0 } } -private extension AdamantNotificationsService { - func onUserLoggedIn() { +extension AdamantNotificationsService { + fileprivate func onUserLoggedIn() { UNUserNotificationCenter.current().removeAllDeliveredNotifications() UIApplication.shared.applicationIconBadgeNumber = 0 - - if let raw: String = securedStore.get(StoreKey.notificationsService.notificationsMode), - let mode = NotificationsMode(string: raw) { + + if let raw: String = SecureStore.get(StoreKey.notificationsService.notificationsMode), + let mode = NotificationsMode(string: raw) + { setNotificationsMode(mode, completion: nil) } else { setNotificationsMode(.disabled, completion: nil) } - + NotificationTarget.allCases.forEach { target in - if let raw: String = securedStore.get(target.storeId), - let sound = NotificationSound(fileName: raw) { + if let raw: String = SecureStore.get(target.storeId), + let sound = NotificationSound(fileName: raw) + { setNotificationSound(sound, for: target) } } - - inAppSound = securedStore.get(StoreKey.notificationsService.inAppSounds) ?? defaultInAppSound - inAppVibrate = securedStore.get(StoreKey.notificationsService.inAppVibrate) ?? defaultInAppVibrate - inAppToasts = securedStore.get(StoreKey.notificationsService.inAppToasts) ?? defaultInAppToasts - + + inAppSound = SecureStore.get(StoreKey.notificationsService.inAppSounds) ?? defaultInAppSound + inAppVibrate = SecureStore.get(StoreKey.notificationsService.inAppVibrate) ?? defaultInAppVibrate + inAppToasts = SecureStore.get(StoreKey.notificationsService.inAppToasts) ?? defaultInAppToasts + preservedBadgeNumber = nil - + Task { await setupUnreadController() } } - - func onUserLoggedOut() { + + fileprivate func onUserLoggedOut() { setNotificationsMode(.disabled, completion: nil) setNotificationSound(defaultNotificationsSound, for: .baseMessage) setNotificationSound(defaultNotificationsReactionSound, for: .reaction) - securedStore.remove(StoreKey.notificationsService.notificationsMode) - securedStore.remove(StoreKey.notificationsService.notificationsSound) - securedStore.remove(StoreKey.notificationsService.notificationsReactionSound) - securedStore.remove(StoreKey.notificationsService.inAppSounds) - securedStore.remove(StoreKey.notificationsService.inAppVibrate) - securedStore.remove(StoreKey.notificationsService.inAppToasts) + SecureStore.remove(StoreKey.notificationsService.notificationsMode) + SecureStore.remove(StoreKey.notificationsService.notificationsSound) + SecureStore.remove(StoreKey.notificationsService.notificationsReactionSound) + SecureStore.remove(StoreKey.notificationsService.inAppSounds) + SecureStore.remove(StoreKey.notificationsService.inAppVibrate) + SecureStore.remove(StoreKey.notificationsService.inAppToasts) preservedBadgeNumber = nil - + resetUnreadController() } - - func onStayInChanged(_ stayIn: Bool) { - if stayIn { - setBadge(number: preservedBadgeNumber, force: false) - } else { - preservedBadgeNumber = nil - setBadge(number: nil, force: true) - } - } - - func setupUnreadController() async { + + fileprivate func setupUnreadController() async { unreadController = await chatsProvider?.getUnreadMessagesController() unreadController?.delegate = self try? unreadController?.performFetch() } - - func resetUnreadController() { + + fileprivate func resetUnreadController() { unreadController = nil unreadController?.delegate = nil } - - func playSound(by fileName: String) { + + fileprivate func playSound(by fileName: String) { guard let url = Bundle.main.url(forResource: fileName.replacingOccurrences(of: ".mp3", with: ""), withExtension: "mp3") else { return } @@ -416,11 +397,11 @@ extension AdamantNotificationsService: NSFetchedResultsControllerDelegate { let transaction = anObject as? ChatTransaction, type == .insert else { return } - + if inAppVibrate { vibroService.applyVibration(.medium) } - + if inAppSound { switch transaction { case let tx as RichMessageTransaction where tx.additionalType == .reaction: diff --git a/Adamant/Services/AdamantPartnerQRService.swift b/Adamant/Services/AdamantPartnerQRService.swift index 3d3657d81..d8387b0fa 100644 --- a/Adamant/Services/AdamantPartnerQRService.swift +++ b/Adamant/Services/AdamantPartnerQRService.swift @@ -6,25 +6,25 @@ // Copyright © 2023 Adamant. All rights reserved. // -import Foundation -import CommonKit import Combine +import CommonKit +import Foundation final class AdamantPartnerQRService: PartnerQRService, @unchecked Sendable { - + // MARK: Dependencies - - let securedStore: SecuredStore - + + let SecureStore: SecureStore + // MARK: Proprieties - + @Atomic private var notificationsSet: Set = [] - + // MARK: Lifecycle - - init(securedStore: SecuredStore) { - self.securedStore = securedStore - + + init(SecureStore: SecureStore) { + self.SecureStore = SecureStore + NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedOut) .sink { [weak self] _ in @@ -32,41 +32,45 @@ final class AdamantPartnerQRService: PartnerQRService, @unchecked Sendable { } .store(in: ¬ificationsSet) } - + // MARK: Notification actions - + private func userLoggedOut() { setIncludeNameEnabled(true) setIncludeURLEnabled(true) } - + // MARK: Update data - + func setIncludeNameEnabled(_ value: Bool) { - securedStore.set(value, for: StoreKey.partnerQR.includeNameEnabled) + SecureStore.set(value, for: StoreKey.partnerQR.includeNameEnabled) } - + func isIncludeNameEnabled() -> Bool { - guard let result: Bool = securedStore.get( - StoreKey.partnerQR.includeNameEnabled - ) else { + guard + let result: Bool = SecureStore.get( + StoreKey.partnerQR.includeNameEnabled + ) + else { return true } - + return result } - + func setIncludeURLEnabled(_ value: Bool) { - securedStore.set(value, for: StoreKey.partnerQR.includeURLEnabled) + SecureStore.set(value, for: StoreKey.partnerQR.includeURLEnabled) } - + func isIncludeURLEnabled() -> Bool { - guard let result: Bool = securedStore.get( - StoreKey.partnerQR.includeURLEnabled - ) else { + guard + let result: Bool = SecureStore.get( + StoreKey.partnerQR.includeURLEnabled + ) + else { return true } - + return result } } diff --git a/Adamant/Services/AdamantPushNotificationsTokenService.swift b/Adamant/Services/AdamantPushNotificationsTokenService.swift index 63984ba18..971980b7f 100644 --- a/Adamant/Services/AdamantPushNotificationsTokenService.swift +++ b/Adamant/Services/AdamantPushNotificationsTokenService.swift @@ -6,43 +6,43 @@ // Copyright © 2022 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation final class AdamantPushNotificationsTokenService: PushNotificationsTokenService, @unchecked Sendable { - private let securedStore: SecuredStore + private let SecureStore: SecureStore private let apiService: AdamantApiServiceProtocol private let adamantCore: AdamantCore private let accountService: AccountService - + private let tokenProcessingQueue = DispatchQueue(label: "com.adamant.push-token-processing-queue") private let tokenProcessingSemaphore = DispatchSemaphore(value: 1) - private let securedStoreSemaphore = DispatchSemaphore(value: 1) - + private let SecureStoreSemaphore = DispatchSemaphore(value: 1) + init( - securedStore: SecuredStore, + SecureStore: SecureStore, apiService: AdamantApiServiceProtocol, adamantCore: AdamantCore, accountService: AccountService ) { - self.securedStore = securedStore + self.SecureStore = SecureStore self.apiService = apiService self.adamantCore = adamantCore self.accountService = accountService } - + func setToken(_ token: Data) { tokenProcessingQueue.async { [weak self] in self?._setToken(token) } } - + func removeCurrentToken() { tokenProcessingQueue.async { [weak self] in self?._removeCurrentToken() } } - + func sendTokenDeletionTransactions() { for transaction in getTokenDeletionTransactions() { Task { @@ -50,11 +50,12 @@ final class AdamantPushNotificationsTokenService: PushNotificationsTokenService, path: ApiCommands.Chats.processTransaction, transaction: transaction ) - + switch result { case .success, .failure(.accountNotFound), .failure(.notLogged): removeTokenDeletionTransaction(transaction) - case .failure(.internalError), .failure(.networkError), .failure(.requestCancelled), .failure(.serverError), .failure(.commonError), .failure(.noEndpointsAvailable): + case .failure(.internalError), .failure(.networkError), .failure(.requestCancelled), .failure(.serverError), .failure(.commonError), + .failure(.noEndpointsAvailable): break } } @@ -62,60 +63,60 @@ final class AdamantPushNotificationsTokenService: PushNotificationsTokenService, } } -private extension AdamantPushNotificationsTokenService { - typealias EncodedPayload = (message: String, nonce: String) - - var ansProvider: ANSPayload.Provider { +extension AdamantPushNotificationsTokenService { + fileprivate typealias EncodedPayload = (message: String, nonce: String) + + fileprivate var ansProvider: ANSPayload.Provider { #if DEBUG - return .apnsSandbox + return .apnsSandbox #else - return .apns + return .apns #endif } - - func _setToken(_ token: Data) { + + fileprivate func _setToken(_ token: Data) { tokenProcessingSemaphore.wait() guard let keypair = accountService.keypair else { assertionFailure("Trying to register with no user logged") tokenProcessingSemaphore.signal() return } - + let token = mapToken(token) AdamantUtilities.consoleLog("APNS token:", token) - + guard token != getToken() else { tokenProcessingSemaphore.signal() return } - + updateCurrentToken(newToken: token, keypair: keypair) { [weak self] in self?.tokenProcessingSemaphore.signal() } } - - func _removeCurrentToken() { + + fileprivate func _removeCurrentToken() { tokenProcessingSemaphore.wait() guard let keypair = accountService.keypair else { assertionFailure("Trying to unregister with no user logged") tokenProcessingSemaphore.signal() return } - + removeCurrentToken(keypair: keypair) { [weak self] in self?.tokenProcessingSemaphore.signal() } } - - func mapToken(_ token: Data) -> String { + + fileprivate func mapToken(_ token: Data) -> String { token.map { String(format: "%02.2hhx", $0) }.joined() } - - func updateCurrentToken(newToken: String, keypair: Keypair, completion: @escaping @Sendable () -> Void) { + + fileprivate func updateCurrentToken(newToken: String, keypair: Keypair, completion: @escaping @Sendable () -> Void) { guard let encodedPayload = makeEncodedPayload(token: newToken, keypair: keypair, action: .add) else { return completion() } - + removeCurrentToken(keypair: keypair) { [weak self] in self?.sendMessageToANS( keypair: keypair, @@ -127,8 +128,8 @@ private extension AdamantPushNotificationsTokenService { } } } - - func removeCurrentToken(keypair: Keypair, completion: @escaping @Sendable () -> Void) { + + fileprivate func removeCurrentToken(keypair: Keypair, completion: @escaping @Sendable () -> Void) { guard let token = getToken(), let encodedPayload = makeEncodedPayload( @@ -137,11 +138,11 @@ private extension AdamantPushNotificationsTokenService { action: .remove ) else { return completion() } - + setTokenToStorage(nil) - + let transaction = Atomic(nil) - + transaction.value = sendMessageToANS( keypair: keypair, encodedPayload: encodedPayload @@ -151,14 +152,14 @@ private extension AdamantPushNotificationsTokenService { self.addTokenDeletionTransaction(transaction) } } - - func makeEncodedPayload( + + fileprivate func makeEncodedPayload( token: String, keypair: Keypair, action: ANSPayload.Action ) -> EncodedPayload? { let payload = ANSPayload(token: token, provider: ansProvider, action: action) - + guard let data = try? JSONEncoder().encode(payload), let payload = String(data: data, encoding: .utf8), @@ -168,26 +169,29 @@ private extension AdamantPushNotificationsTokenService { privateKey: keypair.privateKey ) else { return nil } - + return encodedPayload } - + @discardableResult - func sendMessageToANS( + fileprivate func sendMessageToANS( keypair: Keypair, encodedPayload: EncodedPayload, completion: @escaping @Sendable (_ success: Bool) -> Void ) -> UnregisteredTransaction? { - guard let messageTransaction = try? adamantCore.makeSendMessageTransaction( - senderId: AdamantUtilities.generateAddress(publicKey: keypair.publicKey), - recipientId: AdamantResources.contacts.ansAddress, - keypair: keypair, - message: encodedPayload.message, - type: ChatType.signal, - nonce: encodedPayload.nonce, - amount: nil - ) else { return nil } - + guard + let messageTransaction = try? adamantCore.makeSendMessageTransaction( + senderId: AdamantUtilities.generateAddress(publicKey: keypair.publicKey), + recipientId: AdamantResources.contacts.ansAddress, + keypair: keypair, + message: encodedPayload.message, + type: ChatType.signal, + nonce: encodedPayload.nonce, + amount: nil, + date: AdmWalletService.correctedDate + ) + else { return nil } + Task { switch await apiService.sendMessageTransaction(transaction: messageTransaction) { case .success: @@ -196,48 +200,48 @@ private extension AdamantPushNotificationsTokenService { completion(false) } } - + return messageTransaction } } -// MARK: - SecuredStore +// MARK: - SecureStore + +extension AdamantPushNotificationsTokenService { + fileprivate func setTokenToStorage(_ token: String?) { + SecureStoreSemaphore.wait() + defer { SecureStoreSemaphore.signal() } -private extension AdamantPushNotificationsTokenService { - func setTokenToStorage(_ token: String?) { - securedStoreSemaphore.wait() - defer { securedStoreSemaphore.signal() } - if let token = token { - securedStore.set(token, for: StoreKey.PushNotificationsTokenService.token) + SecureStore.set(token, for: StoreKey.PushNotificationsTokenService.token) } else { - securedStore.remove(StoreKey.PushNotificationsTokenService.token) + SecureStore.remove(StoreKey.PushNotificationsTokenService.token) } } - - func getToken() -> String? { - securedStore.get(StoreKey.PushNotificationsTokenService.token) + + fileprivate func getToken() -> String? { + SecureStore.get(StoreKey.PushNotificationsTokenService.token) } - - func addTokenDeletionTransaction(_ transaction: UnregisteredTransaction) { - securedStoreSemaphore.wait() - defer { securedStoreSemaphore.signal() } - + + fileprivate func addTokenDeletionTransaction(_ transaction: UnregisteredTransaction) { + SecureStoreSemaphore.wait() + defer { SecureStoreSemaphore.signal() } + var transactions = getTokenDeletionTransactions() transactions.insert(transaction) - securedStore.set(transactions, for: StoreKey.PushNotificationsTokenService.tokenDeletionTransactions) + SecureStore.set(transactions, for: StoreKey.PushNotificationsTokenService.tokenDeletionTransactions) } - - func removeTokenDeletionTransaction(_ transaction: UnregisteredTransaction) { - securedStoreSemaphore.wait() - defer { securedStoreSemaphore.signal() } - + + fileprivate func removeTokenDeletionTransaction(_ transaction: UnregisteredTransaction) { + SecureStoreSemaphore.wait() + defer { SecureStoreSemaphore.signal() } + var transactions = getTokenDeletionTransactions() transactions.remove(transaction) - securedStore.set(transactions, for: StoreKey.PushNotificationsTokenService.tokenDeletionTransactions) + SecureStore.set(transactions, for: StoreKey.PushNotificationsTokenService.tokenDeletionTransactions) } - - func getTokenDeletionTransactions() -> Set { - securedStore.get(StoreKey.PushNotificationsTokenService.tokenDeletionTransactions) ?? .init() + + fileprivate func getTokenDeletionTransactions() -> Set { + SecureStore.get(StoreKey.PushNotificationsTokenService.tokenDeletionTransactions) ?? .init() } } diff --git a/Adamant/Services/AdamantReachability.swift b/Adamant/Services/AdamantReachability.swift index 393d415d4..300d726f5 100644 --- a/Adamant/Services/AdamantReachability.swift +++ b/Adamant/Services/AdamantReachability.swift @@ -6,30 +6,30 @@ // Copyright © 2018 Adamant. All rights reserved. // +import CommonKit import Foundation -import Reachability import Network -import CommonKit +import Reachability // MARK: - AdamantReachability wrapper final class AdamantReachability: ReachabilityMonitor, @unchecked Sendable { @ObservableValue private(set) var connection = true - + private let monitor = NWPathMonitor() - + var connectionPublisher: AnyObservable { $connection.eraseToAnyPublisher() } - + func start() { monitor.pathUpdateHandler = { [weak self] _ in guard let self = self else { return } self.updateConnection() - + let userInfo: [String: Any] = [ AdamantUserInfoKey.ReachabilityMonitor.connection: self.connection ] - + NotificationCenter.default.post( name: Notification.Name.AdamantReachabilityMonitor.reachabilityChanged, object: self, @@ -44,7 +44,7 @@ final class AdamantReachability: ReachabilityMonitor, @unchecked Sendable { func stop() { monitor.cancel() } - + private func updateConnection() { switch monitor.currentPath.status { case .satisfied: diff --git a/Adamant/Services/AdamantVisibleWalletsService.swift b/Adamant/Services/AdamantVisibleWalletsService.swift deleted file mode 100644 index ba483e48d..000000000 --- a/Adamant/Services/AdamantVisibleWalletsService.swift +++ /dev/null @@ -1,235 +0,0 @@ -// -// AdamantVisibleWalletsService.swift -// Adamant -// -// Created by Stanislav Jelezoglo on 16.12.2022. -// Copyright © 2022 Adamant. All rights reserved. -// - -import Foundation -import Combine -import CommonKit - -final class AdamantVisibleWalletsService: VisibleWalletsService, @unchecked Sendable { - - // MARK: Dependencies - let securedStore: SecuredStore - let accountService: AccountService - let walletsServiceCompose: WalletServiceCompose - - // MARK: Proprieties - - private enum Types { - case indexes - case visibility - - var path: String { - switch self { - case .indexes: return StoreKey.visibleWallets.useCustomIndexes - case .visibility: return StoreKey.visibleWallets.useCustomVisibility - } - } - } - - @Atomic private var invisibleWallets: [String] = [] - @Atomic private var indexesWallets: [String] = [] - @Atomic private var notificationsSet: Set = [] - - // MARK: Lifecycle - init( - securedStore: SecuredStore, - accountService: AccountService, - walletsServiceCompose: WalletServiceCompose - ) { - self.securedStore = securedStore - self.accountService = accountService - self.walletsServiceCompose = walletsServiceCompose - - NotificationCenter.default - .notifications(named: .AdamantAccountService.userLoggedOut) - .sink { [weak self] _ in - self?.userLoggedOut() - } - .store(in: ¬ificationsSet) - - NotificationCenter.default - .notifications(named: .AdamantAccountService.userLoggedIn) - .sink { [weak self] _ in - self?.userLoggedIn() - } - .store(in: ¬ificationsSet) - } - - // MARK: Notification actions - - private func userLoggedIn() { - invisibleWallets = getInvisibleWallets() - indexesWallets = getSortedWallets(includeInvisible: false) - - NotificationCenter.default.post( - name: Notification.Name.AdamantVisibleWalletsService.visibleWallets, - object: nil - ) - } - - private func userLoggedOut() { - securedStore.remove(StoreKey.visibleWallets.invisibleWallets) - securedStore.remove(StoreKey.visibleWallets.indexWallets) - securedStore.remove(StoreKey.visibleWallets.useCustomIndexes) - securedStore.remove(StoreKey.visibleWallets.useCustomVisibility) - invisibleWallets.removeAll() - indexesWallets.removeAll() - - NotificationCenter.default.post( - name: Notification.Name.AdamantVisibleWalletsService.visibleWallets, - object: nil - ) - } - - // MARK: Visible - - func addToInvisibleWallets(_ wallet: WalletCoreProtocol) { - var wallets = getInvisibleWallets() - wallets.append(wallet.tokenUnicID) - setInvisibleWallets(wallets) - } - - func removeFromInvisibleWallets(_ wallet: WalletCoreProtocol) { - var wallets = getInvisibleWallets() - guard let index = wallets.firstIndex(of: wallet.tokenUnicID) else { return } - wallets.remove(at: index) - setInvisibleWallets(wallets) - } - - func getInvisibleWallets() -> [String] { - guard isUseCustomFilter(for: .visibility) else { - let wallets = walletsServiceCompose.getWallets() - .filter { $0.core.defaultVisibility != true } - .map { $0.core.tokenUnicID } - return wallets - } - - guard let wallets: [String] = securedStore.get(StoreKey.visibleWallets.invisibleWallets) else { - return [] - } - return wallets - } - - func isInvisible(_ wallet: WalletCoreProtocol) -> Bool { - return invisibleWallets.contains(wallet.tokenUnicID) - } - - private func setInvisibleWallets(_ wallets: [String]) { - securedStore.set(wallets, for: StoreKey.visibleWallets.invisibleWallets) - setUseCustomFilter(for: .visibility, value: true) - invisibleWallets = getInvisibleWallets() - } - - // MARK: Index Positions - - func getIndexPosition(for wallet: WalletCoreProtocol) -> Int? { - return indexesWallets.firstIndex(of: wallet.tokenUnicID) - } - - func getSortedWallets(includeInvisible: Bool) -> [String] { - guard isUseCustomFilter(for: .indexes) else { - // Sort by default ordinal number - // Coins without an order are shown last, alphabetically - let wallets = walletsServiceCompose.getWallets().map { $0.core } - let walletsIV = includeInvisible - ? wallets - : wallets.filter { $0.defaultVisibility == true } - - var walletsWithIndexes = walletsIV - .filter { $0.defaultOrdinalLevel != nil } - .sorted(by: { $0.defaultOrdinalLevel! < $1.defaultOrdinalLevel! }) - let walletsWithNoIndexes = walletsIV - .filter { $0.defaultOrdinalLevel == nil } - .sorted(by: { $0.tokenName < $1.tokenName }) - - walletsWithIndexes.append(contentsOf: walletsWithNoIndexes) - - return walletsWithIndexes.map { $0.tokenUnicID } - } - - let path = !includeInvisible - ? StoreKey.visibleWallets.indexWallets - : StoreKey.visibleWallets.indexWalletsWithInvisible - - guard let indexes: [String] = securedStore.get(path) else { - return [] - } - return indexes - } - - func setIndexPositionWallets(_ wallets: [String], includeInvisible: Bool) { - let path = !includeInvisible - ? StoreKey.visibleWallets.indexWallets - : StoreKey.visibleWallets.indexWalletsWithInvisible - - securedStore.set(wallets, for: path) - indexesWallets = getSortedWallets(includeInvisible: false) - setUseCustomFilter(for: .indexes, value: true) - } - - func setIndexPositionWallets(_ wallets: [WalletCoreProtocol], includeInvisible: Bool) { - let wallets = includeInvisible - ? wallets - : wallets.filter { !isInvisible($0) } - - let walletsUnicsId = wallets.map { $0.tokenUnicID } - - setIndexPositionWallets(walletsUnicsId, includeInvisible: includeInvisible) - } - - func reset() { - setUseCustomFilter(for: .indexes, value: false) - setUseCustomFilter(for: .visibility, value: false) - indexesWallets = getSortedWallets(includeInvisible: false) - invisibleWallets = getInvisibleWallets() - NotificationCenter.default.post(name: Notification.Name.AdamantVisibleWalletsService.visibleWallets, object: nil) - } - - private func isUseCustomFilter(for type: Types) -> Bool { - guard let result: Bool = securedStore.get(type.path) else { - return false - } - return result - } - - private func setUseCustomFilter(for type: Types, value: Bool) { - securedStore.set(value, for: type.path) - } - - // MARK: - Sort by indexes - /* How it works: - 1. Get all unsorted wallets - 2. Get the sorted wallets from the database - 3. Shuffle the unsorted wallets (by removing a wallet from the array and inserting it at a certain position). - We can't use only point 2, because in the future we can add new tokens that won't be in the database - */ - func sorted(includeInvisible: Bool) -> [WalletService] { - let wallets = walletsServiceCompose.getWallets() - var availableServices = includeInvisible - ? wallets - : wallets.filter { !isInvisible($0.core) } - - for (newIndex, tokenUnicID) in getSortedWallets(includeInvisible: includeInvisible).enumerated() { - guard let index = availableServices.firstIndex( - where: { $0.core.tokenUnicID == tokenUnicID } - ) else { - continue - } - - let wallet = availableServices.remove(at: index) - - if availableServices.indices.contains(newIndex) { - availableServices.insert(wallet, at: newIndex) - } else { - availableServices.append(wallet) - } - } - - return availableServices - } -} diff --git a/Adamant/Services/AdamantVisibleWalletsService/AdamantVisibleWalletsService.swift b/Adamant/Services/AdamantVisibleWalletsService/AdamantVisibleWalletsService.swift new file mode 100644 index 000000000..8c18c222f --- /dev/null +++ b/Adamant/Services/AdamantVisibleWalletsService/AdamantVisibleWalletsService.swift @@ -0,0 +1,185 @@ +// +// AdamantVisibleWalletsService.swift +// Adamant +// +// Created by Stanislav Jelezoglo on 16.12.2022. +// Copyright © 2022 Adamant. All rights reserved. +// + +import Combine +import CommonKit +import Foundation + +final class AdamantVisibleWalletsService: VisibleWalletsService, @unchecked Sendable { + + // MARK: Dependencies + let SecureStore: SecureStore + let accountService: AccountService + let walletsServiceCompose: WalletServiceCompose + + // MARK: Proprieties + + private enum Types { + case indexes + case visibility + + var path: String { + switch self { + case .indexes: return StoreKey.visibleWallets.useCustomIndexes + case .visibility: return StoreKey.visibleWallets.useCustomVisibility + } + } + } + + @Atomic private var notificationsSet: Set = [] + + @ObservableValue private var state: AdamantVisibleWalletsServiceState + var statePublisher: AnyObservable { + $state.map { _ in }.eraseToAnyPublisher() + } + + // MARK: Lifecycle + init( + SecureStore: SecureStore, + accountService: AccountService, + walletsServiceCompose: WalletServiceCompose + ) { + self.SecureStore = SecureStore + self.accountService = accountService + self.walletsServiceCompose = walletsServiceCompose + self.state = .init() + + NotificationCenter.default + .notifications(named: .AdamantAccountService.userLoggedOut) + .sink { [weak self] _ in + self?.userLoggedOut() + } + .store(in: ¬ificationsSet) + + NotificationCenter.default + .notifications(named: .AdamantAccountService.userLoggedIn) + .sink { [weak self] _ in + self?.userLoggedIn() + } + .store(in: ¬ificationsSet) + } + + // MARK: Notification actions + + private func userLoggedIn() { + state.invisibleWallets = getInvisibleWallets() + state.indexesWallets = getSortedWallets(includeInvisible: false) + } + + private func userLoggedOut() { + SecureStore.remove(StoreKey.visibleWallets.invisibleWallets) + SecureStore.remove(StoreKey.visibleWallets.indexWallets) + SecureStore.remove(StoreKey.visibleWallets.useCustomIndexes) + SecureStore.remove(StoreKey.visibleWallets.useCustomVisibility) + state.invisibleWallets.removeAll() + state.indexesWallets.removeAll() + } + + // MARK: Visible + + func addToInvisibleWallets(_ walletTokenUniqueID: String) { + var wallets = getInvisibleWallets() + wallets.append(walletTokenUniqueID) + setInvisibleWallets(wallets) + } + + func removeFromInvisibleWallets(_ walletTokenUniqueID: String) { + var wallets = getInvisibleWallets() + guard let index = wallets.firstIndex(of: walletTokenUniqueID) else { return } + wallets.remove(at: index) + setInvisibleWallets(wallets) + } + + private func getInvisibleWallets() -> [String] { + guard isUseCustomFilter(for: .visibility) else { + let wallets = walletsServiceCompose.getWallets() + .filter { $0.core.defaultVisibility != true } + .map { $0.core.tokenUniqueID } + return wallets + } + + return SecureStore.get(StoreKey.visibleWallets.invisibleWallets) ?? [] + } + + func isInvisible(_ walletTokenUniqueID: String) -> Bool { + state.invisibleWallets.contains(walletTokenUniqueID) + } + + private func setInvisibleWallets(_ wallets: [String]) { + SecureStore.set(wallets, for: StoreKey.visibleWallets.invisibleWallets) + setUseCustomFilter(for: .visibility, value: true) + state.invisibleWallets = getInvisibleWallets() + } + + // MARK: Index Positions + + func getSortedWallets(includeInvisible: Bool) -> [String] { + guard isUseCustomFilter(for: .indexes) else { + // Sort by default ordinal number + // Coins without an order are shown last, alphabetically + let wallets = walletsServiceCompose.getWallets().map { $0.core } + let walletsIV = + includeInvisible + ? wallets + : wallets.filter { $0.defaultVisibility == true } + + var walletsWithIndexes = + walletsIV + .filter { $0.defaultOrdinalLevel != nil } + .sorted(by: { $0.defaultOrdinalLevel! < $1.defaultOrdinalLevel! }) + let walletsWithNoIndexes = + walletsIV + .filter { $0.defaultOrdinalLevel == nil } + .sorted(by: { $0.tokenName < $1.tokenName }) + + walletsWithIndexes.append(contentsOf: walletsWithNoIndexes) + + return walletsWithIndexes.map { $0.tokenUniqueID } + } + + let path = + !includeInvisible + ? StoreKey.visibleWallets.indexWallets + : StoreKey.visibleWallets.indexWalletsWithInvisible + + guard let indexes: [String] = SecureStore.get(path) else { + return [] + } + return indexes + } + + func setIndexPositionWallets(_ wallets: [String], includeInvisible: Bool) { + let path = + !includeInvisible + ? StoreKey.visibleWallets.indexWallets + : StoreKey.visibleWallets.indexWalletsWithInvisible + + SecureStore.set(wallets, for: path) + state.indexesWallets = getSortedWallets(includeInvisible: false) + setUseCustomFilter(for: .indexes, value: true) + + } + + func reset() { + setUseCustomFilter(for: .indexes, value: false) + setUseCustomFilter(for: .visibility, value: false) + state.indexesWallets = getSortedWallets(includeInvisible: false) + state.invisibleWallets = getInvisibleWallets() + } + + private func isUseCustomFilter(for type: Types) -> Bool { + guard let result: Bool = SecureStore.get(type.path) else { + return false + } + return result + } + + private func setUseCustomFilter(for type: Types, value: Bool) { + SecureStore.set(value, for: type.path) + } +} diff --git a/Adamant/Services/AdamantVisibleWalletsService/AdamantVisibleWalletsServiceState.swift b/Adamant/Services/AdamantVisibleWalletsService/AdamantVisibleWalletsServiceState.swift new file mode 100644 index 000000000..fa0707815 --- /dev/null +++ b/Adamant/Services/AdamantVisibleWalletsService/AdamantVisibleWalletsServiceState.swift @@ -0,0 +1,14 @@ +// +// AdamantVisibleWalletsServiceState.swift +// Adamant +// +// Created by Dmitrij Meidus on 20.03.25. +// Copyright © 2025 Adamant. All rights reserved. +// + +import CommonKit + +struct AdamantVisibleWalletsServiceState { + var indexesWallets: [String] = [] + var invisibleWallets: [String] = [] +} diff --git a/Adamant/Services/AdamantWalletsStoreService.swift b/Adamant/Services/AdamantWalletsStoreService.swift new file mode 100644 index 000000000..e764f202a --- /dev/null +++ b/Adamant/Services/AdamantWalletsStoreService.swift @@ -0,0 +1,58 @@ +// +// AdamantWalletsStoreService.swift +// Adamant +// +// Created by Dmitrij Meidus on 08.02.25. +// Copyright © 2025 Adamant. All rights reserved. +// + +struct AdamantWalletStoreService: WalletStoreServiceProtocol { + let visibleWalletsService: VisibleWalletsService + let walletServiceCompose: WalletServiceCompose + + init( + visibleWalletsService: VisibleWalletsService, + walletServiceCompose: WalletServiceCompose + ) { + self.visibleWalletsService = visibleWalletsService + self.walletServiceCompose = walletServiceCompose + } + + func isInvisible(_ wallet: WalletService) -> Bool { + visibleWalletsService.isInvisible(wallet.core.tokenUniqueID) + } + // MARK: - Sort by indexes + /* How it works: + 1. Get all unsorted wallets + 2. Get the sorted wallets from the database + 3. Shuffle the unsorted wallets (by removing a wallet from the array and inserting it at a certain position). + We can't use only point 2, because in the future we can add new tokens that won't be in the database + */ + func sorted(includeInvisible: Bool) -> [WalletService] { + let wallets = walletServiceCompose.getWallets() + var availableServices = + includeInvisible + ? wallets + : wallets.filter { !isInvisible($0) } + + for (newIndex, tokenUniqueID) in visibleWalletsService.getSortedWallets(includeInvisible: includeInvisible).enumerated() { + guard + let index = availableServices.firstIndex( + where: { $0.core.tokenUniqueID == tokenUniqueID } + ) + else { + continue + } + + let wallet = availableServices.remove(at: index) + + if availableServices.indices.contains(newIndex) { + availableServices.insert(wallet, at: newIndex) + } else { + availableServices.append(wallet) + } + } + + return availableServices + } +} diff --git a/Adamant/Services/AdamantDialogService.swift b/Adamant/Services/AdmDialogService/AdamantDialogService.swift similarity index 72% rename from Adamant/Services/AdamantDialogService.swift rename to Adamant/Services/AdmDialogService/AdamantDialogService.swift index 476a849be..7354984df 100644 --- a/Adamant/Services/AdamantDialogService.swift +++ b/Adamant/Services/AdmDialogService/AdamantDialogService.swift @@ -6,24 +6,23 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit +import AVFoundation +import CommonKit import MessageUI import PopupKit import SafariServices -import CommonKit -import AVFoundation +import UIKit @MainActor final class AdamantDialogService: DialogService { - // MARK: Dependencies private let notificationsService: NotificationsService private let vibroService: VibroService private let popupManager = PopupManager() private let mailDelegate = MailDelegate() - + private weak var window: UIWindow? - + init( vibroService: VibroService, notificationsService: NotificationsService @@ -31,7 +30,7 @@ final class AdamantDialogService: DialogService { self.vibroService = vibroService self.notificationsService = notificationsService } - + func setup(window: UIWindow) { self.window = window popupManager.setup() @@ -44,13 +43,13 @@ extension AdamantDialogService { viewController.modalPresentationStyle = .overFullScreen getTopmostViewController()?.present(viewController, animated: animated, completion: completion) } - + func getTopmostViewController() -> UIViewController? { if var topController = window?.rootViewController { if let tab = topController as? UITabBarController, let selected = tab.selectedViewController { topController = selected } - + if let nav = topController as? UINavigationController, let visible = nav.visibleViewController { if let presented = visible.presentedViewController { return presented @@ -58,14 +57,14 @@ extension AdamantDialogService { return visible } } - + while let presentedViewController = topController.presentedViewController { topController = presentedViewController } - + return topController } - + return nil } } @@ -75,7 +74,7 @@ extension AdamantDialogService { func showToastMessage(_ message: String) { popupManager.showToastMessage(message) } - + func dismissToast() { popupManager.dismissToast() } @@ -86,21 +85,21 @@ extension AdamantDialogService { func showProgress(withMessage message: String?, userInteractionEnable enabled: Bool) { popupManager.showProgressAlert(message: message, userInteractionEnabled: enabled) } - + func dismissProgress() { popupManager.dismissAlert() } - + func showSuccess(withMessage message: String?) { vibroService.applyVibration(.success) popupManager.showSuccessAlert(message: message) } - + func showWarning(withMessage message: String) { vibroService.applyVibration(.error) popupManager.showWarningAlert(message: message) } - + func showError(withMessage message: String, supportEmail: Bool, error: Error? = nil) { internalShowError( withMessage: message, @@ -110,7 +109,7 @@ extension AdamantDialogService { error: error ) } - + private func internalShowError( withMessage message: String, supportEmail: Bool, @@ -119,28 +118,30 @@ extension AdamantDialogService { error: Error? = nil ) { vibroService.applyVibration(.error) - popupManager.showAdvancedAlert(model: .init( - icon: icon ?? .init(), - title: title, - text: message, - secondaryButton: supportEmail - ? .init( - title: AdamantResources.supportEmail, - action: .init(id: .empty) { [weak self] in - self?.sendErrorEmail(errorDescription: message) - self?.popupManager.dismissAdvancedAlert() + popupManager.showAdvancedAlert( + model: .init( + icon: icon ?? .init(), + title: title, + text: message, + secondaryButton: supportEmail + ? .init( + title: AdamantResources.supportEmail, + action: .init(id: .empty) { [weak self] in + self?.sendErrorEmail(errorDescription: message) + self?.popupManager.dismissAdvancedAlert() + } + ) + : nil, + primaryButton: .init( + title: .adamant.alert.ok, + action: .init(id: .empty) { [weak popupManager] in + popupManager?.dismissAdvancedAlert() } ) - : nil, - primaryButton: .init( - title: .adamant.alert.ok, - action: .init(id: .empty) { [weak popupManager] in - popupManager?.dismissAdvancedAlert() - } ) - )) + ) } - + func showRichError(error: RichError) { switch error.level { case .warning: @@ -159,7 +160,7 @@ extension AdamantDialogService { ) } } - + func showRichError(error: Error) { if let error = error as? RichError { showRichError(error: error) @@ -171,7 +172,7 @@ extension AdamantDialogService { ) } } - + func showNoConnectionNotification() { popupManager.showNotification( icon: .asset(named: "error"), @@ -181,18 +182,18 @@ extension AdamantDialogService { tapHandler: nil ) } - + func dissmisNoConnectionNotification() { popupManager.dismissNotification() } - + private func sendErrorEmail(errorDescription: String) { let body = String( format: .adamant.alert.emailErrorMessageBody, errorDescription, AdamantUtilities.deviceInfo ) - + if let vc = getTopmostViewController() { vc.openEmailScreen( recipient: AdamantResources.supportEmail, @@ -214,7 +215,7 @@ extension AdamantDialogService { extension AdamantDialogService { func showNotification(title: String?, message: String?, image: UIImage?, tapHandler: (() -> Void)?) { guard notificationsService.inAppToasts else { return } - + popupManager.showNotification( icon: image, title: title, @@ -223,7 +224,7 @@ extension AdamantDialogService { tapHandler: tapHandler ) } - + func dismissNotification() { popupManager.dismissNotification() } @@ -241,25 +242,26 @@ extension AdamantDialogService { didSelect: ((AddressChatShareType) -> Void)? ) { let source: UIAlertController.SourceView? = from.map { .view($0) } - + let alert = UIAlertController( title: adm, message: nil, preferredStyleSafe: .actionSheet, source: source ) - + for type in types { alert.addAction( UIAlertAction(title: type.localized + name, style: .default) { _ in - didSelect?(type) - }) + didSelect?(type) + } + ) } - + let encodedAddress = AdamantUriTools.encode( request: AdamantUri.address(address: adm, params: nil) ) - + addActions( to: alert, stringForPasteboard: adm, @@ -268,20 +270,21 @@ extension AdamantDialogService { types: [ .copyToPasteboard, .share, - .generateQr(encodedContent: encodedAddress, - sharingTip: adm, - withLogo: true - ) + .generateQr( + encodedContent: encodedAddress, + sharingTip: adm, + withLogo: true + ) ], excludedActivityTypes: ShareContentType.address.excludedActivityTypes, from: source, completion: nil ) - + alert.modalPresentationStyle = .overFullScreen present(alert, animated: animated, completion: completion) } - + func presentShareAlertFor( string: String, types: [ShareType], @@ -304,38 +307,88 @@ extension AdamantDialogService { completion: completion, didSelect: didSelect ) - + alert.modalPresentationStyle = .overFullScreen present(alert, animated: animated, completion: completion) } - - func presentShareAlertFor(string: String, types: [ShareType], excludedActivityTypes: [UIActivity.ActivityType]?, animated: Bool, from: UIView?, completion: (() -> Void)?) { + + func presentShareAlertFor( + string: String, + types: [ShareType], + excludedActivityTypes: [UIActivity.ActivityType]?, + animated: Bool, + from: UIView?, + completion: (() -> Void)? + ) { let source: UIAlertController.SourceView? = from.map { .view($0) } - - let alert = createShareAlertFor(stringForPasteboard: string, stringForShare: string, stringForQR: string, types: types, excludedActivityTypes: excludedActivityTypes, animated: animated, from: source, completion: completion) - + + let alert = createShareAlertFor( + stringForPasteboard: string, + stringForShare: string, + stringForQR: string, + types: types, + excludedActivityTypes: excludedActivityTypes, + animated: animated, + from: source, + completion: completion + ) + alert.modalPresentationStyle = .overFullScreen present(alert, animated: animated, completion: completion) } - - func presentShareAlertFor(string: String, types: [ShareType], excludedActivityTypes: [UIActivity.ActivityType]?, animated: Bool, from: UIBarButtonItem?, completion: (() -> Void)?) { + + func presentShareAlertFor( + string: String, + types: [ShareType], + excludedActivityTypes: [UIActivity.ActivityType]?, + animated: Bool, + from: UIBarButtonItem?, + completion: (() -> Void)? + ) { let source: UIAlertController.SourceView? = from.map { .barButtonItem($0) } - - let alert = createShareAlertFor(stringForPasteboard: string, stringForShare: string, stringForQR: string, types: types, excludedActivityTypes: excludedActivityTypes, animated: animated, from: source, completion: completion) - + + let alert = createShareAlertFor( + stringForPasteboard: string, + stringForShare: string, + stringForQR: string, + types: types, + excludedActivityTypes: excludedActivityTypes, + animated: animated, + from: source, + completion: completion + ) + alert.modalPresentationStyle = .overFullScreen present(alert, animated: animated, completion: completion) } - - func presentShareAlertFor(stringForPasteboard: String, stringForShare: String, stringForQR: String, types: [ShareType], excludedActivityTypes: [UIActivity.ActivityType]?, animated: Bool, from: UIView?, completion: (() -> Void)?) { + + func presentShareAlertFor( + stringForPasteboard: String, + stringForShare: String, + stringForQR: String, + types: [ShareType], + excludedActivityTypes: [UIActivity.ActivityType]?, + animated: Bool, + from: UIView?, + completion: (() -> Void)? + ) { let source: UIAlertController.SourceView? = from.map { .view($0) } - - let alert = createShareAlertFor(stringForPasteboard: stringForPasteboard, stringForShare: stringForShare, stringForQR: stringForQR, types: types, excludedActivityTypes: excludedActivityTypes, animated: animated, from: source, completion: completion) - + + let alert = createShareAlertFor( + stringForPasteboard: stringForPasteboard, + stringForShare: stringForShare, + stringForQR: stringForQR, + types: types, + excludedActivityTypes: excludedActivityTypes, + animated: animated, + from: source, + completion: completion + ) + alert.modalPresentationStyle = .overFullScreen present(alert, animated: animated, completion: completion) } - + private func createShareAlertFor( stringForPasteboard: String, stringForShare: String, @@ -365,10 +418,10 @@ extension AdamantDialogService { completion: completion, didSelect: didSelect ) - + return alert } - + private func addActions( to alert: UIAlertController, stringForPasteboard: String, @@ -383,99 +436,105 @@ extension AdamantDialogService { for type in types { switch type { case .copyToPasteboard: - alert.addAction(UIAlertAction(title: type.localized , style: .default) { [weak self] _ in - UIPasteboard.general.string = stringForPasteboard - self?.showToastMessage(String.adamant.alert.copiedToPasteboardNotification) - didSelect?(.copyToPasteboard) - }) - + alert.addAction( + UIAlertAction(title: type.localized, style: .default) { [weak self] _ in + UIPasteboard.general.string = stringForPasteboard + self?.showToastMessage(String.adamant.alert.copiedToPasteboardNotification) + didSelect?(.copyToPasteboard) + } + ) + case .share: - alert.addAction(UIAlertAction(title: type.localized, style: .default) { [weak self] _ in - didSelect?(.share) - let vc = UIActivityViewController(activityItems: [stringForShare], applicationActivities: nil) - vc.excludedActivityTypes = excludedActivityTypes - - switch from { - case .view(let view)?: - vc.popoverPresentationController?.sourceView = view - vc.popoverPresentationController?.sourceRect = view.bounds - vc.popoverPresentationController?.canOverlapSourceViewRect = false - - case .barButtonItem(let item)?: - vc.popoverPresentationController?.barButtonItem = item - - default: - if UIDevice.current.userInterfaceIdiom == .pad { - vc.popoverPresentationController?.sourceView = alert.view - vc.popoverPresentationController?.sourceRect = alert.view.bounds + alert.addAction( + UIAlertAction(title: type.localized, style: .default) { [weak self] _ in + didSelect?(.share) + let vc = UIActivityViewController(activityItems: [stringForShare], applicationActivities: nil) + vc.excludedActivityTypes = excludedActivityTypes + + switch from { + case .view(let view)?: + vc.popoverPresentationController?.sourceView = view + vc.popoverPresentationController?.sourceRect = view.bounds vc.popoverPresentationController?.canOverlapSourceViewRect = false + + case .barButtonItem(let item)?: + vc.popoverPresentationController?.barButtonItem = item + + default: + if UIDevice.current.userInterfaceIdiom == .pad { + vc.popoverPresentationController?.sourceView = alert.view + vc.popoverPresentationController?.sourceRect = alert.view.bounds + vc.popoverPresentationController?.canOverlapSourceViewRect = false + } } + vc.modalPresentationStyle = .overFullScreen + self?.present(vc, animated: true, completion: completion) } - vc.modalPresentationStyle = .overFullScreen - self?.present(vc, animated: true, completion: completion) - }) - + ) + case .generateQr(let encodedContent, let sharingTip, let withLogo): - alert.addAction(UIAlertAction(title: type.localized, style: .default) { [weak self] _ in - guard let self = self else { return } - - didSelect?(.generateQr(encodedContent: encodedContent, sharingTip: sharingTip, withLogo: withLogo)) - - switch AdamantQRTools.generateQrFrom( - string: encodedContent ?? stringForQR, - withLogo: withLogo - ) { - case .success(let qr): - let vc = ShareQrViewController(dialogService: self) - vc.qrCode = qr - vc.sharingTip = sharingTip - vc.excludedActivityTypes = excludedActivityTypes - vc.modalPresentationStyle = .overFullScreen - present(vc, animated: true, completion: completion) - - case .failure(error: let error): - showError( - withMessage: error.localizedDescription, - supportEmail: true, - error: error - ) + alert.addAction( + UIAlertAction(title: type.localized, style: .default) { [weak self] _ in + guard let self = self else { return } + + didSelect?(.generateQr(encodedContent: encodedContent, sharingTip: sharingTip, withLogo: withLogo)) + + switch AdamantQRTools.generateQrFrom( + string: encodedContent ?? stringForQR, + withLogo: withLogo + ) { + case .success(let qr): + let vc = ShareQrViewController(dialogService: self) + vc.qrCode = qr + vc.sharingTip = sharingTip + vc.excludedActivityTypes = excludedActivityTypes + vc.modalPresentationStyle = .overFullScreen + present(vc, animated: true, completion: completion) + + case .failure(let error): + showError( + withMessage: error.localizedDescription, + supportEmail: true, + error: error + ) + } } - }) + ) case .openInExplorer(let url): let action = UIAlertAction( title: String.adamant.alert.openInExplorer, style: .default ) { [weak self] _ in didSelect?(.openInExplorer(url: url)) - + let safari = SFSafariViewController(url: url) safari.preferredControlTintColor = UIColor.adamant.primary safari.modalPresentationStyle = .overFullScreen self?.present(safari, animated: true, completion: completion) } - + alert.addAction(action) - + case .saveToPhotolibrary(let image): let action = UIAlertAction(title: type.localized, style: .default) { [weak self] _ in didSelect?(.saveToPhotolibrary(image: image)) UIImageWriteToSavedPhotosAlbum(image, self, #selector(self?.image(_:didFinishSavingWithError:contextInfo:)), nil) } - + alert.addAction(action) - + default: let action = UIAlertAction(title: type.localized, style: .default) { [didSelect] _ in didSelect?(type) } - + alert.addAction(action) } } - + alert.addAction(UIAlertAction(title: String.adamant.alert.cancel, style: .cancel, handler: nil)) } - + func presentDummyAlert( for adm: String, from: UIView?, @@ -490,7 +549,7 @@ extension AdamantDialogService { sendCompletion: sendCompletion ) } - + func presentDummyChatAlert( for adm: String, from: UIView?, @@ -505,7 +564,7 @@ extension AdamantDialogService { sendCompletion: sendCompletion ) } - + func presentDummyAlert( for adm: String, from: UIView?, @@ -521,19 +580,20 @@ extension AdamantDialogService { preferredStyleSafe: .alert, source: from.map { .view($0) } ) - + if let url = URL(string: NewChatViewController.faqUrl) { let faq = UIAlertAction( title: String.adamant.newChat.whatDoesItMean, - style: UIAlertAction.Style.default) { [weak self] _ in - let safari = SFSafariViewController(url: url) - safari.preferredControlTintColor = UIColor.adamant.primary - safari.modalPresentationStyle = .overFullScreen - self?.present(safari, animated: true, completion: nil) - } + style: UIAlertAction.Style.default + ) { [weak self] _ in + let safari = SFSafariViewController(url: url) + safari.preferredControlTintColor = UIColor.adamant.primary + safari.modalPresentationStyle = .overFullScreen + self?.present(safari, animated: true, completion: nil) + } alert.addAction(faq) } - + if canSend { let send = UIAlertAction( title: String.adamant.transfer.send, @@ -542,18 +602,18 @@ extension AdamantDialogService { ) alert.addAction(send) } - + let cancel = UIAlertAction( title: String.adamant.alert.cancel, style: .cancel, handler: nil ) - + alert.addAction(cancel) alert.modalPresentationStyle = .overFullScreen present(alert, animated: true, completion: nil) } - + @objc private func image(_ image: UIImage, didFinishSavingWithError error: NSError?, contextInfo: UnsafeRawPointer) { if let error = error { showError(withMessage: error.localizedDescription, supportEmail: true) @@ -561,7 +621,7 @@ extension AdamantDialogService { showSuccess(withMessage: String.adamant.alert.done) } } - + func presentGoToSettingsAlert(title: String?, message: String?) { let alert = UIAlertController( title: title, @@ -569,22 +629,24 @@ extension AdamantDialogService { preferredStyleSafe: .alert, source: nil ) - - alert.addAction(UIAlertAction(title: String.adamant.alert.settings, style: .default) { _ in - if let settingsURL = URL(string: UIApplication.openSettingsURLString) { - UIApplication.shared.open(settingsURL, options: [:], completionHandler: nil) + + alert.addAction( + UIAlertAction(title: String.adamant.alert.settings, style: .default) { _ in + if let settingsURL = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(settingsURL, options: [:], completionHandler: nil) + } } - }) - + ) + alert.addAction(UIAlertAction(title: String.adamant.alert.cancel, style: .cancel, handler: nil)) alert.modalPresentationStyle = .overFullScreen - + present(alert, animated: true, completion: nil) } } -fileprivate extension AdamantAlertStyle { - func asUIAlertControllerStyle() -> UIAlertController.Style { +extension AdamantAlertStyle { + fileprivate func asUIAlertControllerStyle() -> UIAlertController.Style { switch self { case .alert: return .alert @@ -594,9 +656,9 @@ fileprivate extension AdamantAlertStyle { } } -fileprivate extension AdamantAlertAction { +extension AdamantAlertAction { @MainActor - func asUIAlertAction() -> UIAlertAction { + fileprivate func asUIAlertAction() -> UIAlertAction { let handler = self.handler return UIAlertAction(title: self.title, style: self.style, handler: { _ in handler?() }) } @@ -609,14 +671,14 @@ extension AdamantDialogService { let uiStyle = style.asUIAlertControllerStyle() if let actions = actions { let uiActions: [UIAlertAction] = actions.map { $0.asUIAlertAction() } - + showAlert(title: title, message: message, style: uiStyle, actions: uiActions, from: from) } else { showAlert(title: title, message: message, style: uiStyle, actions: nil, from: from) } } } - + func showAlert(title: String?, message: String?, style: UIAlertController.Style, actions: [UIAlertAction]?, from: UIAlertController.SourceView?) { let alert = UIAlertController( title: title, @@ -624,7 +686,7 @@ extension AdamantDialogService { preferredStyleSafe: style, source: from ) - + if let actions = actions { for action in actions { alert.addAction(action) @@ -632,7 +694,7 @@ extension AdamantDialogService { } else { alert.addAction(UIAlertAction(title: String.adamant.alert.ok, style: .default)) } - + present(alert, animated: true, completion: nil) } } diff --git a/Adamant/Services/AdmDialogService/AdmDialogService+Extension.swift b/Adamant/Services/AdmDialogService/AdmDialogService+Extension.swift new file mode 100644 index 000000000..6fac81259 --- /dev/null +++ b/Adamant/Services/AdmDialogService/AdmDialogService+Extension.swift @@ -0,0 +1,134 @@ +import SafariServices +// +// AdmDialogServiceExtent.swift +// Adamant +// +// Created by Владимир Клевцов on 25.1.25.. +// Copyright © 2025 Adamant. All rights reserved. +// +import UIKit + +extension AdamantDialogService { + @MainActor + func makeRenameAlert( + titleFormat: String, + initialText: String?, + isEnoughMoney: Bool, + url: String?, + showVC: @escaping () -> Void, + onRename: @escaping (String) -> Void + ) -> UIAlertController { + let alert = UIAlertController( + title: titleFormat, + message: nil, + preferredStyle: .alert + ) + alert.addTextField { textField in + textField.placeholder = .adamant.chat.name + textField.autocapitalizationType = .words + textField.text = initialText + } + + let renameAction = UIAlertAction( + title: .adamant.chat.rename, + style: .default + ) { _ in + guard + let textField = alert.textFields?.first, + let newName = textField.text, + !newName.isEmpty + else { return } + + onRename(newName) + if !(isEnoughMoney) { + self.showFreeTokenAlert(url: url, type: .contacts, showVC: showVC) + } + } + + alert.addAction(renameAction) + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + alert.modalPresentationStyle = .overFullScreen + return alert + } + + @MainActor + func showFreeTokenAlert(url: String?, type: FreeTokensAlertType, showVC: @escaping () -> Void) { + let window = UIWindow(frame: UIScreen.main.bounds) + window.windowLevel = .alert + 1 + let rootViewController = UIViewController() + rootViewController.view.backgroundColor = .clear + window.rootViewController = rootViewController + window.makeKeyAndVisible() + + let alert = UIAlertController( + title: type.alertTitle, + message: .adamant.chat.freeTokensMessage, + preferredStyle: .alert + ) + + alert.addAction(makeFreeTokensAlertAction(url: url, window: window)) + alert.addAction(makeBuyTokensAction(action: showVC)) + alert.addAction(makeCancelAction(window: window)) + alert.modalPresentationStyle = .overFullScreen + + rootViewController.present(alert, animated: true, completion: nil) + } + +} +extension AdamantDialogService { + fileprivate func makeFreeTokensAlertAction(url: String?, window: UIWindow) -> UIAlertAction { + let action = UIAlertAction( + title: .adamant.chat.freeTokens, + style: .destructive + ) { _ in + guard let url = self.freeTokensURL(url: url) else { return } + let safari = SFSafariViewController(url: url) + safari.preferredControlTintColor = UIColor.adamant.primary + safari.modalPresentationStyle = .overFullScreen + + window.rootViewController?.present(safari, animated: true) + } + return action + } + fileprivate func makeBuyTokensAction(action: @escaping () -> Void) -> UIAlertAction { + .init( + title: .adamant.chat.freeTokensBuyADM, + style: .default + ) { _ in + action() + } + } + fileprivate func makeCancelAction(window: UIWindow) -> UIAlertAction { + .init( + title: .adamant.alert.cancel, + style: .default + ) { _ in + window.isHidden = true + } + } + fileprivate func freeTokensURL(url: String?) -> URL? { + guard let url = url else { + return nil + } + let urlString: String = .adamant.wallets.getFreeTokensUrl(for: url) + let tokenUrl = URL(string: urlString) + + return tokenUrl + } +} +enum FreeTokensAlertType { + case contacts + case message + case notification + + var alertTitle: String { + switch self { + case .contacts: + return .adamant.chat.freeTokensTitleBook + case .message: + return .adamant.chat.freeTokensTitleChat + case .notification: + return .adamant.chat.freeTokensTitleNotification + } + } +} diff --git a/Adamant/Services/ApiServiceCompose.swift b/Adamant/Services/ApiServiceCompose.swift index b64f9a84a..fc2c2369f 100644 --- a/Adamant/Services/ApiServiceCompose.swift +++ b/Adamant/Services/ApiServiceCompose.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation struct ApiServiceCompose: ApiServiceComposeProtocol { let btc: ApiServiceProtocol @@ -19,14 +19,14 @@ struct ApiServiceCompose: ApiServiceComposeProtocol { let adm: ApiServiceProtocol let ipfs: ApiServiceProtocol let infoService: ApiServiceProtocol - + func get(_ group: NodeGroup) -> ApiServiceProtocol? { getApiService(group: group) } } -private extension ApiServiceCompose { - func getApiService(group: NodeGroup) -> ApiServiceProtocol { +extension ApiServiceCompose { + fileprivate func getApiService(group: NodeGroup) -> ApiServiceProtocol { switch group { case .btc: return btc diff --git a/Adamant/Services/DataProviders/AdamantAccountsProvider.swift b/Adamant/Services/DataProviders/AdamantAccountsProvider.swift index cd388de7b..0536b5dda 100644 --- a/Adamant/Services/DataProviders/AdamantAccountsProvider.swift +++ b/Adamant/Services/DataProviders/AdamantAccountsProvider.swift @@ -6,10 +6,10 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation -@preconcurrency import CoreData -import CommonKit import Combine +import CommonKit +@preconcurrency import CoreData +import Foundation // MARK: - Provider @MainActor @@ -22,7 +22,7 @@ final class AdamantAccountsProvider: AccountsProvider { let isHidden: Bool let isSystem: Bool let publicKey: String? - + fileprivate init(contact: AdamantContacts) { self.address = contact.address self.name = contact.name @@ -33,22 +33,22 @@ final class AdamantAccountsProvider: AccountsProvider { self.publicKey = contact.publicKey } } - + private enum GetAccountResult { case core(CoreDataAccount) case dummy(DummyAccount) case notFound } - + // MARK: Dependencies @MainActor private let stack: CoreDataStack private let apiService: AdamantApiServiceProtocol private let addressBookService: AddressBookService - + // MARK: Properties - private let knownContacts: [String:KnownContact] + private let knownContacts: [String: KnownContact] private var subscriptions = Set() - + // MARK: Lifecycle init( stack: CoreDataStack, @@ -58,19 +58,19 @@ final class AdamantAccountsProvider: AccountsProvider { self.stack = stack self.apiService = apiService self.addressBookService = addressBookService - + let ico = KnownContact(contact: AdamantContacts.adamantIco) let bounty = KnownContact(contact: AdamantContacts.adamantBountyWallet) let welcome = KnownContact(contact: AdamantContacts.adamantWelcomeWallet) let newBounty = KnownContact(contact: AdamantContacts.adamantNewBountyWallet) let adamantSupport = KnownContact(contact: AdamantContacts.adamantSupport) - + let adamantExchange = KnownContact(contact: AdamantContacts.adamantExchange) let betOnBitcoin = KnownContact(contact: AdamantContacts.betOnBitcoin) let adelina = KnownContact(contact: AdamantContacts.adelina) - + let donate = KnownContact(contact: AdamantContacts.donate) - + self.knownContacts = [ AdamantContacts.adamantIco.address: ico, AdamantContacts.adamantIco.name: ico, @@ -78,22 +78,22 @@ final class AdamantAccountsProvider: AccountsProvider { AdamantContacts.adamantBountyWallet.name: bounty, AdamantContacts.adamantNewBountyWallet.address: newBounty, AdamantContacts.adamantSupport.address: adamantSupport, - + AdamantContacts.adamantExchange.address: adamantExchange, AdamantContacts.adamantExchange.name: adamantExchange, - + AdamantContacts.betOnBitcoin.address: betOnBitcoin, AdamantContacts.betOnBitcoin.name: betOnBitcoin, - + AdamantContacts.donate.address: donate, - + AdamantContacts.adamantWelcomeWallet.address: welcome, AdamantContacts.adamantWelcomeWallet.name: welcome, - + AdamantContacts.adelina.address: adelina, AdamantContacts.adelina.name: adelina ] - + NotificationCenter.default.addObserver( forName: Notification.Name.AdamantAddressBookService.addressBookUpdated, object: nil, @@ -101,51 +101,52 @@ final class AdamantAccountsProvider: AccountsProvider { ) { [weak self] notification in MainActor.assumeIsolatedSafe { guard let changes = notification.userInfo?[AdamantUserInfoKey.AddressBook.changes] as? [AddressBookChange], - let viewContext = self?.stack.container.viewContext else { + let viewContext = self?.stack.container.viewContext + else { return } - + let requestSingle = NSFetchRequest(entityName: CoreDataAccount.entityName) requestSingle.fetchLimit = 1 - + // Process changes for change in changes { switch change { case .newName(let address, let name), .updated(let address, let name): let predicate = NSPredicate(format: "address == %@", address) requestSingle.predicate = predicate - + guard let result = try? viewContext.fetch(requestSingle), let account = result.first else { continue } - + account.name = name account.chatroom?.title = name - + case .removed(let address): let predicate = NSPredicate(format: "address == %@", address) requestSingle.predicate = predicate - + guard let result = try? viewContext.fetch(requestSingle), let account = result.first else { continue } - + account.name = nil account.chatroom?.title = nil } } - + if viewContext.hasChanges { try? viewContext.save() } } } - + addObservers() } - + private func addObservers() { NotificationCenter.default .notifications(named: .LanguageStorageService.languageUpdated) @@ -154,7 +155,7 @@ final class AdamantAccountsProvider: AccountsProvider { } .store(in: &subscriptions) } - + private func updateSystemAccountsName() async { for account in AdamantContacts.allCases { let accountInDataBase = try? await getAccount(byAddress: account.address) @@ -163,7 +164,7 @@ final class AdamantAccountsProvider: AccountsProvider { accountInDataBase?.chatroom?.title = newName } } - + private func getAccount( byPredicate predicate: NSPredicate, context: NSManagedObjectContext? = nil @@ -171,9 +172,9 @@ final class AdamantAccountsProvider: AccountsProvider { let request = NSFetchRequest(entityName: BaseAccount.baseEntityName) request.fetchLimit = 1 request.predicate = predicate - + var acc: BaseAccount? - + if let context = context { // viewContext only on MainThread if context == stack.container.viewContext { @@ -189,14 +190,14 @@ final class AdamantAccountsProvider: AccountsProvider { acc = (try? stack.container.viewContext.fetch(request))?.first } } - + switch acc { case let core as CoreDataAccount: return .core(core) - + case let dummy as DummyAccount: return .dummy(dummy) - + case .some, nil: return .notFound } @@ -209,28 +210,28 @@ extension AdamantAccountsProvider { /// /// - Parameter address: account's address /// - Returns: do have acccount, or not - + func hasAccount(address: String) async -> Bool { let account = getAccount(byPredicate: NSPredicate(format: "address == %@", address)) - + switch account { case .core, .dummy: return true case .notFound: return false } } - + /// Get account info from server. /// /// - Parameters: /// - address: address of an account /// - completion: returns Account created in viewContext - + func getAccount(byAddress address: String) async throws -> CoreDataAccount { let validation = AdamantUtilities.validateAdamantAddress(address: address) if validation == .invalid { throw AccountsProviderError.invalidAddress(address: address) } - + // Check if there is an account, that we are looking for let dummy: DummyAccount? switch getAccount(byPredicate: NSPredicate(format: "address == %@", address)) { @@ -241,7 +242,7 @@ extension AdamantAccountsProvider { case .notFound: dummy = nil } - + switch validation { case .valid: do { @@ -254,16 +255,16 @@ extension AdamantAccountsProvider { dummy: dummy, in: stack.container.viewContext ) - + return coreAccount } - + let coreAccount = createAndSaveCoreDataAccount( from: account, dummy: dummy, in: stack.container.viewContext ) - + return coreAccount } catch let error { switch error { @@ -273,10 +274,10 @@ extension AdamantAccountsProvider { } else { throw AccountsProviderError.notFound(address: address) } - + case .networkError(let error): throw AccountsProviderError.networkError(error) - + default: throw AccountsProviderError.serverError(error) } @@ -288,14 +289,14 @@ extension AdamantAccountsProvider { throw AccountsProviderError.invalidAddress(address: address) } } - + /// Get account info from server or create instantly /// /// - Parameters: /// - address: address of an account /// - publicKey: publicKey of an account /// - completion: returns Account created in viewContext - + func getAccount( byAddress address: String, publicKey: String @@ -304,13 +305,13 @@ extension AdamantAccountsProvider { if validation == .invalid { throw AccountsProviderError.invalidAddress(address: address) } - + if publicKey.isEmpty { return try await getAccount(byAddress: address) } - + let context = stack.container.viewContext - + // Check if there is an account, that we are looking for let dummy: DummyAccount? switch getAccount(byPredicate: NSPredicate(format: "address == %@", address)) { @@ -321,7 +322,7 @@ extension AdamantAccountsProvider { case .notFound: dummy = nil } - + switch validation { case .valid: return await withUnsafeContinuation { @MainActor contituation in @@ -331,7 +332,7 @@ extension AdamantAccountsProvider { dummy: dummy, in: context ) - + contituation.resume(returning: account) } case .system: @@ -341,7 +342,7 @@ extension AdamantAccountsProvider { throw AccountsProviderError.invalidAddress(address: address) } } - + private func createAndSaveCoreDataAccount( for address: String, publicKey: String, @@ -352,20 +353,20 @@ extension AdamantAccountsProvider { if case .core(let account) = result { return account } - + let coreAccount = createCoreDataAccountIfNeeded( with: address, publicKey: publicKey, context: context ) - + if let dummy = dummy { coreAccount.name = dummy.name - + if let transfers = dummy.transfers { dummy.removeFromTransfers(transfers) coreAccount.addToTransfers(transfers) - + if let chatroom = coreAccount.chatroom { chatroom.addToTransactions(transfers) chatroom.updateLastTransaction() @@ -373,11 +374,11 @@ extension AdamantAccountsProvider { } context.delete(dummy) } - + try? context.save() return coreAccount } - + private func createAndSaveCoreDataAccount( from account: AdamantAccount, dummy: DummyAccount?, @@ -387,18 +388,18 @@ extension AdamantAccountsProvider { if case .core(let account) = result { return account } - + let coreAccount = createCoreDataAccount(from: account, context: context) - + coreAccount.isDummy = account.isDummy - + if let dummy = dummy { coreAccount.name = dummy.name - + if let transfers = dummy.transfers { dummy.removeFromTransfers(transfers) coreAccount.addToTransfers(transfers) - + if let chatroom = coreAccount.chatroom { chatroom.addToTransactions(transfers) chatroom.updateLastTransaction() @@ -406,15 +407,15 @@ extension AdamantAccountsProvider { } context.delete(dummy) } - + try? context.save() return coreAccount } - + /* - + Запросы на аккаунт по publicId и запросы на аккаунт по address надо взаимно согласовывать. Иначе может случиться такое, что разные службы запросят один и тот же аккаунт через разные методы - он будет добавлен дважды. - + /// Get account info from servier. /// /// - Parameters: @@ -424,28 +425,28 @@ extension AdamantAccountsProvider { // Go background, to not to hang threads (especially main) on semaphores and dispatch groups queue.async { self.groupsSemaphore.wait() - + // If there is already request for a this address, wait if let group = self.requestGroups[publicKey] { self.groupsSemaphore.signal() group.wait() self.groupsSemaphore.wait() } - - + + // Check account if let account = self.getAccount(byPredicate: NSPredicate(format: "publicKey == %@", publicKey)) { self.groupsSemaphore.signal() completion(.success(account)) return } - + // Not found, maybe on server? let group = DispatchGroup() self.requestGroups[publicKey] = group group.enter() self.groupsSemaphore.signal() - + self.apiService.getAccount(byPublicKey: publicKey) { result in defer { self.groupsSemaphore.wait() @@ -453,17 +454,17 @@ extension AdamantAccountsProvider { self.groupsSemaphore.signal() group.leave() } - + switch result { case .success(let account): let coreAccount = self.createCoreDataAccount(from: account) completion(.success(coreAccount)) - + case .failure(let error): switch error { case .accountNotFound: completion(.notFound) - + default: completion(.serverError(error)) } @@ -473,7 +474,7 @@ extension AdamantAccountsProvider { } */ - + private func createCoreDataAccountIfNeeded( with address: String, publicKey: String, @@ -483,7 +484,7 @@ extension AdamantAccountsProvider { if case .core(let account) = result { return account } - + let coreAccount = createCoreDataAccount( with: address, publicKey: publicKey, @@ -491,7 +492,7 @@ extension AdamantAccountsProvider { ) return coreAccount } - + private func createCoreDataAccount( with address: String, publicKey: String @@ -500,7 +501,7 @@ extension AdamantAccountsProvider { if case .core(let account) = result { return account } - + let coreAccount = createCoreDataAccount( with: address, publicKey: publicKey, @@ -508,11 +509,11 @@ extension AdamantAccountsProvider { ) return coreAccount } - + private func createCoreDataAccount(from account: AdamantAccount) -> CoreDataAccount { createCoreDataAccount(from: account, context: stack.container.viewContext) } - + private func createCoreDataAccount( from account: AdamantAccount, context: NSManagedObjectContext @@ -521,16 +522,16 @@ extension AdamantAccountsProvider { if case .core(let account) = result { return account } - + let coreAccount = CoreDataAccount(entity: CoreDataAccount.entity(), insertInto: context) coreAccount.address = account.address coreAccount.publicKey = account.publicKey - + let chatroom = Chatroom(entity: Chatroom.entity(), insertInto: context) chatroom.updatedAt = NSDate() - + coreAccount.chatroom = chatroom - + if let acc = knownContacts[account.address] { coreAccount.name = acc.name coreAccount.avatar = acc.avatar @@ -540,16 +541,17 @@ extension AdamantAccountsProvider { chatroom.isHidden = acc.isHidden chatroom.title = acc.name } - + if let address = coreAccount.address, - let name = addressBookService.getName(for: address) { + let name = addressBookService.getName(for: address) + { coreAccount.name = name chatroom.title = name.checkAndReplaceSystemWallets() } - + return coreAccount } - + private func createCoreDataAccount( with address: String, publicKey: String, @@ -558,12 +560,12 @@ extension AdamantAccountsProvider { let coreAccount = CoreDataAccount(entity: CoreDataAccount.entity(), insertInto: context) coreAccount.address = address coreAccount.publicKey = publicKey - + let chatroom = Chatroom(entity: Chatroom.entity(), insertInto: context) chatroom.updatedAt = NSDate() - + coreAccount.chatroom = chatroom - + if let acc = knownContacts[address] { coreAccount.name = acc.name coreAccount.avatar = acc.avatar @@ -574,33 +576,33 @@ extension AdamantAccountsProvider { chatroom.isReadonly = acc.isReadonly chatroom.title = acc.name } - + return coreAccount } } // MARK: - Dummy extension AdamantAccountsProvider { - + func getDummyAccount(for address: String) async throws -> DummyAccount { let validation = AdamantUtilities.validateAdamantAddress(address: address) if validation == .invalid { throw AccountsProviderDummyAccountError.invalidAddress(address: address) } - + let context = stack.container.viewContext - + switch getAccount(byPredicate: NSPredicate(format: "address == %@", address)) { case .core(let account): throw AccountsProviderDummyAccountError.foundRealAccount(account) - + case .dummy(let account): return account - + case .notFound: let dummy = DummyAccount(entity: DummyAccount.entity(), insertInto: context) dummy.address = address - + do { try context.save() return dummy diff --git a/Adamant/Services/DataProviders/AdamantChatTransactionService.swift b/Adamant/Services/DataProviders/AdamantChatTransactionService.swift index af0a86d58..9d3abcd1a 100644 --- a/Adamant/Services/DataProviders/AdamantChatTransactionService.swift +++ b/Adamant/Services/DataProviders/AdamantChatTransactionService.swift @@ -6,39 +6,39 @@ // Copyright © 2022 Adamant. All rights reserved. // -import Foundation +import CommonKit import CoreData -import UIKit +import Foundation import MarkdownKit -import CommonKit +import UIKit actor AdamantChatTransactionService: ChatTransactionService { - + // MARK: Dependencies - + private let adamantCore: AdamantCore private let walletServiceCompose: WalletServiceCompose - + private let markdownParser = MarkdownParser(font: UIFont.systemFont(ofSize: UIFont.systemFontSize)) - + private lazy var queue: OperationQueue = { let queue = OperationQueue() queue.maxConcurrentOperationCount = 1 return queue }() - + // MARK: Lifecycle - + init(adamantCore: AdamantCore, walletServiceCompose: WalletServiceCompose) { self.adamantCore = adamantCore self.walletServiceCompose = walletServiceCompose } - + /// Make operations serial func addOperations(_ op: Operation) { queue.addOperation(op) } - + /// Search transaction in local storage /// /// - Parameter id: Transacton ID @@ -47,7 +47,7 @@ actor AdamantChatTransactionService: ChatTransactionService { let request = NSFetchRequest(entityName: TransferTransaction.entityName) request.predicate = NSPredicate(format: "transactionId == %@", String(id)) request.fetchLimit = 1 - + do { let result = try context.fetch(request) return result.first @@ -55,7 +55,7 @@ actor AdamantChatTransactionService: ChatTransactionService { return nil } } - + /// Search transaction in local storage /// /// - Parameter id: Transacton ID @@ -64,7 +64,7 @@ actor AdamantChatTransactionService: ChatTransactionService { let request = NSFetchRequest(entityName: "ChatTransaction") request.predicate = NSPredicate(format: "transactionId == %@", String(id)) request.fetchLimit = 1 - + do { let result = try context.fetch(request) return result.first @@ -72,7 +72,7 @@ actor AdamantChatTransactionService: ChatTransactionService { return nil } } - + /// Parse raw transaction into CoreData chat transaction /// /// - Parameters: @@ -93,15 +93,15 @@ actor AdamantChatTransactionService: ChatTransactionService { removedMessages: [String], context: NSManagedObjectContext ) -> ChatTransaction? { - let messageTransaction: ChatTransaction + let chatTransaction: ChatTransaction guard let chat = transaction.asset.chat else { if transaction.type == .send { - messageTransaction = transferTransaction(from: transaction, isOut: isOutgoing, partner: partner, context: context) - return messageTransaction + chatTransaction = transferTransaction(from: transaction, isOut: isOutgoing, partner: partner, context: context) + return chatTransaction } return nil } - + // MARK: Decode message, message must contain data if let decodedMessage = adamantCore.decodeMessage( rawMessage: chat.message, @@ -111,11 +111,11 @@ actor AdamantChatTransactionService: ChatTransactionService { )?.trimmingCharacters(in: .whitespacesAndNewlines) { if (decodedMessage.isEmpty && transaction.amount > 0) || !decodedMessage.isEmpty { switch chat.type { - // MARK: Text message + // MARK: Text message case .message, .messageOld, .signal, .unknown: if transaction.amount > 0 { let trs: TransferTransaction - + if let trsDB = getTransfer( id: String(transaction.id), context: context @@ -127,75 +127,75 @@ actor AdamantChatTransactionService: ChatTransactionService { insertInto: context ) } - + trs.comment = decodedMessage - messageTransaction = trs + chatTransaction = trs } else { let trs = MessageTransaction(entity: MessageTransaction.entity(), insertInto: context) trs.message = decodedMessage - messageTransaction = trs - + chatTransaction = trs + let markdown = markdownParser.parse(decodedMessage) - + trs.isMarkdown = markdown.length != decodedMessage.count } - - // MARK: Rich message + + // MARK: Rich message case .richMessage: if let trs = baseRichTransaction( decodedMessage, transaction: transaction, context: context ) { - messageTransaction = trs + chatTransaction = trs break } - + if let trs = transferReplyTransaction( decodedMessage, transaction: transaction, context: context ) { - messageTransaction = trs + chatTransaction = trs break } - + if let trs = replyTransaction( decodedMessage, transaction: transaction, context: context ) { - messageTransaction = trs + chatTransaction = trs break } - + if let trs = reactionTransaction( decodedMessage, transaction: transaction, context: context ) { - messageTransaction = trs + chatTransaction = trs break } - + if let trs = fileTransaction( decodedMessage, transaction: transaction, context: context ) { - messageTransaction = trs + chatTransaction = trs break } - + let trs = MessageTransaction(entity: MessageTransaction.entity(), insertInto: context) trs.message = decodedMessage - messageTransaction = trs + chatTransaction = trs } } else { let trs = MessageTransaction(entity: MessageTransaction.entity(), insertInto: context) trs.message = "" trs.isHidden = true - messageTransaction = trs + chatTransaction = trs } } // MARK: Failed to decode, or message was empty @@ -203,32 +203,33 @@ actor AdamantChatTransactionService: ChatTransactionService { let trs = MessageTransaction(entity: MessageTransaction.entity(), insertInto: context) trs.message = "" trs.isHidden = true - messageTransaction = trs + chatTransaction = trs } - - messageTransaction.amount = transaction.amount as NSDecimalNumber - messageTransaction.date = transaction.date as NSDate - messageTransaction.recipientId = transaction.recipientId - messageTransaction.senderId = transaction.senderId - messageTransaction.transactionId = String(transaction.id) - messageTransaction.type = Int16(chat.type.rawValue) - messageTransaction.height = Int64(transaction.height) - messageTransaction.isConfirmed = true - messageTransaction.isOutgoing = isOutgoing - messageTransaction.blockId = transaction.blockId - messageTransaction.confirmations = transaction.confirmations - messageTransaction.chatMessageId = String(transaction.id) - messageTransaction.fee = transaction.fee as NSDecimalNumber - messageTransaction.statusEnum = MessageStatus.delivered - messageTransaction.partner = partner - messageTransaction.senderPublicKey = transaction.senderPublicKey - - let transactionId = messageTransaction.transactionId - messageTransaction.isHidden = removedMessages.contains(transactionId) - - return messageTransaction + + chatTransaction.date = transaction.date as NSDate + chatTransaction.timestampMs = Int64(transaction.timestampMs) + chatTransaction.amount = transaction.amount as NSDecimalNumber + chatTransaction.recipientId = transaction.recipientId + chatTransaction.senderId = transaction.senderId + chatTransaction.transactionId = String(transaction.id) + chatTransaction.type = Int16(chat.type.rawValue) + chatTransaction.height = Int64(transaction.height) + chatTransaction.isConfirmed = true + chatTransaction.isOutgoing = isOutgoing + chatTransaction.blockId = transaction.blockId + chatTransaction.confirmations = transaction.confirmations + chatTransaction.chatMessageId = String(transaction.id) + chatTransaction.fee = transaction.fee as NSDecimalNumber + chatTransaction.statusEnum = MessageStatus.delivered + chatTransaction.partner = partner + chatTransaction.senderPublicKey = transaction.senderPublicKey + + let transactionId = chatTransaction.transactionId + chatTransaction.isHidden = removedMessages.contains(transactionId) + + return chatTransaction } - + func transferTransaction( from transaction: Transaction, isOut: Bool, @@ -246,8 +247,9 @@ actor AdamantChatTransactionService: ChatTransactionService { transfer.blockId = transaction.blockId } else { transfer = TransferTransaction(context: context) - transfer.amount = transaction.amount as NSDecimalNumber transfer.date = transaction.date as NSDate + transfer.timestampMs = Int64(transaction.timestampMs) + transfer.amount = transaction.amount as NSDecimalNumber transfer.recipientId = transaction.recipientId transfer.senderId = transaction.senderId transfer.transactionId = String(transaction.id) @@ -263,7 +265,7 @@ actor AdamantChatTransactionService: ChatTransactionService { transfer.statusEnum = MessageStatus.delivered transfer.partner = partner } - + transfer.chatMessageId = String(transaction.id) transfer.isOutgoing = isOut transfer.partner = partner @@ -271,51 +273,52 @@ actor AdamantChatTransactionService: ChatTransactionService { } } -private extension AdamantChatTransactionService { - func baseRichTransaction( +extension AdamantChatTransactionService { + fileprivate func baseRichTransaction( _ decodedMessage: String, transaction: Transaction, context: NSManagedObjectContext ) -> ChatTransaction? { guard let data = decodedMessage.data(using: String.Encoding.utf8), - let richContent = RichMessageTools.richContent(from: data), - let type = richContent[RichContentKeys.type] as? String, - type != RichContentKeys.reply.reply, - type != RichContentKeys.file.file, - richContent[RichContentKeys.reply.replyToId] == nil, - richContent[RichContentKeys.file.files] == nil + let richContent = RichMessageTools.richContent(from: data), + let type = richContent[RichContentKeys.type] as? String, + type != RichContentKeys.reply.reply, + type != RichContentKeys.file.file, + richContent[RichContentKeys.reply.replyToId] == nil, + richContent[RichContentKeys.file.files] == nil else { return nil } - + let trs = RichMessageTransaction( entity: RichMessageTransaction.entity(), insertInto: context ) - + trs.richTransferHash = richContent[RichContentKeys.hash] as? String trs.richContent = richContent trs.richType = type trs.blockchainType = type - trs.transactionStatus = walletServiceCompose.getWallet(by: type) != nil - ? .notInitiated - : nil + trs.transactionStatus = + walletServiceCompose.getWallet(by: type) != nil + ? .notInitiated + : nil trs.additionalType = .base - + return trs } - - func transferReplyTransaction( + + fileprivate func transferReplyTransaction( _ decodedMessage: String, transaction: Transaction, context: NSManagedObjectContext ) -> ChatTransaction? { guard let data = decodedMessage.data(using: String.Encoding.utf8), - let richContent = RichMessageTools.richContent(from: data), - richContent[RichContentKeys.reply.replyToId] != nil, - transaction.amount > 0 + let richContent = RichMessageTools.richContent(from: data), + richContent[RichContentKeys.reply.replyToId] != nil, + transaction.amount > 0 else { return nil } - + let trs: TransferTransaction - + if let trsDB = getTransfer( id: String(transaction.id), context: context @@ -327,109 +330,110 @@ private extension AdamantChatTransactionService { insertInto: context ) } - + trs.comment = richContent[RichContentKeys.reply.replyMessage] as? String trs.replyToId = richContent[RichContentKeys.reply.replyToId] as? String - + return trs } - - func replyTransaction( + + fileprivate func replyTransaction( _ decodedMessage: String, transaction: Transaction, context: NSManagedObjectContext ) -> ChatTransaction? { guard let data = decodedMessage.data(using: String.Encoding.utf8), - let richContent = RichMessageTools.richContent(from: data), - richContent[RichContentKeys.reply.replyToId] != nil, - transaction.amount <= 0 + let richContent = RichMessageTools.richContent(from: data), + richContent[RichContentKeys.reply.replyToId] != nil, + transaction.amount <= 0 else { return nil } - + if let trs = getChatTransactionFromDB( id: String(transaction.id), context: context ) { return trs } - + let trs = RichMessageTransaction( entity: RichMessageTransaction.entity(), insertInto: context ) let transferContent = richContent[RichContentKeys.reply.replyMessage] as? [String: String] let type = (transferContent?[RichContentKeys.type] as? String) ?? RichContentKeys.reply.reply - + trs.richTransferHash = richContent[RichContentKeys.hash] as? String trs.richContent = richContent trs.richType = type trs.blockchainType = type - trs.transactionStatus = walletServiceCompose.getWallet(by: type) != nil - ? .notInitiated - : nil + trs.transactionStatus = + walletServiceCompose.getWallet(by: type) != nil + ? .notInitiated + : nil trs.additionalType = .reply - + return trs } - - func reactionTransaction( + + fileprivate func reactionTransaction( _ decodedMessage: String, transaction: Transaction, context: NSManagedObjectContext ) -> ChatTransaction? { guard let data = decodedMessage.data(using: String.Encoding.utf8), - let richContent = RichMessageTools.richContent(from: data), - richContent[RichContentKeys.react.reactto_id] != nil + let richContent = RichMessageTools.richContent(from: data), + richContent[RichContentKeys.react.reactto_id] != nil else { return nil } - + if let trs = getChatTransactionFromDB( id: String(transaction.id), context: context ) { return trs } - + let trs = RichMessageTransaction( entity: RichMessageTransaction.entity(), insertInto: context ) - + trs.richTransferHash = richContent[RichContentKeys.hash] as? String trs.richContent = richContent trs.richType = RichContentKeys.react.react trs.transactionStatus = nil trs.additionalType = .reaction - + return trs } - - func fileTransaction( + + fileprivate func fileTransaction( _ decodedMessage: String, transaction: Transaction, context: NSManagedObjectContext ) -> ChatTransaction? { guard let data = decodedMessage.data(using: String.Encoding.utf8), - let richContent = RichMessageTools.richContent(from: data), - richContent[RichContentKeys.file.files] != nil + let richContent = RichMessageTools.richContent(from: data), + richContent[RichContentKeys.file.files] != nil else { return nil } - + if let trs = getChatTransactionFromDB( id: String(transaction.id), context: context ) { return trs } - + let trs = RichMessageTransaction( entity: RichMessageTransaction.entity(), insertInto: context ) - + trs.richTransferHash = richContent[RichContentKeys.hash] as? String trs.richContent = richContent trs.richType = RichContentKeys.file.file trs.transactionStatus = nil trs.additionalType = .file - + return trs } } diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider+backgroundFetch.swift b/Adamant/Services/DataProviders/AdamantChatsProvider+backgroundFetch.swift index bffdbadfb..74a16e672 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider+backgroundFetch.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider+backgroundFetch.swift @@ -6,33 +6,33 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation extension AdamantChatsProvider: BackgroundFetchService { func fetchBackgroundData(notificationsService: NotificationsService) async -> FetchResult { - guard let address: String = securedStore.get(StoreKey.chatProvider.address) else { + guard let address: String = SecureStore.get(StoreKey.chatProvider.address) else { return .failed } - + var lastHeight: Int64? - if let raw: String = securedStore.get(StoreKey.chatProvider.receivedLastHeight) { + if let raw: String = SecureStore.get(StoreKey.chatProvider.receivedLastHeight) { lastHeight = Int64(raw) } else { lastHeight = nil } - + var notifiedCount = 0 - if let raw: String = securedStore.get(StoreKey.chatProvider.notifiedLastHeight), let notifiedHeight = Int64(raw), let h = lastHeight { + if let raw: String = SecureStore.get(StoreKey.chatProvider.notifiedLastHeight), let notifiedHeight = Int64(raw), let h = lastHeight { if h < notifiedHeight { lastHeight = notifiedHeight - - if let raw: String = securedStore.get(StoreKey.chatProvider.notifiedMessagesCount), let count = Int(raw) { + + if let raw: String = SecureStore.get(StoreKey.chatProvider.notifiedMessagesCount), let count = Int(raw) { notifiedCount = count } } } - + do { let transactions = try await apiService.getMessageTransactions( address: address, @@ -40,28 +40,28 @@ extension AdamantChatsProvider: BackgroundFetchService { offset: nil, waitsForConnectivity: false ).get() - + guard transactions.count > 0 else { return .noData } - + let total = transactions.count - securedStore.set( + SecureStore.set( String(total + notifiedCount), for: StoreKey.chatProvider.notifiedMessagesCount ) - - if let newLastHeight = transactions.map({$0.height}).sorted().last { - securedStore.set( + + if let newLastHeight = transactions.map({ $0.height }).sorted().last { + SecureStore.set( String(newLastHeight), for: StoreKey.chatProvider.notifiedLastHeight ) } - + await notificationsService.showNotification( title: NotificationStrings.newMessageTitle, body: NotificationStrings.newMessageBody(total + notifiedCount), type: .newMessages(count: total) ) - + return .newData } catch { return .failed diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider+fakeMessages.swift b/Adamant/Services/DataProviders/AdamantChatsProvider+fakeMessages.swift index 90dd268c8..fad2d2d3e 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider+fakeMessages.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider+fakeMessages.swift @@ -6,9 +6,9 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation -import CoreData import CommonKit +import CoreData +import Foundation extension AdamantChatsProvider { // MARK: - Public @@ -25,7 +25,7 @@ extension AdamantChatsProvider { let result = try await validate(message: message, partnerId: recipientId) let loggedAddress = result.loggedAccount let partner = result.partner - + switch message { case .text(let text): fakeSent( @@ -38,7 +38,7 @@ extension AdamantChatsProvider { showsChatroom: showsChatroom, completion: completion ) - + case .richMessage(let payload): fakeSent( text: payload.serialized(), @@ -50,7 +50,7 @@ extension AdamantChatsProvider { showsChatroom: showsChatroom, completion: completion ) - + case .markdownText(let text): fakeSent( text: text, @@ -70,7 +70,7 @@ extension AdamantChatsProvider { } } } - + func fakeReceived( message: AdamantMessage, senderId: String, @@ -78,12 +78,12 @@ extension AdamantChatsProvider { unread: Bool, silent: Bool, showsChatroom: Bool - ) async throws -> ChatTransaction { + ) async throws -> ChatTransaction { do { let result = try await validate(message: message, partnerId: senderId) let loggedAccount = result.loggedAccount let partner = result.partner - + switch message { case .text(let text): return try await fakeReceived( @@ -96,7 +96,7 @@ extension AdamantChatsProvider { markdown: false, showsChatroom: showsChatroom ) - + case .richMessage(let payload): return try await fakeReceived( text: payload.serialized(), @@ -108,7 +108,7 @@ extension AdamantChatsProvider { markdown: false, showsChatroom: showsChatroom ) - + case .markdownText(let text): return try await fakeReceived( text: text, @@ -127,14 +127,23 @@ extension AdamantChatsProvider { throw ChatsProviderError.internalError(error) } } - + // MARK: - Logic - - private func fakeSent(text: String, loggedAddress: String, recipient: CoreDataAccount, date: Date, status: MessageStatus, markdown: Bool, showsChatroom: Bool, completion: @escaping (ChatsProviderResultWithTransaction) -> Void) { + + private func fakeSent( + text: String, + loggedAddress: String, + recipient: CoreDataAccount, + date: Date, + status: MessageStatus, + markdown: Bool, + showsChatroom: Bool, + completion: @escaping (ChatsProviderResultWithTransaction) -> Void + ) { // MARK: 0. Prepare let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) privateContext.parent = stack.container.viewContext - + // MARK: 1. Create transaction let transaction = MessageTransaction(entity: MessageTransaction.entity(), insertInto: privateContext) transaction.date = date as NSDate @@ -148,16 +157,16 @@ extension AdamantChatsProvider { transaction.status = status.rawValue transaction.showsChatroom = showsChatroom transaction.partner = privateContext.object(with: recipient.objectID) as? BaseAccount - + transaction.transactionId = UUID().uuidString transaction.blockId = UUID().uuidString transaction.chatMessageId = transaction.transactionId - + // MARK: 2. Get Chatroom guard let id = recipient.chatroom?.objectID, let chatroom = privateContext.object(with: id) as? Chatroom else { return } - + // MARK: 3. Save it do { chatroom.addToTransactions(transaction) @@ -168,7 +177,7 @@ extension AdamantChatsProvider { completion(.failure(.internalError(error))) } } - + private func fakeReceived( text: String, loggedAddress: String, @@ -182,7 +191,7 @@ extension AdamantChatsProvider { // MARK: 0. Prepare let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) privateContext.parent = stack.container.viewContext - + // MARK: 1. Create transaction let transaction = MessageTransaction(entity: MessageTransaction.entity(), insertInto: privateContext) transaction.date = date as NSDate @@ -198,22 +207,22 @@ extension AdamantChatsProvider { transaction.status = MessageStatus.delivered.rawValue transaction.showsChatroom = showsChatroom transaction.partner = privateContext.object(with: sender.objectID) as? BaseAccount - + transaction.transactionId = UUID().uuidString transaction.blockId = UUID().uuidString transaction.chatMessageId = transaction.transactionId - + // MARK: 2. Get Chatroom guard let id = sender.chatroom?.objectID, - let chatroom = privateContext.object(with: id) as? Chatroom + let chatroom = privateContext.object(with: id) as? Chatroom else { throw ChatsProviderError.requestCancelled } - + if unread { chatroom.hasUnreadMessages = true } - + // MARK: 3. Save it do { chatroom.addToTransactions(transaction) @@ -224,32 +233,32 @@ extension AdamantChatsProvider { throw ChatsProviderError.internalError(error) } } - + // MARK: - Validate & prepare - + private enum ValidateResult { case success(loggedAccount: String, partner: CoreDataAccount) case failure(ChatsProviderError) } - + private func validate(message: AdamantMessage, partnerId: String) async throws -> (loggedAccount: String, partner: CoreDataAccount) { // MARK: 1. Logged account guard let loggedAddress = accountService.account?.address else { throw ChatsProviderError.notLogged } - + // MARK: 2. Validate message switch validateMessage(message) { case .isValid: break - + case .empty: throw ChatsProviderError.messageNotValid(.empty) - + case .tooLong: throw ChatsProviderError.messageNotValid(.tooLong) } - + // MARK: 3. Get recipient do { let account = try await accountsProvider.getAccount(byAddress: partnerId) @@ -258,28 +267,29 @@ extension AdamantChatsProvider { switch error { case .notFound, .invalidAddress, .notInitiated, .dummy: throw ChatsProviderError.accountNotFound(partnerId) - + case .networkError: throw ChatsProviderError.networkError - + case .serverError(let error): - throw ChatsProviderError.serverError(error) + throw ChatsProviderError.serverError(.init(from: error)) } } catch { - throw ChatsProviderError.serverError(error) + throw ChatsProviderError.serverError(.init(from: error)) } } - + // MARK: - Tools - + private func recheckLastTransactionFor(chatroom: Chatroom, with transaction: ChatTransaction) { if let ch = transaction.chatroom, ch != chatroom { return } - + if let lastTransaction = chatroom.lastTransaction { if let dateA = lastTransaction.date as Date?, let dateB = transaction.date as Date?, - dateA.compare(dateB) == ComparisonResult.orderedAscending { + dateA.compare(dateB) == ComparisonResult.orderedAscending + { chatroom.lastTransaction = transaction chatroom.updatedAt = transaction.date } diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider+search.swift b/Adamant/Services/DataProviders/AdamantChatsProvider+search.swift index 41e8cc39c..7f4517b32 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider+search.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider+search.swift @@ -6,40 +6,41 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation -import CoreData import CommonKit +import CoreData +import Foundation extension AdamantChatsProvider { func getMessages(containing text: String, in chatroom: Chatroom?) -> [MessageTransaction]? { let request = NSFetchRequest(entityName: "MessageTransaction") - + if let chatroom = chatroom { request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ NSPredicate(format: "chatroom == %@", chatroom), NSPredicate(format: "message CONTAINS[cd] %@", text), NSPredicate(format: "chatroom.isHidden == false"), - NSPredicate(format: "isHidden == false")]) + NSPredicate(format: "isHidden == false") + ]) } else { request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ NSPredicate(format: "message CONTAINS[cd] %@", text), NSPredicate(format: "chatroom.isHidden == false"), - NSPredicate(format: "isHidden == false")]) + NSPredicate(format: "isHidden == false") + ]) } - - request.sortDescriptors = [NSSortDescriptor.init(key: "date", ascending: false), - NSSortDescriptor(key: "transactionId", ascending: false)] - + + request.sortDescriptors = .sortChatTransactions(ascending: false) + do { let results = try stack.container.viewContext.fetch(request) return results } catch let error { print(error) } - + return nil } - + func isTransactionUnique(_ transaction: RichMessageTransaction) -> Bool { guard let type = transaction.richType, @@ -47,17 +48,16 @@ extension AdamantChatsProvider { else { return false } - + let request = NSFetchRequest(entityName: "RichMessageTransaction") - + request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ NSPredicate(format: "richType == %@", type), NSPredicate(format: "richContent.hash CONTAINS[cd] %@", hash) ]) - - request.sortDescriptors = [NSSortDescriptor.init(key: "date", ascending: false), - NSSortDescriptor(key: "transactionId", ascending: false)] - + + request.sortDescriptors = .sortChatTransactions(ascending: false) + do { let results = try stack.container.viewContext.fetch(request) return results.count <= 1 diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider.swift b/Adamant/Services/DataProviders/AdamantChatsProvider.swift index e75b27790..f2121ee56 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider.swift @@ -6,70 +6,75 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -@preconcurrency import CoreData -import MarkdownKit import Combine import CommonKit +@preconcurrency import CoreData +import MarkdownKit +import UIKit actor AdamantChatsProvider: ChatsProvider { - + // MARK: Dependencies - + private let socketService: SocketService private let adamantCore: AdamantCore private let transactionService: ChatTransactionService private let walletServiceCompose: WalletServiceCompose - + let accountService: AccountService let accountsProvider: AccountsProvider - let securedStore: SecuredStore + let SecureStore: SecureStore let apiService: AdamantApiServiceProtocol let stack: CoreDataStack - + // MARK: Properties - @ObservableValue private var stateNotifier: State = .empty - var stateObserver: AnyObservable { $stateNotifier.eraseToAnyPublisher() } - - private(set) var state: State = .empty + @ObservableValue private var stateNotifier: DataProviderState = .empty + var stateObserver: AnyObservable { $stateNotifier.eraseToAnyPublisher() } + + private(set) var state: DataProviderState = .empty private(set) var receivedLastHeight: Int64? private(set) var readedLastHeight: Int64? private let apiTransactions = 100 private let chatTransactionsLimit = 50 - private var unconfirmedTransactions: [UInt64:NSManagedObjectID] = [:] + private var unconfirmedTransactions: [UInt64: NSManagedObjectID] = [:] private var unconfirmedTransactionsBySignature: [String] = [] - - @MainActor private var chatPositon: [String : Double] = [:] + private var chatsMarkAsUnread: Set? = Set() + + @MainActor private var chatPositon: [String: Double] = [:] private(set) var blockList: [String] = [] private(set) var removedMessages: [String] = [] - + @ObservableValue private var chatLoadingStatusDictionary: [String: ChatRoomLoadingStatus] = [:] var chatLoadingStatusPublisher: AnyObservable<[String: ChatRoomLoadingStatus]> { $chatLoadingStatusDictionary.eraseToAnyPublisher() } - - var chatMaxMessages: [String : Int] = [:] - var chatLoadedMessages: [String : Int] = [:] + + var chatMaxMessages: [String: Int] = [:] + var chatLoadedMessages: [String: Int] = [:] private let preLoadChatsCount = 5 private var isConnectedToTheInternet = true private var onConnectionToTheInternetRestoredTasks = [() -> Void]() - + private(set) var isInitiallySynced: Bool = false { didSet { - NotificationCenter.default.post(name: Notification.Name.AdamantChatsProvider.initiallySyncedChanged, object: self, userInfo: [AdamantUserInfoKey.ChatProvider.initiallySynced : isInitiallySynced]) + NotificationCenter.default.post( + name: Notification.Name.AdamantChatsProvider.initiallySyncedChanged, + object: self, + userInfo: [AdamantUserInfoKey.ChatProvider.initiallySynced: isInitiallySynced] + ) } } - + private let markdownParser = MarkdownParser(font: UIFont.systemFont(ofSize: UIFont.systemFontSize)) - + private var previousAppState: UIApplication.State? - + private(set) var roomsMaxCount: Int? private(set) var roomsLoadedCount: Int? - + private var subscriptions = Set() private let minReactionsProcent = 30 - + // MARK: Lifecycle init( accountService: AccountService, @@ -79,7 +84,7 @@ actor AdamantChatsProvider: ChatsProvider { adamantCore: AdamantCore, accountsProvider: AccountsProvider, transactionService: ChatTransactionService, - securedStore: SecuredStore, + SecureStore: SecureStore, walletServiceCompose: WalletServiceCompose ) { self.accountService = accountService @@ -89,150 +94,155 @@ actor AdamantChatsProvider: ChatsProvider { self.adamantCore = adamantCore self.accountsProvider = accountsProvider self.transactionService = transactionService - self.securedStore = securedStore + self.SecureStore = SecureStore self.walletServiceCompose = walletServiceCompose - + Task { - await setupSecuredStore() + await setupSecureStore() await addObservers() } } - + private func addObservers() { NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedIn) .sink { [weak self] in await self?.userLoggedInAction($0) } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedOut) .sink { [weak self] _ in await self?.userLogOutAction() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantAccountService.stayInChanged) .sink { [weak self] in await self?.stayInChangedAction($0) } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: UIApplication.didBecomeActiveNotification) .sink { [weak self] _ in await self?.didBecomeActiveAction() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: UIApplication.willResignActiveNotification) .sink { [weak self] _ in await self?.willResignActiveAction() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantReachabilityMonitor.reachabilityChanged) .sink { [weak self] in await self?.reachabilityChangedAction($0) } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantTransfersProvider.initialSyncFinished) .sink { [weak self] _ in await self?.getChatRooms(offset: nil) } .store(in: &subscriptions) } - + deinit { NotificationCenter.default.removeObserver(self) } - + // MARK: - Notifications action - + private func userLoggedInAction(_ notification: Notification) { - let store = self.securedStore - + let store = self.SecureStore + guard let loggedAddress = notification.userInfo?[AdamantUserInfoKey.AccountService.loggedAccountAddress] as? String else { store.remove(StoreKey.chatProvider.address) store.remove(StoreKey.chatProvider.receivedLastHeight) store.remove(StoreKey.chatProvider.readedLastHeight) + store.remove(StoreKey.chatProvider.markedChatsAsUnread) self.dropStateData() return } - + if let savedAddress: String = store.get(StoreKey.chatProvider.address), savedAddress == loggedAddress { if let raw: String = store.get(StoreKey.chatProvider.readedLastHeight), - let h = Int64(raw) { + let h = Int64(raw) + { self.readedLastHeight = h } } else { store.remove(StoreKey.chatProvider.receivedLastHeight) store.remove(StoreKey.chatProvider.readedLastHeight) + store.remove(StoreKey.chatProvider.markedChatsAsUnread) self.dropStateData() store.set(loggedAddress, for: StoreKey.chatProvider.address) } - + self.connectToSocket() } - + private func userLogOutAction() { // Drop everything reset() - + // BackgroundFetch dropStateData() - + blockList = [] removedMessages = [] - + disconnectFromSocket() } - + private func stayInChangedAction(_ notification: Notification) { guard let state = notification.userInfo?[AdamantUserInfoKey.AccountService.newStayInState] as? Bool, - state + state else { return } - + if state { - securedStore.set(blockList, for: StoreKey.accountService.blockList) - securedStore.set(removedMessages, for: StoreKey.accountService.removedMessages) + SecureStore.set(blockList, for: StoreKey.accountService.blockList) + SecureStore.set(removedMessages, for: StoreKey.accountService.removedMessages) } } - + private func didBecomeActiveAction() async { if let previousAppState = previousAppState, - previousAppState == .background { + previousAppState == .background + { self.previousAppState = .active _ = await update(notifyState: true) } } - + private func willResignActiveAction() { if isInitiallySynced { previousAppState = .background } } - + private func reachabilityChangedAction(_ notification: Notification) { - guard let connection = notification - .userInfo?[AdamantUserInfoKey.ReachabilityMonitor.connection] as? Bool + guard + let connection = notification + .userInfo?[AdamantUserInfoKey.ReachabilityMonitor.connection] as? Bool else { return } - + guard connection == true else { isConnectedToTheInternet = false return } - + if isConnectedToTheInternet == false { onConnectionToTheInternetRestored() } - + isConnectedToTheInternet = true } - + // MARK: Tools /// Free stateSemaphore before calling this method, or you will deadlock. - private func setState(_ state: State, previous prevState: State, notify: Bool = true) { + private func setState(_ state: DataProviderState, previous prevState: DataProviderState, notify: Bool = true) { self.state = state - + guard notify else { return } - + if case .failedToUpdate = prevState { NotificationCenter.default.post( name: Notification.Name.AdamantTransfersProvider.stateChanged, @@ -242,13 +252,13 @@ actor AdamantChatsProvider: ChatsProvider { AdamantUserInfoKey.TransfersProvider.prevState: prevState ] ) - + stateNotifier = state return } - + guard prevState != state else { return } - + NotificationCenter.default.post( name: Notification.Name.AdamantTransfersProvider.stateChanged, object: self, @@ -257,20 +267,20 @@ actor AdamantChatsProvider: ChatsProvider { AdamantUserInfoKey.TransfersProvider.prevState: prevState ] ) - + stateNotifier = state } - - private func setupSecuredStore() { - blockList = securedStore.get(StoreKey.accountService.blockList) ?? [] - removedMessages = securedStore.get(StoreKey.accountService.removedMessages) ?? [] + + private func setupSecureStore() { + blockList = SecureStore.get(StoreKey.accountService.blockList) ?? [] + removedMessages = SecureStore.get(StoreKey.accountService.removedMessages) ?? [] } - + func dropStateData() { - securedStore.remove(StoreKey.chatProvider.notifiedLastHeight) - securedStore.remove(StoreKey.chatProvider.notifiedMessagesCount) + SecureStore.remove(StoreKey.chatProvider.notifiedLastHeight) + SecureStore.remove(StoreKey.chatProvider.notifiedMessagesCount) } - + func isMessageDeleted(id: String) -> Bool { removedMessages.contains(id) } @@ -282,16 +292,16 @@ extension AdamantChatsProvider { reset(notify: false) _ = await update(notifyState: false) } - + func reset() { reset(notify: true) } - + private func reset(notify: Bool) { isInitiallySynced = false let prevState = self.state - setState(.updating, previous: prevState, notify: false) // Block update calls - + setState(.updating, previous: prevState, notify: false) // Block update calls + // Drop props receivedLastHeight = nil readedLastHeight = nil @@ -300,34 +310,35 @@ extension AdamantChatsProvider { chatLoadingStatusDictionary.removeAll() chatMaxMessages.removeAll() chatLoadedMessages.removeAll() - + // Drop store - securedStore.remove(StoreKey.chatProvider.address) - securedStore.remove(StoreKey.chatProvider.receivedLastHeight) - securedStore.remove(StoreKey.chatProvider.readedLastHeight) - + SecureStore.remove(StoreKey.chatProvider.address) + SecureStore.remove(StoreKey.chatProvider.receivedLastHeight) + SecureStore.remove(StoreKey.chatProvider.readedLastHeight) + SecureStore.remove(StoreKey.chatProvider.markedChatsAsUnread) + // Set State setState(.empty, previous: prevState, notify: notify) } - + func getChatRooms(offset: Int?) async { guard let address = accountService.account?.address, - let privateKey = accountService.keypair?.privateKey + let privateKey = accountService.keypair?.privateKey else { return } - + let prevState = state state = .updating - + // MARK: 3. Get transactions - + let chatrooms = try? await apiService.getChatRooms( address: address, offset: offset, waitsForConnectivity: true ).get() - + guard let chatrooms = chatrooms else { if !isInitiallySynced { isInitiallySynced = true @@ -335,36 +346,36 @@ extension AdamantChatsProvider { setState(.upToDate, previous: prevState) return } - + roomsMaxCount = chatrooms.count - + if let roomsLoadedCount = roomsLoadedCount { self.roomsLoadedCount = roomsLoadedCount + (chatrooms.chats?.count ?? 0) } else { self.roomsLoadedCount = chatrooms.chats?.count ?? 0 } - + var array = [Transaction]() chatrooms.chats?.forEach({ room in if let last = room.lastTransaction { array.append(last) } }) - + await process( messageTransactions: array, senderId: address, privateKey: privateKey ) - + if !isInitiallySynced { isInitiallySynced = true preLoadChats(array, address: address) } - + setState(.upToDate, previous: prevState) } - + func preLoadChats(_ array: [Transaction], address: String) { let preLoadChatsCount = preLoadChatsCount array.prefix(preLoadChatsCount).forEach { transaction in @@ -376,36 +387,37 @@ extension AdamantChatsProvider { } } } - + func getChatMessages(with addressRecipient: String, offset: Int?) async { await getChatMessages(with: addressRecipient, offset: offset, loadedCount: .zero) } - + func getChatMessages( with addressRecipient: String, offset: Int?, loadedCount: Int ) async { guard let address = accountService.account?.address, - let privateKey = accountService.keypair?.privateKey else { + let privateKey = accountService.keypair?.privateKey + else { return } - + // MARK: 3. Get transactions - + if getChatStatus(for: addressRecipient) == .none { setChatStatus(for: addressRecipient, status: .loading) } - + let chatroom = try? await apiGetChatMessages( address: address, addressRecipient: addressRecipient, offset: offset, limit: chatTransactionsLimit ) - + guard let transactions = chatroom?.messages, - transactions.count > 0 + transactions.count > 0 else { setChatDoneStatus( for: addressRecipient, @@ -414,13 +426,13 @@ extension AdamantChatsProvider { ) return } - + let result = await process( messageTransactions: transactions, senderId: address, privateKey: privateKey ) - + await processChatMessages( result: result, chatroom: chatroom, @@ -429,7 +441,7 @@ extension AdamantChatsProvider { loadedCount: loadedCount ) } - + func processChatMessages( result: (reactionsCount: Int, totalCount: Int), chatroom: ChatRooms?, @@ -438,38 +450,38 @@ extension AdamantChatsProvider { loadedCount: Int ) async { let messageCount = chatroom?.messages?.count ?? 0 - + let minRectionsCount = result.totalCount * minReactionsProcent / 100 let newLoadedCount = loadedCount + (result.totalCount - result.reactionsCount) - + guard result.reactionsCount > minRectionsCount, - newLoadedCount < chatTransactionsLimit + newLoadedCount < chatTransactionsLimit else { setChatDoneStatus( for: addressRecipient, messageCount: messageCount, maxCount: chatroom?.count ) - + NotificationCenter.default.post( name: .AdamantChatsProvider.initiallyLoadedMessages, object: addressRecipient ) return } - + let offset = (offset ?? 0) + messageCount let loadedCount = chatLoadedMessages[addressRecipient] ?? 0 chatLoadedMessages[addressRecipient] = loadedCount + messageCount - + return await getChatMessages( with: addressRecipient, offset: offset, loadedCount: newLoadedCount ) } - + func setChatDoneStatus( for addressRecipient: String, messageCount: Int, @@ -477,11 +489,11 @@ extension AdamantChatsProvider { ) { setChatStatus(for: addressRecipient, status: .loaded) chatMaxMessages[addressRecipient] = maxCount - + let loadedCount = chatLoadedMessages[addressRecipient] ?? 0 chatLoadedMessages[addressRecipient] = loadedCount + messageCount } - + func apiGetChatMessages( address: String, addressRecipient: String, @@ -500,9 +512,9 @@ extension AdamantChatsProvider { guard case .networkError = error else { return nil } - + try await Task.sleep(interval: requestRepeatDelay) - + return try await apiGetChatMessages( address: address, addressRecipient: addressRecipient, @@ -511,16 +523,17 @@ extension AdamantChatsProvider { ) } } - + func connectToSocket() { // MARK: 2. Prepare guard let address = accountService.account?.address, - let privateKey = accountService.keypair?.privateKey else { + let privateKey = accountService.keypair?.privateKey + else { return } // MARK: 3. Get transactions - + socketService.connect(address: address) { [weak self] result in switch result { case .success(let trans): @@ -535,36 +548,44 @@ extension AdamantChatsProvider { break } } + setupAsReadyToSyncChats() + } + + func setupAsReadyToSyncChats() { + if isInitiallySynced { + isInitiallySynced = false + } } - + func disconnectFromSocket() { self.socketService.disconnect() } - + func update(notifyState: Bool) async -> ChatsProviderResult? { // MARK: 1. Check state guard isInitiallySynced, - state != .updating + state != .updating else { + stateNotifier = state return nil } - + // MARK: 2. Prepare let prevState = state - + guard let address = accountService.account?.address, - let privateKey = accountService.keypair?.privateKey + let privateKey = accountService.keypair?.privateKey else { setState(.failedToUpdate(ChatsProviderError.notLogged), previous: prevState) return .failure(ChatsProviderError.notLogged) } - + setState(.updating, previous: prevState, notify: notifyState) - + // MARK: 3. Get transactions - + let prevHeight = receivedLastHeight - + try? await getTransactions( senderId: address, privateKey: privateKey, @@ -572,88 +593,104 @@ extension AdamantChatsProvider { offset: nil, waitsForConnectivity: true ) - + // MARK: 4. Check - + switch state { case .upToDate, .empty, .updating: setState(.upToDate, previous: state, notify: notifyState) - + if prevHeight != receivedLastHeight, - let h = receivedLastHeight { + let h = receivedLastHeight + { NotificationCenter.default.post( name: Notification.Name.AdamantChatsProvider.newUnreadMessages, object: self, - userInfo: [AdamantUserInfoKey.ChatProvider.lastMessageHeight:h] + userInfo: [AdamantUserInfoKey.ChatProvider.lastMessageHeight: h] ) } - + if let h = receivedLastHeight { readedLastHeight = h } else { readedLastHeight = 0 } - + if let h = receivedLastHeight { - securedStore.set(String(h), for: StoreKey.chatProvider.receivedLastHeight) + SecureStore.set(String(h), for: StoreKey.chatProvider.receivedLastHeight) } - + if let h = readedLastHeight, - h > 0 { - securedStore.set(String(h), for: StoreKey.chatProvider.readedLastHeight) + h > 0 + { + SecureStore.set(String(h), for: StoreKey.chatProvider.readedLastHeight) } - + if !isInitiallySynced { isInitiallySynced = true } - + return .success - case .failedToUpdate(let error): // Processing failed + case .failedToUpdate(let error): // Processing failed let err: ChatsProviderError - + switch error { case let error as ApiServiceError: switch error { case .notLogged: err = .notLogged - + case .accountNotFound: err = .accountNotFound(address) - + case .serverError, .commonError, .noEndpointsAvailable: - err = .serverError(error) - + err = .serverError(.init(from: error)) + case .internalError(let message, _): err = .dependencyError(message) - + case .networkError: err = .networkError - + case .requestCancelled: err = .requestCancelled } - + default: err = .internalError(error) } - + setState(.failedToUpdate(error), previous: state, notify: notifyState) return .failure(err) } } - + + func setManualMarkChatAsUnread(chatroomId: String) { + chatsMarkAsUnread?.insert(chatroomId) + SecureStore.set(chatsMarkAsUnread, for: StoreKey.chatProvider.markedChatsAsUnread) + } + + func removeManualMarkChatAsUnread(chatroomId: String) { + chatsMarkAsUnread?.remove(chatroomId) + SecureStore.set(chatsMarkAsUnread, for: StoreKey.chatProvider.markedChatsAsUnread) + } + + func getMarkAdressesFromChain() -> Set { + return SecureStore.get(StoreKey.chatProvider.markedChatsAsUnread) ?? Set() + } + func isChatLoading(with addressRecipient: String) -> Bool { chatLoadingStatusDictionary[addressRecipient] == .loading } - + func isChatLoaded(with addressRecipient: String) -> Bool { chatLoadingStatusDictionary[addressRecipient] == .loaded } - + func getChatStatus(for recipient: String) -> ChatRoomLoadingStatus { chatLoadingStatusDictionary[recipient] ?? .none } - + func setChatStatus(for recipient: String, status: ChatRoomLoadingStatus) { chatLoadingStatusDictionary[recipient] = status } @@ -665,26 +702,26 @@ extension AdamantChatsProvider { guard let loggedAccount = accountService.account, let keypair = accountService.keypair else { throw ChatsProviderError.notLogged } - + guard loggedAccount.balance >= message.fee else { throw ChatsProviderError.notEnoughMoneyToSend } - + switch validateMessage(message) { case .isValid: break - + case .empty: throw ChatsProviderError.messageNotValid(.empty) case .tooLong: throw ChatsProviderError.messageNotValid(.tooLong) } - + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) context.parent = stack.container.viewContext - + let transactionLocaly: ChatTransaction - + switch message { case .text(let text): transactionLocaly = try await sendTextMessageLocaly( @@ -718,7 +755,7 @@ extension AdamantChatsProvider { context: context ) } - + let transaction = try await sendMessageToServer( senderId: loggedAccount.address, recipientId: recipientId, @@ -727,22 +764,22 @@ extension AdamantChatsProvider { keypair: keypair, context: context ) - + return transaction } - + @MainActor func removeChatPositon(for address: String) { chatPositon.removeValue(forKey: address) } - + @MainActor func setChatPositon(for address: String, position: Double?) { chatPositon[address] = position } - + @MainActor func getChatPositon(for address: String) -> Double? { return chatPositon[address] } - + /// Logic: /// create and write transaction in local database /// send transaction to server @@ -750,11 +787,11 @@ extension AdamantChatsProvider { guard let loggedAccount = accountService.account, let keypair = accountService.keypair else { throw ChatsProviderError.notLogged } - + guard loggedAccount.balance >= message.fee else { throw ChatsProviderError.notEnoughMoneyToSend } - + switch validateMessage(message) { case .isValid: break @@ -763,12 +800,12 @@ extension AdamantChatsProvider { case .tooLong: throw ChatsProviderError.messageNotValid(.tooLong) } - + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) context.parent = stack.container.viewContext - + let transactionLocaly: ChatTransaction - + switch message { case .text(let text): transactionLocaly = try await sendTextMessageLocaly( @@ -805,7 +842,7 @@ extension AdamantChatsProvider { from: chatroom ) } - + let transaction = try await sendMessageToServer( senderId: loggedAccount.address, recipientId: recipientId, @@ -815,10 +852,10 @@ extension AdamantChatsProvider { context: context, from: chatroom ) - + return transactionLocaly } - + func sendFileMessageLocally( _ message: AdamantMessage, recipientId: String, @@ -827,11 +864,11 @@ extension AdamantChatsProvider { guard let loggedAccount = accountService.account, let keypair = accountService.keypair else { throw ChatsProviderError.notLogged } - + guard loggedAccount.balance >= message.fee else { throw ChatsProviderError.notEnoughMoneyToSend } - + switch validateMessage(message) { case .isValid: break @@ -840,14 +877,14 @@ extension AdamantChatsProvider { case .tooLong: throw ChatsProviderError.messageNotValid(.tooLong) } - + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) context.parent = stack.container.viewContext - + guard case let .richMessage(payload) = message else { throw ChatsProviderError.messageNotValid(.empty) } - + let transactionLocaly = try await sendRichMessageLocaly( richContent: payload.content(), richContentSerialized: payload.serialized(), @@ -862,7 +899,7 @@ extension AdamantChatsProvider { return transactionLocaly.transactionId } - + func sendFileMessage( _ message: AdamantMessage, recipientId: String, @@ -872,11 +909,11 @@ extension AdamantChatsProvider { guard let loggedAccount = accountService.account, let keypair = accountService.keypair else { throw ChatsProviderError.notLogged } - + guard loggedAccount.balance >= message.fee else { throw ChatsProviderError.notEnoughMoneyToSend } - + switch validateMessage(message) { case .isValid: break @@ -885,25 +922,26 @@ extension AdamantChatsProvider { case .tooLong: throw ChatsProviderError.messageNotValid(.tooLong) } - + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) context.parent = stack.container.viewContext - - guard let transactionLocaly = getBaseTransactionFromDB( - id: transactionLocalyId, - context: context - ) as? RichMessageTransaction + + guard + let transactionLocaly = getBaseTransactionFromDB( + id: transactionLocalyId, + context: context + ) as? RichMessageTransaction else { throw ChatsProviderError.transactionNotFound(id: transactionLocalyId) } - + guard case let .richMessage(payload) = message else { throw ChatsProviderError.messageNotValid(.empty) } - + transactionLocaly.richContent = payload.content() transactionLocaly.richContentSerialized = payload.serialized() - + let transaction = try await sendMessageToServer( senderId: loggedAccount.address, recipientId: recipientId, @@ -913,46 +951,48 @@ extension AdamantChatsProvider { context: context, from: chatroom ) - + return transaction } - + func setTxMessageStatus( txId: String, status: MessageStatus ) throws { let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) context.parent = stack.container.viewContext - - guard let transaction = getBaseTransactionFromDB( - id: txId, - context: context - ) as? RichMessageTransaction + + guard + let transaction = getBaseTransactionFromDB( + id: txId, + context: context + ) as? RichMessageTransaction else { throw ChatsProviderError.transactionNotFound(id: txId) } - + transaction.statusEnum = status try context.save() } - + func updateTxMessageContent(txId: String, richMessage: RichMessage) throws { let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) context.parent = stack.container.viewContext - - guard let transaction = getBaseTransactionFromDB( - id: txId, - context: context - ) as? RichMessageTransaction + + guard + let transaction = getBaseTransactionFromDB( + id: txId, + context: context + ) as? RichMessageTransaction else { throw ChatsProviderError.transactionNotFound(id: txId) } - + transaction.richContent = richMessage.content() transaction.richContentSerialized = richMessage.serialized() try context.save() } - + private func sendTextMessageLocaly( text: String, isMarkdown: Bool, @@ -966,6 +1006,7 @@ extension AdamantChatsProvider { let transaction = MessageTransaction(context: context) let id = UUID().uuidString transaction.date = Date() as NSDate + transaction.timestampMs = transaction.timeIntervalMillisecondsSince1970 transaction.recipientId = recipientId transaction.senderId = senderId transaction.type = Int16(type.rawValue) @@ -973,19 +1014,19 @@ extension AdamantChatsProvider { transaction.chatMessageId = id transaction.isMarkdown = isMarkdown transaction.transactionId = id - + transaction.message = text - - if - let c = chatroom, + + if let c = chatroom, let chatroom = context.object(with: c.objectID) as? Chatroom, let partner = chatroom.partner { transaction.statusEnum = MessageStatus.pending transaction.partner = context.object(with: partner.objectID) as? BaseAccount - + chatroom.lastTransaction = transaction + chatroom.updatedAt = transaction.date chatroom.addToTransactions(transaction) - + do { try context.save() return transaction @@ -993,10 +1034,10 @@ extension AdamantChatsProvider { throw ChatsProviderError.internalError(error) } } - + return transaction } - + private func sendRichMessageLocaly( richContent: [String: Any], richContentSerialized: String, @@ -1012,6 +1053,7 @@ extension AdamantChatsProvider { let id = UUID().uuidString let transaction = RichMessageTransaction(context: context) transaction.date = Date() as NSDate + transaction.timestampMs = transaction.timeIntervalMillisecondsSince1970 transaction.recipientId = recipientId transaction.senderId = senderId transaction.type = Int16(type.rawValue) @@ -1024,21 +1066,22 @@ extension AdamantChatsProvider { transaction.richContentSerialized = richContentSerialized transaction.blockchainType = richType transaction.richTransferHash = id - - transaction.transactionStatus = walletServiceCompose.getWallet(by: richType) != nil - ? .notInitiated - : nil - - if - let c = chatroom, + + transaction.transactionStatus = + walletServiceCompose.getWallet(by: richType) != nil + ? .notInitiated + : nil + + if let c = chatroom, let chatroom = context.object(with: c.objectID) as? Chatroom, let partner = chatroom.partner { transaction.statusEnum = MessageStatus.pending transaction.partner = context.object(with: partner.objectID) as? BaseAccount - + chatroom.addToTransactions(transaction) - + chatroom.lastTransaction = transaction + chatroom.updatedAt = transaction.date do { try context.save() return transaction @@ -1046,10 +1089,10 @@ extension AdamantChatsProvider { throw ChatsProviderError.internalError(error) } } - + return transaction } - + private func sendMessageToServer( senderId: String, recipientId: String, @@ -1067,10 +1110,10 @@ extension AdamantChatsProvider { keypair: keypair, from: chatroom ) - + return sendTransaction } - + /// Transaction must be in passed context private func prepareAndSendChatTransaction( _ transaction: ChatTransaction, @@ -1080,44 +1123,42 @@ extension AdamantChatsProvider { keypair: Keypair, from chatroom: Chatroom? = nil ) async throws -> ChatTransaction { - + // MARK: 1. Get account - + do { let recipientAccount = try await accountsProvider.getAccount(byAddress: recipientId) - + guard let recipientPublicKey = recipientAccount.publicKey else { throw ChatsProviderError.accountNotFound(recipientId) } - + let isAdded = chatroom == nil - + // MARK: 2. Get Chatroom - + guard let id = recipientAccount.chatroom?.objectID, - let chatroom = context.object(with: id) as? Chatroom + let chatroom = context.object(with: id) as? Chatroom else { throw ChatsProviderError.accountNotFound(recipientId) } - + // MARK: 3. Prepare transaction - if var correctedDate = transaction.date as? Date { - correctedDate -= AdmWalletService.adamantTimestampCorrection - transaction.date = correctedDate as NSDate - } + transaction.statusEnum = MessageStatus.pending transaction.partner = context.object(with: recipientAccount.objectID) as? BaseAccount - + if isAdded { chatroom.addToTransactions(transaction) } - + // MARK: 4. Last in - + if let lastTransaction = chatroom.lastTransaction { if let dateA = lastTransaction.date as Date?, - let dateB = transaction.date as Date?, - dateA.compare(dateB) == ComparisonResult.orderedAscending { + let dateB = transaction.date as Date?, + dateA.compare(dateB) == ComparisonResult.orderedAscending + { chatroom.lastTransaction = transaction chatroom.updatedAt = transaction.date } @@ -1125,11 +1166,11 @@ extension AdamantChatsProvider { chatroom.lastTransaction = transaction chatroom.updatedAt = transaction.date } - + defer { try? context.save() } - + return try await sendTransaction( transaction, type: type, @@ -1143,7 +1184,7 @@ extension AdamantChatsProvider { case .notInitiated, .dummy: throw ChatsProviderError.accountNotInitiated(recipientId) case .serverError(let error): - throw ChatsProviderError.serverError(error) + throw ChatsProviderError.serverError(.init(from: error)) case .networkError: throw ChatsProviderError.networkError } @@ -1151,40 +1192,42 @@ extension AdamantChatsProvider { throw error } } - + func retrySendMessage(_ message: ChatTransaction) async throws { // MARK: 0. Prepare switch message.statusEnum { case .delivered, .pending: - return + return case .failed: break } - + guard let keypair = accountService.keypair else { throw ChatsProviderError.notLogged } - + guard let recipientPublicKey = message.chatroom?.partner?.publicKey else { throw ChatsProviderError.accountNotFound(message.recipientId ?? "") } - + // MARK: 1. Prepare private context let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) privateContext.parent = stack.container.viewContext - + guard let transaction = privateContext.object(with: message.objectID) as? MessageTransaction else { throw ChatsProviderError.notLogged } - + // MARK: 2. Update transaction transaction.date = Date() as NSDate + transaction.timestampMs = transaction.timeIntervalMillisecondsSince1970 transaction.statusEnum = .pending - + if let chatroom = transaction.chatroom { if let lastTransaction = chatroom.lastTransaction { if let dateA = lastTransaction.date as Date?, let dateB = transaction.date as Date?, - dateA.compare(dateB) == ComparisonResult.orderedAscending { + dateA.compare(dateB) == ComparisonResult.orderedAscending + { chatroom.lastTransaction = transaction chatroom.updatedAt = transaction.date } @@ -1193,11 +1236,11 @@ extension AdamantChatsProvider { chatroom.updatedAt = transaction.date } } - + defer { try? privateContext.save() } - + _ = try await sendTransaction( transaction, type: .message, @@ -1205,9 +1248,9 @@ extension AdamantChatsProvider { recipientPublicKey: recipientPublicKey ) } - + // MARK: - Delete local message - func cancelMessage(_ message: ChatTransaction) async throws { + func cancelMessage(_ message: ChatTransaction) async throws { // MARK: 0. Prepare switch message.statusEnum { case .delivered, .pending: @@ -1216,14 +1259,14 @@ extension AdamantChatsProvider { case .failed: break } - + // MARK: 1. Find. Destroy. Save. - + let chatroom = message.chatroom - + let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) privateContext.parent = stack.container.viewContext - + privateContext.delete(privateContext.object(with: message.objectID)) do { try privateContext.save() @@ -1234,9 +1277,9 @@ extension AdamantChatsProvider { throw ChatsProviderError.internalError(error) } } - + // MARK: - Logic - + /// Send transaction. /// /// If success - update transaction's id, status and add it to unconfirmed transactions. @@ -1249,21 +1292,22 @@ extension AdamantChatsProvider { ) async throws -> ChatTransaction { // MARK: 0. Prepare guard let senderId = transaction.senderId, - let recipientId = transaction.recipientId else { + let recipientId = transaction.recipientId + else { throw ChatsProviderError.accountNotFound(recipientPublicKey) } - + // MARK: 1. Encode guard let text = transaction.serializedMessage(), - let encodedMessage = adamantCore.encodeMessage( + let encodedMessage = adamantCore.encodeMessage( text, recipientPublicKey: recipientPublicKey, privateKey: keypair.privateKey - ) + ) else { throw ChatsProviderError.dependencyError("Failed to encode message") } - + // MARK: 2. Create let signedTransaction = try? adamantCore.makeSendMessageTransaction( senderId: senderId, @@ -1272,56 +1316,59 @@ extension AdamantChatsProvider { message: encodedMessage.message, type: type, nonce: encodedMessage.nonce, - amount: nil + amount: nil, + date: AdmWalletService.correctedDate ) - + guard let signedTransaction = signedTransaction else { throw ChatsProviderError.internalError(AdamantError(message: InternalAPIError.signTransactionFailed.localizedDescription)) } - + unconfirmedTransactionsBySignature.append(signedTransaction.signature) - + // MARK: 3. Send - + do { let locallyID = signedTransaction.generateId() ?? UUID().uuidString transaction.transactionId = locallyID transaction.chatMessageId = locallyID - + let id = try await apiService.sendMessageTransaction(transaction: signedTransaction).get() - + // Update ID with recieved, add to unconfirmed transactions. transaction.transactionId = String(id) transaction.chatMessageId = String(id) transaction.statusEnum = .delivered - + transaction.chatroom?.updatedAt = NSDate() + removeTxFromUnconfirmed( signature: signedTransaction.signature, transaction: transaction ) - + return transaction } catch { - guard case let(apiError) = (error as? ApiServiceError), - case let(.serverError(text)) = apiError, - text.contains("Transaction is already confirmed") + guard case let (apiError) = (error as? ApiServiceError), + case let (.serverError(text)) = apiError, + text.contains("Transaction is already confirmed") || text.contains("Transaction is already processed") else { transaction.statusEnum = .failed + transaction.chatroom?.updatedAt = NSDate() throw handleTransactionError(error, recipientId: recipientId) } - + transaction.statusEnum = .pending - + removeTxFromUnconfirmed( signature: signedTransaction.signature, transaction: transaction ) - + return transaction } } - + func removeTxFromUnconfirmed( signature: String, transaction: ChatTransaction @@ -1331,10 +1378,10 @@ extension AdamantChatsProvider { ) { unconfirmedTransactionsBySignature.remove(at: index) } - + unconfirmedTransactions[UInt64(transaction.transactionId) ?? .zero] = transaction.objectID } - + func handleTransactionError(_ error: Error, recipientId: String) -> Error { switch error as? ApiServiceError { case .networkError: @@ -1344,17 +1391,23 @@ extension AdamantChatsProvider { case .notLogged: return ChatsProviderError.notLogged case .serverError(let e), .commonError(let e): - return ChatsProviderError.serverError(AdamantError(message: e)) + return ChatsProviderError.serverError( + .init(from: AdamantError(message: e)) + ) case .noEndpointsAvailable: - return ChatsProviderError.serverError(AdamantError( - message: error.localizedDescription - )) + return ChatsProviderError.serverError( + .init( + from: AdamantError( + message: error.localizedDescription + ) + ) + ) case .internalError(let message, _): return ChatsProviderError.internalError(AdamantError(message: message)) case .requestCancelled: return ChatsProviderError.requestCancelled case .none: - return ChatsProviderError.serverError(error) + return ChatsProviderError.serverError(.init(from: error)) } } } @@ -1363,29 +1416,38 @@ extension AdamantChatsProvider { extension AdamantChatsProvider { func getChatroomsController() -> NSFetchedResultsController { let request: NSFetchRequest = NSFetchRequest(entityName: Chatroom.entityName) - request.sortDescriptors = [NSSortDescriptor(key: "updatedAt", ascending: false), - NSSortDescriptor(key: "title", ascending: true)] + request.sortDescriptors = [ + NSSortDescriptor(key: "updatedAt", ascending: false), + NSSortDescriptor(key: "title", ascending: true) + ] request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ NSPredicate(format: "partner!=nil"), NSPredicate(format: "isForcedVisible = true OR isHidden = false"), NSPredicate(format: "isForcedVisible = true OR ANY transactions.showsChatroom = true") ]) - - let controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: stack.container.viewContext, sectionNameKeyPath: nil, cacheName: nil) + + let controller = NSFetchedResultsController( + fetchRequest: request, + managedObjectContext: stack.container.viewContext, + sectionNameKeyPath: nil, + cacheName: nil + ) return controller } - + func getChatroom(for adm: String) -> Chatroom? { let request: NSFetchRequest = NSFetchRequest(entityName: Chatroom.entityName) - request.sortDescriptors = [NSSortDescriptor(key: "updatedAt", ascending: false), - NSSortDescriptor(key: "title", ascending: true)] + request.sortDescriptors = [ + NSSortDescriptor(key: "updatedAt", ascending: false), + NSSortDescriptor(key: "title", ascending: true) + ] request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ NSPredicate(format: "partner!=nil"), NSPredicate(format: "partner.address CONTAINS[cd] %@", adm), NSPredicate(format: "isForcedVisible = true OR isHidden = false"), NSPredicate(format: "isForcedVisible = true OR ANY transactions.showsChatroom = true") ]) - + do { let result = try stack.container.viewContext.fetch(request) return result.first @@ -1393,38 +1455,43 @@ extension AdamantChatsProvider { return nil } } - - @MainActor func getChatController(for chatroom: Chatroom) -> NSFetchedResultsController { + + func getChatController(for chatroom: Chatroom) -> NSFetchedResultsController { guard let context = chatroom.managedObjectContext else { fatalError() } - + let request: NSFetchRequest = NSFetchRequest(entityName: "ChatTransaction") request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ NSPredicate(format: "chatroom = %@", chatroom), - NSPredicate(format: "isHidden == false")]) - request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true), - NSSortDescriptor(key: "transactionId", ascending: true)] + NSPredicate(format: "isHidden == false") + ]) + request.sortDescriptors = .sortChatTransactions(ascending: true) let controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil) - + return controller } - + func getUnreadMessagesController() -> NSFetchedResultsController { let request = NSFetchRequest(entityName: "ChatTransaction") request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ NSPredicate(format: "chatroom.isHidden == false"), NSPredicate(format: "isUnread == true"), - NSPredicate(format: "isHidden == false")]) - - request.sortDescriptors = [NSSortDescriptor.init(key: "date", ascending: false), - NSSortDescriptor(key: "transactionId", ascending: false)] - - let controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: stack.container.viewContext, sectionNameKeyPath: "chatroom.partner.address", cacheName: nil) - + NSPredicate(format: "isHidden == false") + ]) + + request.sortDescriptors = .sortChatTransactions(ascending: false) + + let controller = NSFetchedResultsController( + fetchRequest: request, + managedObjectContext: stack.container.viewContext, + sectionNameKeyPath: "chatroom.partner.address", + cacheName: nil + ) + return controller } - + /// Search transaction in local storage /// /// - Parameter id: Transacton ID @@ -1433,7 +1500,7 @@ extension AdamantChatsProvider { let request = NSFetchRequest(entityName: TransferTransaction.entityName) request.predicate = NSPredicate(format: "transactionId == %@", String(id)) request.fetchLimit = 1 - + do { let result = try context.fetch(request) return result.first @@ -1441,7 +1508,7 @@ extension AdamantChatsProvider { return nil } } - + /// Search transaction in local storage /// /// - Parameter id: Transacton ID @@ -1450,7 +1517,7 @@ extension AdamantChatsProvider { let request = NSFetchRequest(entityName: "BaseTransaction") request.predicate = NSPredicate(format: "transactionId == %@", String(id)) request.fetchLimit = 1 - + do { let result = try context.fetch(request) return result.first @@ -1479,7 +1546,7 @@ extension AdamantChatsProvider { if self.accountService.account == nil { throw ApiServiceError.accountNotFound } - + do { let transactions = try await apiService.getMessageTransactions( address: senderId, @@ -1487,17 +1554,17 @@ extension AdamantChatsProvider { offset: offset, waitsForConnectivity: waitsForConnectivity ).get() - + if transactions.count == 0 { return } - + await process( messageTransactions: transactions, senderId: senderId, privateKey: privateKey ) - + // MARK: Get more transactions if needed if transactions.count == self.apiTransactions { let newOffset: Int @@ -1506,7 +1573,7 @@ extension AdamantChatsProvider { } else { newOffset = self.apiTransactions } - + try await getTransactions( senderId: senderId, privateKey: privateKey, @@ -1520,27 +1587,25 @@ extension AdamantChatsProvider { throw error } } - + func loadTransactionsUntilFound( _ transactionId: String, recipient: String ) async throws { guard let address = accountService.account?.address, - let privateKey = accountService.keypair?.privateKey + let privateKey = accountService.keypair?.privateKey else { throw ApiServiceError.accountNotFound } - + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) context.parent = self.stack.container.viewContext - - guard getBaseTransactionFromDB(id: transactionId, context: context) == nil else { return } - + var transactions: [Transaction] = [] var offset = chatLoadedMessages[recipient] ?? 0 var needToRepeat = false var isFound = false - + repeat { let messages = try await apiGetChatMessages( address: address, @@ -1548,52 +1613,52 @@ extension AdamantChatsProvider { offset: offset, limit: chatTransactionsLimit )?.messages - + guard let messages = messages else { needToRepeat = false break } - + offset += messages.count transactions.append(contentsOf: messages) isFound = transactions.contains(where: { $0.id == UInt64(transactionId) }) needToRepeat = messages.count >= chatTransactionsLimit && !isFound } while needToRepeat - + guard isFound else { throw ApiServiceError.commonError( message: String.adamant.reply.longUnknownMessageError ) } - + chatLoadedMessages[recipient] = offset - + await process( messageTransactions: transactions, senderId: address, privateKey: privateKey ) - + // MARK: Get more transactions - + let messages = try await apiGetChatMessages( address: address, addressRecipient: recipient, offset: offset, limit: chatTransactionsLimit )?.messages - + guard let messages = messages, messages.count > 0 else { return } chatLoadedMessages[recipient] = offset + messages.count - + await process( messageTransactions: messages, senderId: address, privateKey: privateKey ) } - + /// - New unread messagess ids @discardableResult private func process( messageTransactions: [Transaction], @@ -1604,23 +1669,23 @@ extension AdamantChatsProvider { let transaction: Transaction let isOut: Bool } - + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) context.parent = self.stack.container.viewContext context.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyObjectTrumpMergePolicyType) - + // MARK: 1. Gather partner keys let mapped = messageTransactions.map({ DirectionedTransaction(transaction: $0, isOut: $0.senderId == senderId) }) let grouppedTransactions = Dictionary(grouping: mapped, by: { $0.isOut ? $0.transaction.recipientId : $0.transaction.senderId }) - + // MARK: 2. Gather Accounts var partners: [CoreDataAccount: [DirectionedTransaction]] = [:] - + let request = NSFetchRequest(entityName: CoreDataAccount.entityName) request.fetchLimit = grouppedTransactions.count let predicates = grouppedTransactions.keys.map { NSPredicate(format: "address = %@", $0) } request.predicate = NSCompoundPredicate(orPredicateWithSubpredicates: predicates) - + do { let results = try context.fetch(request) for account in results { @@ -1631,27 +1696,28 @@ extension AdamantChatsProvider { } catch { print("NSPredicate fetch error \(error.localizedDescription)") } - + // MARK: 2.5 Get accounts, that we did not found. if partners.count != grouppedTransactions.keys.count { - let foundedKeys = partners.keys.compactMap {$0.address} + let foundedKeys = partners.keys.compactMap { $0.address } let notFound = Set(grouppedTransactions.keys).subtracting(foundedKeys) var ids = [NSManagedObjectID]() - + for address in notFound { let transaction = grouppedTransactions[address]?.first let isOut = transaction?.isOut ?? false - - let publicKey = isOut - ? transaction?.transaction.recipientPublicKey - : transaction?.transaction.senderPublicKey - + + let publicKey = + isOut + ? transaction?.transaction.recipientPublicKey + : transaction?.transaction.senderPublicKey + do { let account = try await accountsProvider.getAccount( byAddress: address, publicKey: publicKey ?? "" ) - + ids.append(account.objectID) } catch AccountsProviderError.dummy(let dummyAccount) { ids.append(dummyAccount.objectID) @@ -1659,45 +1725,49 @@ extension AdamantChatsProvider { print(error) } } - + // Get in our context for id in ids { if let account = context.object(with: id) as? CoreDataAccount, - let address = account.address, - let transactions = grouppedTransactions[address] { + let address = account.address, + let transactions = grouppedTransactions[address] + { partners[account] = transactions } } } - + if partners.count != grouppedTransactions.keys.count { // TODO: Log this strange thing - print("Failed to get all accounts: Needed keys:\n\(grouppedTransactions.keys.joined(separator: "\n"))\nFounded Addresses: \(partners.keys.compactMap { $0.address }.joined(separator: "\n"))") + print( + "Failed to get all accounts: Needed keys:\n\(grouppedTransactions.keys.joined(separator: "\n"))\nFounded Addresses: \(partners.keys.compactMap { $0.address }.joined(separator: "\n"))" + ) } - + // MARK: 3. Process Chatrooms and Transactions var height: Int64 = 0 let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) privateContext.parent = context privateContext.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyObjectTrumpMergePolicyType) - + var newMessageTransactions = [ChatTransaction]() var transactionInProgress: [UInt64] = [] - + var reactions = 0 - + for (account, transactions) in partners { // We can't save whole context while we are mass creating MessageTransactions. guard let chatroom = account.chatroom else { continue } let privateChatroom = privateContext.object(with: chatroom.objectID) as! Chatroom - + // MARK: Transactions var messages = Set() - + for trs in transactions { transactionInProgress.append(trs.transaction.id) if let objectId = unconfirmedTransactions[trs.transaction.id], - let unconfirmed = context.object(with: objectId) as? ChatTransaction { + let unconfirmed = context.object(with: objectId) as? ChatTransaction + { confirmTransaction( unconfirmed, id: trs.transaction.id, @@ -1705,76 +1775,80 @@ extension AdamantChatsProvider { blockId: trs.transaction.blockId, confirmations: trs.transaction.confirmations ) - + let h = Int64(trs.transaction.height) if height < h { height = h } continue } - + // if transaction in pending status then ignore it if unconfirmedTransactionsBySignature.contains(trs.transaction.signature) { continue } - + let publicKey: String if trs.isOut { publicKey = account.publicKey ?? "" } else { publicKey = trs.transaction.senderPublicKey } - + if let partner = privateContext.object(with: account.objectID) as? BaseAccount, - let chatTransaction = await transactionService.chatTransaction( - from: trs.transaction, - isOutgoing: trs.isOut, - publicKey: publicKey, - privateKey: privateKey, - partner: partner, - removedMessages: self.removedMessages, - context: privateContext - ) { + let chatTransaction = await transactionService.chatTransaction( + from: trs.transaction, + isOutgoing: trs.isOut, + publicKey: publicKey, + privateKey: privateKey, + partner: partner, + removedMessages: self.removedMessages, + context: privateContext + ) + { if let transaction = chatTransaction as? RichMessageTransaction, - transaction.additionalType == .reaction { + transaction.additionalType == .reaction + { reactions += 1 } if height < chatTransaction.height { height = chatTransaction.height } - - let transactionExist = privateChatroom.transactions?.first(where: { message in - return (message as? ChatTransaction)?.txId == chatTransaction.txId - }) as? ChatTransaction - + + let transactionExist = + privateChatroom.transactions?.first(where: { message in + return (message as? ChatTransaction)?.txId == chatTransaction.txId + }) as? ChatTransaction + if !trs.isOut { if transactionExist == nil { newMessageTransactions.append(chatTransaction) } - + // Preset messages if account.isSystem, let address = account.address, let messages = AdamantContacts.messagesFor(address: address), let messageTransaction = chatTransaction as? MessageTransaction, let key = messageTransaction.message, - let systemMessage = messages.first(where: { key.range(of: $0.key) != nil })?.value { - + let systemMessage = messages.first(where: { key.range(of: $0.key) != nil })?.value + { + switch systemMessage.message { case .text(let text): messageTransaction.message = text - + case .markdownText(let text): messageTransaction.message = text messageTransaction.isMarkdown = true - + case .richMessage(let payload): messageTransaction.message = payload.serialized() } - + messageTransaction.silentNotification = systemMessage.silentNotification } } - + if transactionExist == nil { if (chatTransaction.blockId?.isEmpty ?? true) && (chatTransaction.amountValue ?? 0.0 > 0.0) { chatTransaction.statusEnum = .pending @@ -1788,16 +1862,16 @@ extension AdamantChatsProvider { } } } - + if !messages.isEmpty { privateChatroom.addToTransactions(messages as NSSet) } - + if let address = privateChatroom.partner?.address { chatroom.isHidden = self.blockList.contains(address) } } - + // MARK: 4. Unread messagess if let readedLastHeight = readedLastHeight { var unreadTransactions = newMessageTransactions.filter { $0.height > readedLastHeight } @@ -1813,7 +1887,7 @@ extension AdamantChatsProvider { trs.forEach { $0.isUnread = true } } } - + // MARK: 5. Dump new transactions if privateContext.hasChanges { do { @@ -1822,11 +1896,11 @@ extension AdamantChatsProvider { print(error) } } - + // MARK: 6. Save to main! do { let rooms = partners.keys.compactMap { $0.chatroom } - + if context.hasChanges { try context.save() await updateLastTransactionForChatrooms(rooms) @@ -1834,7 +1908,7 @@ extension AdamantChatsProvider { } catch { print(error) } - + // MARK: 7. Last message height if let lastHeight = receivedLastHeight { if lastHeight < height { @@ -1843,33 +1917,43 @@ extension AdamantChatsProvider { } else { updateLastHeight(height: height) } - + return (reactionsCount: reactions, totalCount: messageTransactions.count) } - + func updateLastHeight(height: Int64) { receivedLastHeight = height } - - @MainActor - func updateLastTransactionForChatrooms(_ rooms: [Chatroom]) { + + @MainActor + func updateLastTransactionForChatrooms(_ rooms: [Chatroom]) async { let viewContextChatrooms = Set(rooms).compactMap { self.stack.container.viewContext.object(with: $0.objectID) as? Chatroom } - + for chatroom in viewContextChatrooms { chatroom.updateLastTransaction() } + let addresses = await getMarkAdressesFromChain() + + for chatroom in viewContextChatrooms { + if let address = chatroom.partner?.address, + addresses.contains(address) + { + chatroom.markAsUnread() + try? chatroom.managedObjectContext?.save() + } + } } } // MARK: - Tools extension AdamantChatsProvider { - + func addUnconfirmed(transactionId id: UInt64, managedObjectId: NSManagedObjectID) { self.unconfirmedTransactions[id] = managedObjectId } - + /// Check if message is valid for sending func validateMessage(_ message: AdamantMessage) -> ValidateMessageResult { switch message { @@ -1877,28 +1961,28 @@ extension AdamantChatsProvider { if text.count == 0 { return .empty } - + if Double(text.count) * 1.5 > 20000.0 { return .tooLong } - + return .isValid - + case .richMessage(let payload): let text = payload.serialized() - + if text.count == 0 { return .empty } - + if Double(text.count) * 1.5 > 20000.0 { return .tooLong } - + return .isValid } } - + /// Confirm transactions /// /// - Parameters: @@ -1908,52 +1992,78 @@ extension AdamantChatsProvider { if transaction.isConfirmed { return } - + transaction.height = height transaction.blockId = blockId transaction.confirmations = confirmations - + if !blockId.isEmpty { self.unconfirmedTransactions.removeValue(forKey: id) } - + if let lastHeight = receivedLastHeight, lastHeight < height { self.receivedLastHeight = height } - + if height != .zero { transaction.isConfirmed = true } transaction.statusEnum = .delivered } - + func blockChat(with address: String) { if !self.blockList.contains(address) { self.blockList.append(address) - + if self.accountService.hasStayInAccount { - self.securedStore.set(blockList, for: StoreKey.accountService.blockList) + self.SecureStore.set(blockList, for: StoreKey.accountService.blockList) } } } - + func removeMessage(with id: String) { if !self.removedMessages.contains(id) { self.removedMessages.append(id) - + markTransactionAsHidden(id: id) + if self.accountService.hasStayInAccount { - self.securedStore.set(removedMessages, for: StoreKey.accountService.removedMessages) + self.SecureStore.set(removedMessages, for: StoreKey.accountService.removedMessages) } } } - + + func markMessageAsRead(chatroom: Chatroom, message: String) { + chatroom.managedObjectContext?.perform { [weak self] in + guard let self else { return } + chatroom.markMessageAsReaded(chatMessageId: message, stack: self.stack) + try? chatroom.managedObjectContext?.save() + } + } + func markChatAsRead(chatroom: Chatroom) { chatroom.managedObjectContext?.perform { chatroom.markAsReaded() try? chatroom.managedObjectContext?.save() } } - + + private func markTransactionAsHidden(id: String) { + let request = NSFetchRequest(entityName: "ChatTransaction") + request.predicate = NSPredicate(format: "transactionId == %@", String(id)) + request.fetchLimit = 1 + + let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + privateContext.parent = stack.container.viewContext + + privateContext.performAndWait { + if let transaction = (try? privateContext.fetch(request))?.first { + transaction.isHidden = true + try? transaction.managedObjectContext?.save() + transaction.chatroom?.updateLastTransaction() + } + } + } + private func onConnectionToTheInternetRestored() { onConnectionToTheInternetRestoredTasks.forEach { $0() } onConnectionToTheInternetRestoredTasks = [] diff --git a/Adamant/Services/DataProviders/AdamantTransfersProvider+backgroundFetch.swift b/Adamant/Services/DataProviders/AdamantTransfersProvider+backgroundFetch.swift index 7397774eb..91b2d16cd 100644 --- a/Adamant/Services/DataProviders/AdamantTransfersProvider+backgroundFetch.swift +++ b/Adamant/Services/DataProviders/AdamantTransfersProvider+backgroundFetch.swift @@ -6,33 +6,33 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation extension AdamantTransfersProvider: BackgroundFetchService { func fetchBackgroundData(notificationsService: NotificationsService) async -> FetchResult { - guard let address: String = securedStore.get(StoreKey.transfersProvider.address) else { + guard let address: String = SecureStore.get(StoreKey.transfersProvider.address) else { return .failed } - + var lastHeight: Int64? - if let raw: String = securedStore.get(StoreKey.transfersProvider.receivedLastHeight) { + if let raw: String = SecureStore.get(StoreKey.transfersProvider.receivedLastHeight) { lastHeight = Int64(raw) } else { lastHeight = nil } - + var notifiedCount = 0 - if let raw: String = securedStore.get(StoreKey.transfersProvider.notifiedLastHeight), let notifiedHeight = Int64(raw), let h = lastHeight { + if let raw: String = SecureStore.get(StoreKey.transfersProvider.notifiedLastHeight), let notifiedHeight = Int64(raw), let h = lastHeight { if h < notifiedHeight { lastHeight = notifiedHeight - - if let raw: String = securedStore.get(StoreKey.transfersProvider.notifiedTransfersCount), let count = Int(raw) { + + if let raw: String = SecureStore.get(StoreKey.transfersProvider.notifiedTransfersCount), let count = Int(raw) { notifiedCount = count } } } - + do { let transactions = try await apiService.getTransactions( forAccount: address, @@ -42,38 +42,38 @@ extension AdamantTransfersProvider: BackgroundFetchService { limit: 100, waitsForConnectivity: false ).get() - - let total = transactions.filter({$0.recipientId == address}).count - + + let total = transactions.filter({ $0.recipientId == address }).count + guard total > 0 else { return .noData } - - securedStore.set( + + SecureStore.set( String(total + notifiedCount), for: StoreKey.transfersProvider.notifiedTransfersCount ) - - if var newLastHeight = transactions.map({$0.height}).sorted().last { - newLastHeight += 1 // Server will return new transactions including this one - securedStore.set( + + if var newLastHeight = transactions.map({ $0.height }).sorted().last { + newLastHeight += 1 // Server will return new transactions including this one + SecureStore.set( String(newLastHeight), for: StoreKey.transfersProvider.notifiedLastHeight ) } - + await notificationsService.showNotification( title: NotificationStrings.newTransferTitle, body: NotificationStrings.newTransferBody(total + notifiedCount), type: .newTransactions(count: total) ) - + return .newData } catch { return .failed } } - + func dropStateData() { - securedStore.remove(StoreKey.transfersProvider.notifiedLastHeight) - securedStore.remove(StoreKey.transfersProvider.notifiedTransfersCount) + SecureStore.remove(StoreKey.transfersProvider.notifiedLastHeight) + SecureStore.remove(StoreKey.transfersProvider.notifiedTransfersCount) } } diff --git a/Adamant/Services/DataProviders/AdamantTransfersProvider.swift b/Adamant/Services/DataProviders/AdamantTransfersProvider.swift index c64361867..087a24095 100644 --- a/Adamant/Services/DataProviders/AdamantTransfersProvider.swift +++ b/Adamant/Services/DataProviders/AdamantTransfersProvider.swift @@ -6,59 +6,71 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation -@preconcurrency import CoreData import Combine import CommonKit +@preconcurrency import CoreData +import Foundation actor AdamantTransfersProvider: TransfersProvider { // MARK: Constants static let transferFee: Decimal = Decimal(sign: .plus, exponent: -1, significand: 5) - + // MARK: Dependencies let apiService: AdamantApiServiceProtocol private let stack: CoreDataStack private let adamantCore: AdamantCore private let accountService: AccountService private let accountsProvider: AccountsProvider - let securedStore: SecuredStore + let SecureStore: SecureStore private let transactionService: ChatTransactionService weak var chatsProvider: ChatsProvider? - - @ObservableValue private(set) var state: State = .empty - var stateObserver: AnyObservable { $state.eraseToAnyPublisher() } + + @ObservableValue private(set) var state: DataProviderState = .empty + var stateObserver: AnyObservable { $state.eraseToAnyPublisher() } private(set) var isInitiallySynced: Bool = false private(set) var receivedLastHeight: Int64? private(set) var readedLastHeight: Int64? private(set) var hasTransactions: Bool = false private let apiTransactions = 100 - - private var unconfirmedTransactions: [UInt64:NSManagedObjectID] = [:] + + private var unconfirmedTransactions: [UInt64: NSManagedObjectID] = [:] private var subscriptions = Set() - + var offsetTransactions = 0 - + // MARK: Tools - + /// Free stateSemaphore before calling this method, or you will deadlock. - private func setState(_ state: State, previous prevState: State, notify: Bool = false) { + private func setState(_ state: DataProviderState, previous prevState: DataProviderState, notify: Bool = false) { self.state = state - + if notify { switch prevState { case .failedToUpdate: - NotificationCenter.default.post(name: Notification.Name.AdamantTransfersProvider.stateChanged, object: nil, userInfo: [AdamantUserInfoKey.TransfersProvider.newState: state, - AdamantUserInfoKey.TransfersProvider.prevState: prevState]) - + NotificationCenter.default.post( + name: Notification.Name.AdamantTransfersProvider.stateChanged, + object: nil, + userInfo: [ + AdamantUserInfoKey.TransfersProvider.newState: state, + AdamantUserInfoKey.TransfersProvider.prevState: prevState + ] + ) + default: if prevState != self.state { - NotificationCenter.default.post(name: Notification.Name.AdamantTransfersProvider.stateChanged, object: nil, userInfo: [AdamantUserInfoKey.TransfersProvider.newState: state, - AdamantUserInfoKey.TransfersProvider.prevState: prevState]) + NotificationCenter.default.post( + name: Notification.Name.AdamantTransfersProvider.stateChanged, + object: nil, + userInfo: [ + AdamantUserInfoKey.TransfersProvider.newState: state, + AdamantUserInfoKey.TransfersProvider.prevState: prevState + ] + ) } } } } - + // MARK: Lifecycle init( apiService: AdamantApiServiceProtocol, @@ -66,7 +78,7 @@ actor AdamantTransfersProvider: TransfersProvider { adamantCore: AdamantCore, accountService: AccountService, accountsProvider: AccountsProvider, - securedStore: SecuredStore, + SecureStore: SecureStore, transactionService: ChatTransactionService, chatsProvider: ChatsProvider ) { @@ -75,27 +87,28 @@ actor AdamantTransfersProvider: TransfersProvider { self.adamantCore = adamantCore self.accountService = accountService self.accountsProvider = accountsProvider - self.securedStore = securedStore + self.SecureStore = SecureStore self.transactionService = transactionService self.chatsProvider = chatsProvider - + Task { await addObservers() } } - + private func addObservers() { NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedIn, object: nil) .sink { [weak self] notification in - let loggedAddress = notification + let loggedAddress = + notification .userInfo?[AdamantUserInfoKey.AccountService.loggedAccountAddress] as? String - + await self?.userLoggedInAction(loggedAddress) } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedOut, object: nil) .sink { [weak self] _ in @@ -103,12 +116,12 @@ actor AdamantTransfersProvider: TransfersProvider { } .store(in: &subscriptions) } - + // MARK: - Notifications action - + private func userLoggedInAction(_ loggedAddress: String?) async { - let store = securedStore - + let store = SecureStore + guard let loggedAddress = loggedAddress else { store.remove(StoreKey.transfersProvider.address) store.remove(StoreKey.transfersProvider.receivedLastHeight) @@ -116,7 +129,7 @@ actor AdamantTransfersProvider: TransfersProvider { dropStateData() return } - + if let savedAddress: String = store.get(StoreKey.transfersProvider.address), savedAddress == loggedAddress { if let raw: String = store.get(StoreKey.transfersProvider.readedLastHeight), let h = Int64(raw) { readedLastHeight = h @@ -127,16 +140,16 @@ actor AdamantTransfersProvider: TransfersProvider { dropStateData() store.set(loggedAddress, for: StoreKey.transfersProvider.address) } - + await loadFirstTransactions() } - + private func loadFirstTransactions() async { guard let loggedAddress = accountService.account?.address else { return } - + do { setState(.updating, previous: .empty, notify: false) - + _ = try await getTransactions( forAccount: loggedAddress, type: .send, @@ -144,32 +157,32 @@ actor AdamantTransfersProvider: TransfersProvider { limit: apiTransactions, orderByTime: true ) - + offsetTransactions += apiTransactions if !isInitiallySynced { isInitiallySynced = true NotificationCenter.default.post(name: Notification.Name.AdamantTransfersProvider.initialSyncFinished, object: self) } - + setState(.upToDate, previous: .updating, notify: false) } catch { setState(.failedToUpdate(error), previous: .updating, notify: false) } } - + private func userLogOutAction() { // Drop everything reset() - + // BackgroundFetch dropStateData() } - + deinit { NotificationCenter.default.removeObserver(self) } - + func setChatsProvider(_ chatsProvider: ChatsProvider?) { self.chatsProvider = chatsProvider } @@ -181,24 +194,24 @@ extension AdamantTransfersProvider { reset(notify: false) _ = await update() } - + func update() async -> TransfersProviderResult? { if state == .updating { return nil } - + let prevState = state state = .updating - + guard let address = accountService.account?.address else { self.setState(.failedToUpdate(TransfersProviderError.notLogged), previous: prevState) return .failure(TransfersProviderError.notLogged) } - + // MARK: 3. Get transactions - + let prevHeight = receivedLastHeight - + await getTransactions( forAccount: address, type: .send, @@ -206,33 +219,33 @@ extension AdamantTransfersProvider { offset: nil, waitsForConnectivity: true ) - + // MARK: 4. Check - + switch state { case .empty, .updating, .upToDate: setState(.upToDate, previous: prevState) - + if prevHeight != receivedLastHeight, - let h = receivedLastHeight { + let h = receivedLastHeight + { NotificationCenter.default.post( name: Notification.Name.AdamantChatsProvider.newUnreadMessages, object: self, - userInfo: [AdamantUserInfoKey.TransfersProvider.lastTransactionHeight:h] + userInfo: [AdamantUserInfoKey.TransfersProvider.lastTransactionHeight: h] ) } - + if let h = receivedLastHeight { readedLastHeight = h } else { readedLastHeight = nil } - - let store = securedStore + + let store = SecureStore // Received if let h = receivedLastHeight { - if - let raw: String = store.get(StoreKey.transfersProvider.receivedLastHeight), + if let raw: String = store.get(StoreKey.transfersProvider.receivedLastHeight), let prev = Int64(raw) { if h > prev { @@ -242,11 +255,10 @@ extension AdamantTransfersProvider { store.set(String(h), for: StoreKey.transfersProvider.receivedLastHeight) } } - + // Readed if let h = readedLastHeight { - if - let raw: String = store.get(StoreKey.transfersProvider.readedLastHeight), + if let raw: String = store.get(StoreKey.transfersProvider.readedLastHeight), let prev = Int64(raw) { if h > prev { @@ -256,80 +268,83 @@ extension AdamantTransfersProvider { store.set(String(h), for: StoreKey.transfersProvider.readedLastHeight) } } - + if !isInitiallySynced { isInitiallySynced = true NotificationCenter.default.post(name: Notification.Name.AdamantTransfersProvider.initialSyncFinished, object: self) } - + return .success - - case .failedToUpdate(let error): // Processing failed + + case .failedToUpdate(let error): // Processing failed let err: TransfersProviderError - + switch error { case let error as ApiServiceError: switch error { case .notLogged: err = .notLogged - + case .accountNotFound: err = .accountNotFound(address: address) - + case .serverError, .commonError, .noEndpointsAvailable: err = .serverError(error) - + case .internalError(let message, _): err = .dependencyError(message: message) - + case .networkError: err = .networkError - + case .requestCancelled: err = .requestCancelled } - + default: - err = TransfersProviderError.internalError(message: String.adamant.sharedErrors.internalError(message: error.localizedDescription), error: error) + err = TransfersProviderError.internalError( + message: String.adamant.sharedErrors.internalError(message: error.localizedDescription), + error: error + ) } - + return .failure(err) } } - + func reset() { reset(notify: true) } - + private func reset(notify: Bool) { offsetTransactions = 0 hasTransactions = false isInitiallySynced = false let prevState = self.state - setState(.updating, previous: prevState, notify: false) // Block update calls - + setState(.updating, previous: prevState, notify: false) // Block update calls + // Drop props receivedLastHeight = nil readedLastHeight = nil - + // Drop store - securedStore.remove(StoreKey.transfersProvider.address) - securedStore.remove(StoreKey.transfersProvider.receivedLastHeight) - securedStore.remove(StoreKey.transfersProvider.readedLastHeight) - + SecureStore.remove(StoreKey.transfersProvider.address) + SecureStore.remove(StoreKey.transfersProvider.receivedLastHeight) + SecureStore.remove(StoreKey.transfersProvider.readedLastHeight) + // Drop CoreData -// let request = NSFetchRequest(entityName: TransferTransaction.entityName) -// let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) -// context.parent = stack.container.viewContext -// -// if let result = try? context.fetch(request) { -// for obj in result { -// context.delete(obj) -// } -// -// try? context.save() -// } - + // let request = NSFetchRequest(entityName: TransferTransaction.entityName) + // let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + // context.parent = stack.container.viewContext + // + // if let result = try? context.fetch(request) { + // for obj in result { + // context.delete(obj) + // } + // + // try? context.save() + // } + // Set state setState(.empty, previous: prevState, notify: notify) } @@ -340,36 +355,48 @@ extension AdamantTransfersProvider { // MARK: Controllers func transfersController() -> NSFetchedResultsController { let request = NSFetchRequest(entityName: TransferTransaction.entityName) - request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false), - NSSortDescriptor(key: "transactionId", ascending: false)] - let controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: stack.container.viewContext, sectionNameKeyPath: nil, cacheName: nil) - + request.sortDescriptors = .sortChatTransactions(ascending: false) + let controller = NSFetchedResultsController( + fetchRequest: request, + managedObjectContext: stack.container.viewContext, + sectionNameKeyPath: nil, + cacheName: nil + ) + return controller } - + func transfersController(for account: CoreDataAccount) -> NSFetchedResultsController { let request = NSFetchRequest(entityName: TransferTransaction.entityName) - request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false), - NSSortDescriptor(key: "transactionId", ascending: false)] + request.sortDescriptors = .sortChatTransactions(ascending: false) request.predicate = NSPredicate(format: "partner = %@", account) - - let controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: stack.container.viewContext, sectionNameKeyPath: nil, cacheName: nil) + + let controller = NSFetchedResultsController( + fetchRequest: request, + managedObjectContext: stack.container.viewContext, + sectionNameKeyPath: nil, + cacheName: nil + ) try! controller.performFetch() return controller } - + func unreadTransfersController() -> NSFetchedResultsController { let request = NSFetchRequest(entityName: TransferTransaction.entityName) request.predicate = NSPredicate(format: "isUnread == true") - request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: false), - NSSortDescriptor(key: "transactionId", ascending: false)] - let controller = NSFetchedResultsController(fetchRequest: request, managedObjectContext: stack.container.viewContext, sectionNameKeyPath: nil, cacheName: nil) - + request.sortDescriptors = .sortChatTransactions(ascending: false) + let controller = NSFetchedResultsController( + fetchRequest: request, + managedObjectContext: stack.container.viewContext, + sectionNameKeyPath: nil, + cacheName: nil + ) + return controller } - + // MARK: Sending Funds - + // Wrapper func transferFunds( toAddress recipient: String, @@ -386,13 +413,13 @@ extension AdamantTransfersProvider { replyToMessageId: replyToMessageId ) } - + return try await transferFundsInternal( toAddress: recipient, amount: amount ) } - + private func transferFundsInternal( toAddress recipient: String, amount: Decimal, @@ -403,49 +430,50 @@ extension AdamantTransfersProvider { guard let loggedAccount = accountService.account, let keypair = accountService.keypair else { throw TransfersProviderError.notLogged } - + guard loggedAccount.balance > amount + Self.transferFee else { throw TransfersProviderError.notEnoughMoney } - + // MARK: 1. Get recipient let recipientAccount: CoreDataAccount - + do { recipientAccount = try await accountsProvider.getAccount(byAddress: recipient) } catch let error as AccountsProviderError { switch error { case .notFound, .invalidAddress, .notInitiated, .dummy: throw TransfersProviderError.accountNotFound(address: recipient) - + case .serverError(let error): throw TransfersProviderError.serverError(error) - + case .networkError: throw TransfersProviderError.networkError } } - + guard let recipientPublicKey = recipientAccount.publicKey else { throw TransfersProviderError.accountNotFound(address: recipient) } - + // MARK: 2. Chatroom let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) context.parent = stack.container.viewContext - + guard let id = recipientAccount.chatroom?.objectID, - let chatroom = context.object(with: id) as? Chatroom, - let partner = context.object(with: recipientAccount.objectID) as? BaseAccount + let chatroom = context.object(with: id) as? Chatroom, + let partner = context.object(with: recipientAccount.objectID) as? BaseAccount else { throw TransfersProviderError.accountNotFound(address: recipient) } - + // MARK: 3. Transaction let transaction = TransferTransaction(context: context) transaction.amount = amount as NSDecimalNumber transaction.date = Date() as NSDate + transaction.timestampMs = transaction.timeIntervalMillisecondsSince1970 transaction.recipientId = recipient transaction.senderId = loggedAccount.address transaction.type = Int16(TransactionType.chatMessage.rawValue) @@ -458,13 +486,14 @@ extension AdamantTransfersProvider { transaction.partner = partner transaction.transactionId = UUID().uuidString transaction.replyToId = replyToMessageId - + chatroom.addToTransactions(transaction) - + // MARK: 4. Last in if let lastTransaction = chatroom.lastTransaction { if let dateA = lastTransaction.date as Date?, let dateB = transaction.date as Date?, - dateA.compare(dateB) == ComparisonResult.orderedAscending { + dateA.compare(dateB) == ComparisonResult.orderedAscending + { chatroom.lastTransaction = transaction chatroom.updatedAt = transaction.date } @@ -472,37 +501,41 @@ extension AdamantTransfersProvider { chatroom.lastTransaction = transaction chatroom.updatedAt = transaction.date } - + // MARK: 5. Save unconfirmed transaction do { try context.save() } catch { throw TransfersProviderError.internalError(message: String.adamant.sharedErrors.unknownError, error: error) } - + // MARK: 6. Encode - - let asset = replyToMessageId == nil - ? comment - : RichMessageReply( - replyto_id: replyToMessageId ?? "", - reply_message: comment - ).serialized() - - guard let encodedMessage = adamantCore.encodeMessage( - asset, - recipientPublicKey: recipientPublicKey, - privateKey: keypair.privateKey) + + let asset = + replyToMessageId == nil + ? comment + : RichMessageReply( + replyto_id: replyToMessageId ?? "", + reply_message: comment + ).serialized() + + guard + let encodedMessage = adamantCore.encodeMessage( + asset, + recipientPublicKey: recipientPublicKey, + privateKey: keypair.privateKey + ) else { throw TransfersProviderError.internalError(message: "Failed to encode message", error: nil) } - + // MARK: 7. Send - - let type: ChatType = replyToMessageId == nil - ? .message - : .richMessage - + + let type: ChatType = + replyToMessageId == nil + ? .message + : .richMessage + let signedTransaction = try? adamantCore.makeSendMessageTransaction( senderId: loggedAccount.address, recipientId: recipient, @@ -510,21 +543,22 @@ extension AdamantTransfersProvider { message: encodedMessage.message, type: type, nonce: encodedMessage.nonce, - amount: amount + amount: amount, + date: AdmWalletService.correctedDate ) - + guard let signedTransaction = signedTransaction else { throw TransfersProviderError.internalError( message: InternalAPIError.signTransactionFailed.localizedDescription, error: nil ) } - + do { let id = try await apiService.sendMessageTransaction(transaction: signedTransaction).get() transaction.transactionId = String(id) await chatsProvider?.addUnconfirmed(transactionId: id, managedObjectId: transaction.objectID) - + do { try context.save() } catch { @@ -533,7 +567,7 @@ extension AdamantTransfersProvider { error: error ) } - + if let trs = stack.container.viewContext.object(with: transaction.objectID) as? TransferTransaction { return trs } else { @@ -545,11 +579,11 @@ extension AdamantTransfersProvider { } catch { transaction.statusEnum = MessageStatus.failed try? context.save() - + throw TransfersProviderError.serverError(error) } } - + private func transferFundsInternal( toAddress recipient: String, amount: Decimal @@ -558,37 +592,37 @@ extension AdamantTransfersProvider { guard let loggedAccount = accountService.account, let keypair = accountService.keypair else { throw TransfersProviderError.notLogged } - + guard loggedAccount.balance > amount + Self.transferFee else { throw TransfersProviderError.notEnoughMoney } - + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) context.parent = stack.container.viewContext - + // MARK: 1. Get recipient - + var recipientAccount: BaseAccount? - + do { recipientAccount = try await accountsProvider.getAccount(byAddress: recipient) } catch let error as AccountsProviderError { switch error { case .dummy(let account): recipientAccount = account - + case .notFound, .notInitiated: - + do { recipientAccount = try await accountsProvider.getDummyAccount(for: recipient) } catch let error as AccountsProviderDummyAccountError { switch error { case .foundRealAccount(let account): recipientAccount = account - + case .invalidAddress(let address): throw TransfersProviderError.accountNotFound(address: address) - + case .internalError(let error): throw TransfersProviderError.internalError( message: error.localizedDescription, @@ -596,68 +630,74 @@ extension AdamantTransfersProvider { ) } } - + case .invalidAddress: throw TransfersProviderError.accountNotFound(address: recipient) - + case .serverError(let error): throw TransfersProviderError.serverError(error) - + case .networkError: throw TransfersProviderError.networkError } } catch { throw error } - + let backgroundAccount: BaseAccount if let acc = recipientAccount, - let obj = context.object(with: acc.objectID) as? BaseAccount { + let obj = context.object(with: acc.objectID) as? BaseAccount + { backgroundAccount = obj } else { throw TransfersProviderError.accountNotFound(address: recipient) } - + // MARK: 2. Create transaction let signedTransaction = adamantCore.createTransferTransaction( senderId: loggedAccount.address, recipientId: recipient, keypair: keypair, - amount: amount + amount: amount, + date: AdmWalletService.correctedDate ) - + guard let signedTransaction = signedTransaction else { throw TransfersProviderError.internalError( message: InternalAPIError.signTransactionFailed.localizedDescription, error: InternalAPIError.signTransactionFailed ) } - + let locallyID = signedTransaction.generateId() ?? UUID().uuidString let transaction = TransferTransaction(context: context) transaction.amount = amount as NSDecimalNumber transaction.date = Date() as NSDate + transaction.timestampMs = transaction.timeIntervalMillisecondsSince1970 transaction.recipientId = recipient transaction.senderId = loggedAccount.address transaction.type = Int16(TransactionType.send.rawValue) transaction.isOutgoing = true transaction.showsChatroom = false transaction.fee = Self.transferFee as NSDecimalNumber - + transaction.transactionId = locallyID transaction.blockId = nil transaction.chatMessageId = locallyID transaction.statusEnum = MessageStatus.pending - + // MARK: 3. Chatroom backgroundAccount.addToTransfers(transaction) - - if let coreDataAccount = backgroundAccount as? CoreDataAccount, let id = coreDataAccount.chatroom?.objectID, let chatroom = context.object(with: id) as? Chatroom { + + if let coreDataAccount = backgroundAccount as? CoreDataAccount, let id = coreDataAccount.chatroom?.objectID, + let chatroom = context.object(with: id) as? Chatroom + { chatroom.addToTransactions(transaction) - + if let lastTransaction = chatroom.lastTransaction { if let dateA = lastTransaction.date as Date?, let dateB = transaction.date as Date?, - dateA.compare(dateB) == ComparisonResult.orderedAscending { + dateA.compare(dateB) == ComparisonResult.orderedAscending + { chatroom.lastTransaction = transaction chatroom.updatedAt = transaction.date } @@ -666,7 +706,7 @@ extension AdamantTransfersProvider { chatroom.updatedAt = transaction.date } } - + // MARK: 4. Save unconfirmed transaction do { try context.save() @@ -676,17 +716,17 @@ extension AdamantTransfersProvider { error: error ) } - + // MARK: 5. Send do { let id = try await apiService.transferFunds( transaction: signedTransaction ).get() - + transaction.transactionId = String(id) - + self.unconfirmedTransactions[id] = transaction.objectID - + do { try context.save() } catch { @@ -695,7 +735,7 @@ extension AdamantTransfersProvider { error: error ) } - + if let trs = self.stack.container.viewContext.object(with: transaction.objectID) as? AdamantTransactionDetails { return trs } else { @@ -708,9 +748,9 @@ extension AdamantTransfersProvider { throw TransfersProviderError.serverError(error) } } - + // MARK: Getting & refreshing transfers - + /// Search transaction in local storage /// /// - Parameter id: Transacton ID @@ -719,7 +759,7 @@ extension AdamantTransfersProvider { let request = NSFetchRequest(entityName: TransferTransaction.entityName) request.predicate = NSPredicate(format: "transactionId == %@", String(id)) request.fetchLimit = 1 - + do { let result = try stack.container.viewContext.fetch(request) return result.first @@ -727,7 +767,7 @@ extension AdamantTransfersProvider { return nil } } - + /// Search transaction in local storage /// /// - Parameter id: Transacton ID, context: NSManagedObjectContext @@ -736,7 +776,7 @@ extension AdamantTransfersProvider { let request = NSFetchRequest(entityName: TransferTransaction.entityName) request.predicate = NSPredicate(format: "transactionId == %@", String(id)) request.fetchLimit = 1 - + do { let result = try context.fetch(request) return result.first @@ -753,35 +793,35 @@ extension AdamantTransfersProvider { guard let transfer = getTransfer(id: id) else { throw TransfersProviderError.transactionNotFound(id: id) } - + guard let intId = UInt64(id) else { throw TransfersProviderError.internalError( message: "Can't parse transaction id: \(id)", error: nil ) } - + do { let transaction = try await apiService.getTransaction(id: intId).get() - + guard transfer.confirmations != transaction.confirmations else { return } - + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) context.parent = self.stack.container.viewContext - + guard let trsfr = context.object(with: transfer.objectID) as? TransferTransaction else { throw TransfersProviderError.internalError( message: "Failed to update transaction: CoreData context changed", error: nil ) } - + trsfr.confirmations = transaction.confirmations trsfr.blockId = transaction.blockId trsfr.isConfirmed = transaction.confirmations > 0 ? true : false - + do { try context.save() return @@ -804,7 +844,7 @@ extension AdamantTransfersProvider { case accountNotFound(address: String) case error(Error) } - + /// Get transactions /// /// - Parameters: @@ -819,7 +859,7 @@ extension AdamantTransfersProvider { offset: Int?, waitsForConnectivity: Bool ) async { - + do { let transactions = try await apiService.getTransactions( forAccount: account, @@ -829,18 +869,18 @@ extension AdamantTransfersProvider { limit: self.apiTransactions, waitsForConnectivity: waitsForConnectivity ).get() - + guard transactions.count > 0 else { return } - + // MARK: 2. Process transactions in background - + await processRawTransactions( transactions, currentAddress: account ) - + // MARK: 3. Get more transactions if transactions.count == self.apiTransactions { let newOffset: Int @@ -849,7 +889,7 @@ extension AdamantTransfersProvider { } else { newOffset = self.apiTransactions } - + await self.getTransactions( forAccount: account, type: type, @@ -862,7 +902,7 @@ extension AdamantTransfersProvider { setState(.failedToUpdate(error), previous: .updating) } } - + func getTransactions( forAccount account: String, type: TransactionType, @@ -879,41 +919,41 @@ extension AdamantTransfersProvider { orderByTime: orderByTime, waitsForConnectivity: true ).get() - + guard transactions.count > 0 else { return 0 } - + // MARK: 2. Process transactions in background - + await processRawTransactions( transactions, currentAddress: account ) - + return transactions.count } - + func updateOffsetTransactions(_ value: Int) { offsetTransactions = value } - + private func processRawTransactions( _ transactions: [Transaction], currentAddress address: String ) async { - + // MARK: 0. Transactions? guard transactions.count > 0 else { return } - + hasTransactions = true - + // MARK: 1. Collect all partners var partnerIds: Set = [] var partnerPublicKey: [String: String] = [:] - + for t in transactions { if t.senderId == address { partnerIds.insert(t.recipientId) @@ -923,15 +963,15 @@ extension AdamantTransfersProvider { partnerPublicKey[t.senderId] = t.senderPublicKey } } - + // MARK: 2. Let AccountProvider get all partners from server. var errors: [ProcessingResult] = [] - + var ignorList: Set = [] - + for id in partnerIds { let publicKey = partnerPublicKey[id] ?? "" - + do { _ = try await accountsProvider.getAccount( byAddress: id, @@ -941,7 +981,7 @@ extension AdamantTransfersProvider { switch error { case .dummy: break - + case .notFound, .invalidAddress, .notInitiated: do { _ = try await accountsProvider.getDummyAccount(for: id) @@ -949,17 +989,17 @@ extension AdamantTransfersProvider { switch error { case .foundRealAccount: break - + case .invalidAddress(let address): ignorList.insert(address) - + case .internalError(let error): errors.append(ProcessingResult.error(error)) } } catch { ignorList.insert(id) } - + case .networkError(let error), .serverError(let error): errors.append(ProcessingResult.error(error)) } @@ -967,23 +1007,23 @@ extension AdamantTransfersProvider { ignorList.insert(id) } } - + // MARK: 2.5. If we have any errors - drop processing. if let error = errors.first { print(error) return } - + ignorList.forEach { address in partnerIds.remove(address) } - + // MARK: 3. Create private context, and process transactions let contextPrivate = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) contextPrivate.parent = self.stack.container.viewContext contextPrivate.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyObjectTrumpMergePolicyType) - - var partners: [String:BaseAccount] = [:] + + var partners: [String: BaseAccount] = [:] for id in partnerIds { let request = NSFetchRequest(entityName: BaseAccount.baseEntityName) request.predicate = NSPredicate(format: "address == %@", id) @@ -992,67 +1032,68 @@ extension AdamantTransfersProvider { partners[id] = partner } } - + var transfers = [TransferTransaction]() var height: Int64 = 0 var transactionInProgress: [UInt64] = [] - + for t in transactions { - + if ignorList.contains(t.senderId) || ignorList.contains(t.recipientId) { continue } - + transactionInProgress.append(t.id) if let objectId = unconfirmedTransactions[t.id], - let transaction = contextPrivate.object(with: objectId) as? TransferTransaction { + let transaction = contextPrivate.object(with: objectId) as? TransferTransaction + { transaction.isConfirmed = true transaction.height = t.height transaction.blockId = t.blockId transaction.confirmations = t.confirmations transaction.statusEnum = .delivered transaction.fee = t.fee as NSDecimalNumber - + unconfirmedTransactions.removeValue(forKey: t.id) - + let h = Int64(t.height) if height < h { height = h } - + continue } - + let isOutgoing = t.senderId == address let partnerId = isOutgoing ? t.recipientId : t.senderId let partner = partners[partnerId] - + let transfer = await transactionService.transferTransaction( from: t, isOut: isOutgoing, partner: partner, context: contextPrivate ) - + transfer.isOutgoing = isOutgoing - + if let partner = partners[partnerId] { transfer.partner = partner } - + if t.height > height { height = t.height } - + transfers.append(transfer) } - + // MARK: 4. Check lastHeight // API returns transactions from lastHeight INCLUDING transaction with height == lastHeight, so +1 - + if height > 0 { let uH = Int64(height + 1) - + if let lastHeight = receivedLastHeight { if lastHeight < uH { self.receivedLastHeight = uH @@ -1064,17 +1105,17 @@ extension AdamantTransfersProvider { // MARK: 5. Unread transactions if let unreadedHeight = readedLastHeight { let unreadTransactions = transfers.filter { !$0.isOutgoing && $0.height > unreadedHeight } - + if unreadTransactions.count > 0 { unreadTransactions.forEach { $0.isUnread = true } Set(unreadTransactions.compactMap { $0.chatroom }).forEach { $0.hasUnreadMessages = true } } } - + // MARK: 6. Dump transactions to viewContext do { let rooms = transfers.compactMap { $0.chatroom } - + if contextPrivate.hasChanges { try contextPrivate.save() await updateContext(rooms: rooms) @@ -1083,12 +1124,12 @@ extension AdamantTransfersProvider { print(error) } } - + @MainActor func updateContext(rooms: [Chatroom]) async { let viewContextChatrooms = Set(rooms).compactMap { self.stack.container.viewContext.object(with: $0.objectID) as? Chatroom } - + for chatroom in viewContextChatrooms { chatroom.updateLastTransaction() } diff --git a/Adamant/Services/DataProviders/AdamantWalletStoreServiceProvider.swift b/Adamant/Services/DataProviders/AdamantWalletStoreServiceProvider.swift new file mode 100644 index 000000000..e96044b8c --- /dev/null +++ b/Adamant/Services/DataProviders/AdamantWalletStoreServiceProvider.swift @@ -0,0 +1,50 @@ +// +// AdamantWalletStoreServiceProvider.swift +// Adamant +// +// Created by Dmitrij Meidus on 08.02.25. +// Copyright © 2025 Adamant. All rights reserved. +// + +import Combine +import CommonKit +import Foundation + +protocol WalletStoreServiceProviderProtocol: WalletStoreServiceProtocol { + @MainActor + var currentWalletPublisher: AnyObservable { get } +} + +final class AdamantWalletStoreServiceProvider: WalletStoreServiceProviderProtocol { + private let secretWalletsManager: SecretWalletsManagerProtocol + + private var cancellables = Set() + + @ObservableValue private var currentWallet: WalletStoreServiceProtocol + var currentWalletPublisher: AnyObservable { + $currentWallet.map { _ in () }.eraseToAnyPublisher() + } + + init(secretWalletsManager: SecretWalletsManagerProtocol) { + self.secretWalletsManager = secretWalletsManager + self._currentWallet = ObservableValue(secretWalletsManager.getCurrentWallet()) + + setupBindings() + } + + private func setupBindings() { + secretWalletsManager.statePublisher + .map { $0.currentWallet } + .receive(on: DispatchQueue.main) + .assign(to: _currentWallet) + .store(in: &cancellables) + } + + func sorted(includeInvisible: Bool) -> [WalletService] { + currentWallet.sorted(includeInvisible: includeInvisible) + } + + func isInvisible(_ wallet: WalletService) -> Bool { + currentWallet.isInvisible(wallet) + } +} diff --git a/Adamant/Services/DataProviders/CoreDataRealationMapper.swift b/Adamant/Services/DataProviders/CoreDataRealationMapper.swift new file mode 100644 index 000000000..4c480fe53 --- /dev/null +++ b/Adamant/Services/DataProviders/CoreDataRealationMapper.swift @@ -0,0 +1,53 @@ +import CommonKit +// +// CoreDataRealationMapper.swift +// Adamant +// +// Created by Владимир Клевцов on 24.2.25.. +// Copyright © 2025 Adamant. All rights reserved. +// +import CoreData + +protocol CoreDataRealationMapperProtocol { + func mapReactionRelationship(transaction: RichMessageTransaction) async -> [String] +} + +final class CoreDataRealationMapper: CoreDataRealationMapperProtocol { + private let stack: CoreDataStack + + init(stack: CoreDataStack) { + self.stack = stack + } + + func mapReactionRelationship(transaction: RichMessageTransaction) async -> [String] { + let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + privateContext.parent = stack.container.viewContext + + return await privateContext.perform { + guard let id = transaction.getRichValue(for: RichContentKeys.react.reactto_id) else { + return [] + } + + let processedIds: [String] = [id] + + let chatRequest = NSFetchRequest(entityName: "ChatTransaction") + chatRequest.predicate = NSPredicate(format: "transactionId == %@", id) + chatRequest.fetchLimit = 1 + + do { + if let chatTrs = try privateContext.fetch(chatRequest).first { + let transactionInContext = privateContext.object(with: transaction.objectID) as? RichMessageTransaction + transactionInContext?.chatTransaction = chatTrs + chatTrs.addToRichMessageTransactions(transactionInContext!) + if privateContext.hasChanges { + try privateContext.save() + } + } + + return processedIds + } catch { + return processedIds + } + } + } +} diff --git a/Adamant/Services/DataProviders/DefaultNodesProvider.swift b/Adamant/Services/DataProviders/DefaultNodesProvider.swift index a202d2de3..cda28651d 100644 --- a/Adamant/Services/DataProviders/DefaultNodesProvider.swift +++ b/Adamant/Services/DataProviders/DefaultNodesProvider.swift @@ -10,14 +10,16 @@ import CommonKit struct DefaultNodesProvider: Sendable { func get(_ groups: Set) -> [NodeGroup: [Node]] { - .init(uniqueKeysWithValues: groups.map { - ($0, defaultItems(group: $0)) - }) + .init( + uniqueKeysWithValues: groups.map { + ($0, defaultItems(group: $0)) + } + ) } } -private extension DefaultNodesProvider { - func defaultItems(group: NodeGroup) -> [Node] { +extension DefaultNodesProvider { + fileprivate func defaultItems(group: NodeGroup) -> [Node] { switch group { case .btc: return BtcWalletService.nodes diff --git a/Adamant/Services/DataProviders/InMemoryCoreDataStack.swift b/Adamant/Services/DataProviders/InMemoryCoreDataStack.swift index 2b199ec11..7212f53e3 100644 --- a/Adamant/Services/DataProviders/InMemoryCoreDataStack.swift +++ b/Adamant/Services/DataProviders/InMemoryCoreDataStack.swift @@ -6,43 +6,41 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation -import CoreData import CommonKit +import CoreData +import Foundation final class InMemoryCoreDataStack: CoreDataStack { let container: NSPersistentContainer - + init(modelUrl url: URL) throws { guard let model = NSManagedObjectModel(contentsOf: url) else { throw AdamantError(message: "Can't load ManagedObjectModel") } - + let description = NSPersistentStoreDescription() description.type = NSInMemoryStoreType - + container = NSPersistentContainer(name: "Adamant", managedObjectModel: model) container.persistentStoreDescriptions = [description] container.loadPersistentStores { (_, _) in } container.viewContext.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyObjectTrumpMergePolicyType) - - NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userLoggedOut, object: nil, queue: OperationQueue.main) { [weak self] _ in - guard let context = self?.container.viewContext else { - return - } - - let fetch = NSFetchRequest(entityName: "BaseAccount") - - do { - let result = try context.fetch(fetch) - for account in result { - context.delete(account) - } - - try context.save() - } catch { - print("Got error saving context after reset") + } + + func clearCoreData() { + let context = container.viewContext + + let fetch = NSFetchRequest(entityName: "BaseAccount") + + do { + let result = try context.fetch(fetch) + for account in result { + context.delete(account) } + + try context.save() + } catch { + print("Got error saving context after reset") } } } diff --git a/Adamant/Services/FilesNetworkManager/FilesNetworkManager.swift b/Adamant/Services/FilesNetworkManager/FilesNetworkManager.swift index 28e0147e5..6ea974924 100644 --- a/Adamant/Services/FilesNetworkManager/FilesNetworkManager.swift +++ b/Adamant/Services/FilesNetworkManager/FilesNetworkManager.swift @@ -10,11 +10,11 @@ import Foundation final class FilesNetworkManager: FilesNetworkManagerProtocol { private let ipfsService: IPFSApiService - + init(ipfsService: IPFSApiService) { self.ipfsService = ipfsService } - + func uploadFiles( _ data: Data, type: NetworkFileProtocolType, @@ -25,7 +25,7 @@ final class FilesNetworkManager: FilesNetworkManagerProtocol { return await ipfsService.uploadFile(data: data, uploadProgress: uploadProgress) } } - + func downloadFile( _ id: String, type: String, @@ -34,7 +34,7 @@ final class FilesNetworkManager: FilesNetworkManagerProtocol { guard let netwrokProtocol = NetworkFileProtocolType(rawValue: type) else { return .failure(.cantDownloadFile) } - + switch netwrokProtocol { case .ipfs: return await ipfsService.downloadFile( diff --git a/Adamant/Services/FilesNetworkManager/IPFS+Constants.swift b/Adamant/Services/FilesNetworkManager/IPFS+Constants.swift index 9b0ba1cc6..71e209c15 100644 --- a/Adamant/Services/FilesNetworkManager/IPFS+Constants.swift +++ b/Adamant/Services/FilesNetworkManager/IPFS+Constants.swift @@ -6,14 +6,14 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation extension IPFSApiService { static var symbol: String { "IPFS" } - + static var nodes: [Node] { [ Node.makeDefaultNode( @@ -30,7 +30,7 @@ extension IPFSApiService { ) ] } - + static let healthCheckParameters = BlockchainHealthCheckParams( group: .ipfs, name: symbol, diff --git a/Adamant/Services/FilesNetworkManager/IPFSApiCore.swift b/Adamant/Services/FilesNetworkManager/IPFSApiCore.swift index 480aaf213..3d7011362 100644 --- a/Adamant/Services/FilesNetworkManager/IPFSApiCore.swift +++ b/Adamant/Services/FilesNetworkManager/IPFSApiCore.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation extension IPFSApiCommands { static let status = "/api/node/info" @@ -15,11 +15,11 @@ extension IPFSApiCommands { final class IPFSApiCore: Sendable { let apiCore: APICoreProtocol - + init(apiCore: APICoreProtocol) { self.apiCore = apiCore } - + func getNodeStatus(origin: NodeOrigin) async -> ApiServiceResult { await apiCore.sendRequestJsonResponse( origin: origin, @@ -33,15 +33,22 @@ extension IPFSApiCore: BlockchainHealthCheckableService { let startTimestamp = Date.now.timeIntervalSince1970 let statusResponse = await getNodeStatus(origin: origin) let ping = Date.now.timeIntervalSince1970 - startTimestamp - - return statusResponse.map { _ in + + return statusResponse.map { .init( ping: ping, - height: .zero, + height: getHeightFrom(timestamp: $0.timestamp), wsEnabled: false, wsPort: nil, - version: nil + version: .init($0.version) ) } } } + +extension IPFSApiCore { + fileprivate func getHeightFrom(timestamp: UInt64) -> Int { + let timestampInSeconds = timestamp / 1000 + return Int(timestampInSeconds % 100_000_000) + } +} diff --git a/Adamant/Services/FilesNetworkManager/IPFSApiService.swift b/Adamant/Services/FilesNetworkManager/IPFSApiService.swift index cc0175406..672849035 100644 --- a/Adamant/Services/FilesNetworkManager/IPFSApiService.swift +++ b/Adamant/Services/FilesNetworkManager/IPFSApiService.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation enum IPFSApiCommands { static let file = ( @@ -19,21 +19,21 @@ enum IPFSApiCommands { final class IPFSApiService: FileApiServiceProtocol { let service: BlockchainHealthCheckWrapper - + @MainActor var nodesInfoPublisher: AnyObservable { service.nodesInfoPublisher } - + @MainActor var nodesInfo: NodesListInfo { service.nodesInfo } - + func healthCheck() { service.healthCheck() } - + init( healthCheckWrapper: BlockchainHealthCheckWrapper ) { service = healthCheckWrapper } - + func request( _ request: @Sendable (APICoreProtocol, NodeOrigin) async -> ApiServiceResult ) async -> ApiServiceResult { @@ -41,7 +41,7 @@ final class IPFSApiService: FileApiServiceProtocol { await request(admApiCore.apiCore, node) } } - + func uploadFile( data: Data, uploadProgress: @escaping @Sendable (Progress) -> Void @@ -51,7 +51,7 @@ final class IPFSApiService: FileApiServiceProtocol { fileName: defaultFileName, data: data ) - + let result: Result = await request { core, origin in await core.sendRequestMultipartFormDataJsonResponse( origin: origin, @@ -61,18 +61,18 @@ final class IPFSApiService: FileApiServiceProtocol { uploadProgress: uploadProgress ) } - + return result.flatMap { result in guard let cid = result.cids.first else { return .failure( .serverError(error: FileManagerError.cantUploadFile.localizedDescription) ) } - + return .success(cid) }.mapError { .apiError(error: $0) } } - + func downloadFile( id: String, downloadProgress: @escaping @Sendable (Progress) -> Void @@ -84,30 +84,31 @@ final class IPFSApiService: FileApiServiceProtocol { timeout: .extended, downloadProgress: downloadProgress ) - + if let error = handleError(result) { return .failure(error) } - + return result.result } - + return result.flatMap { .success($0) } .mapError { .apiError(error: $0) } } } -private extension IPFSApiService { - func handleError(_ result: APIResponseModel) -> ApiServiceError? { +extension IPFSApiService { + fileprivate func handleError(_ result: APIResponseModel) -> ApiServiceError? { guard let code = result.code, - !(200 ... 299).contains(code) + !(200...299).contains(code) else { return nil } - + let serverError = ApiServiceError.serverError(error: "\(code)") - let error = code == 500 || code == 502 || code == 504 - ? .networkError(error: serverError) - : serverError - + let error = + code == 500 || code == 502 || code == 504 + ? .networkError(error: serverError) + : serverError + return error } } diff --git a/Adamant/Services/FilesNetworkManager/Models/FileManagerError.swift b/Adamant/Services/FilesNetworkManager/Models/FileManagerError.swift index d34d39af1..0b94b1356 100644 --- a/Adamant/Services/FilesNetworkManager/Models/FileManagerError.swift +++ b/Adamant/Services/FilesNetworkManager/Models/FileManagerError.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation enum NetworkFileProtocolType: String { case ipfs diff --git a/Adamant/Services/FilesStorageProprietiesService.swift b/Adamant/Services/FilesStorageProprietiesService.swift index 18a708397..063884885 100644 --- a/Adamant/Services/FilesStorageProprietiesService.swift +++ b/Adamant/Services/FilesStorageProprietiesService.swift @@ -6,18 +6,18 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import Combine import CommonKit +import Foundation @MainActor final class FilesStorageProprietiesService: FilesStorageProprietiesProtocol, Sendable { // MARK: Dependencies - - let securedStore: SecuredStore - + + let SecureStore: SecureStore + // MARK: Proprieties - + private var notificationsSet: Set = [] @ObservableValue private var autoDownloadPreviewState: DownloadPolicy = .everybody @ObservableValue private var autoDownloadFullMediaState: DownloadPolicy = .everybody @@ -25,27 +25,27 @@ final class FilesStorageProprietiesService: FilesStorageProprietiesProtocol, Sen private let autoDownloadFullMediaDefaultState: DownloadPolicy = .contacts private var saveFileEncryptedValue = true private let saveFileEncryptedDefault = true - + var autoDownloadPreviewPolicyPublisher: AnyObservable { $autoDownloadPreviewState.eraseToAnyPublisher() } - + var autoDownloadFullMediaPolicyPublisher: AnyObservable { $autoDownloadFullMediaState.eraseToAnyPublisher() } - + // MARK: Lifecycle - - init(securedStore: SecuredStore) { - self.securedStore = securedStore - + + init(SecureStore: SecureStore) { + self.SecureStore = SecureStore + NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedIn) .sink { @MainActor [weak self] _ in self?.userLoggedIn() } .store(in: ¬ificationsSet) - + NotificationCenter.default .notifications(named: .AdamantAccountService.userLoggedOut) .sink { @MainActor [weak self] _ in @@ -53,77 +53,83 @@ final class FilesStorageProprietiesService: FilesStorageProprietiesProtocol, Sen } .store(in: ¬ificationsSet) } - + // MARK: Notification actions - + private func userLoggedIn() { autoDownloadPreviewState = getAutoDownloadPreview() autoDownloadFullMediaState = getAutoDownloadFullMedia() saveFileEncryptedValue = getSaveFileEncrypted() } - + private func userLoggedOut() { setAutoDownloadPreview(autoDownloadPreviewDefaultState) setAutoDownloadFullMedia(autoDownloadFullMediaDefaultState) saveFileEncryptedValue = saveFileEncryptedDefault } - + // MARK: Update data - + func saveFileEncrypted() -> Bool { saveFileEncryptedValue } - + func getSaveFileEncrypted() -> Bool { - guard let result: Bool = securedStore.get( - StoreKey.storage.saveFileEncrypted - ) else { + guard + let result: Bool = SecureStore.get( + StoreKey.storage.saveFileEncrypted + ) + else { return saveFileEncryptedDefault } - + return result } - + func setSaveFileEncrypted(_ value: Bool) { - securedStore.set(value, for: StoreKey.storage.saveFileEncrypted) + SecureStore.set(value, for: StoreKey.storage.saveFileEncrypted) saveFileEncryptedValue = value } - + func autoDownloadPreviewPolicy() -> DownloadPolicy { autoDownloadPreviewState } - + func getAutoDownloadPreview() -> DownloadPolicy { - guard let result: String = securedStore.get( - StoreKey.storage.autoDownloadPreview - ) else { + guard + let result: String = SecureStore.get( + StoreKey.storage.autoDownloadPreview + ) + else { return autoDownloadPreviewDefaultState } - + return DownloadPolicy(rawValue: result) ?? autoDownloadPreviewDefaultState } - + func setAutoDownloadPreview(_ value: DownloadPolicy) { - securedStore.set(value.rawValue, for: StoreKey.storage.autoDownloadPreview) + SecureStore.set(value.rawValue, for: StoreKey.storage.autoDownloadPreview) autoDownloadPreviewState = value } - + func autoDownloadFullMediaPolicy() -> DownloadPolicy { autoDownloadFullMediaState } - + func getAutoDownloadFullMedia() -> DownloadPolicy { - guard let result: String = securedStore.get( - StoreKey.storage.autoDownloadFullMedia - ) else { + guard + let result: String = SecureStore.get( + StoreKey.storage.autoDownloadFullMedia + ) + else { return autoDownloadFullMediaDefaultState } - + return DownloadPolicy(rawValue: result) ?? autoDownloadFullMediaDefaultState } - + func setAutoDownloadFullMedia(_ value: DownloadPolicy) { - securedStore.set(value.rawValue, for: StoreKey.storage.autoDownloadFullMedia) + SecureStore.set(value.rawValue, for: StoreKey.storage.autoDownloadFullMedia) autoDownloadFullMediaState = value } } diff --git a/Adamant/Services/LanguageService.swift b/Adamant/Services/LanguageService.swift index 832c0e7f1..f48ffb22c 100644 --- a/Adamant/Services/LanguageService.swift +++ b/Adamant/Services/LanguageService.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation final class LanguageStorageService: LanguageStorageProtocol { func getLanguage() -> Language { @@ -15,7 +15,7 @@ final class LanguageStorageService: LanguageStorageProtocol { let language: Language = .init(rawValue: raw) ?? .auto return language } - + func setLanguage(_ language: Language) { UserDefaults.standard.set(language.rawValue, forKey: StoreKey.language.language) UserDefaults.standard.set(language.locale, forKey: StoreKey.language.languageLocale) diff --git a/Adamant/Services/RepeaterService.swift b/Adamant/Services/RepeaterService.swift index 9eb026c9a..a76d584a4 100644 --- a/Adamant/Services/RepeaterService.swift +++ b/Adamant/Services/RepeaterService.swift @@ -6,8 +6,8 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation final class RepeaterService { private class Client: @unchecked Sendable { @@ -15,28 +15,28 @@ final class RepeaterService { let queue: DispatchQueue? var timer: Timer? let callback: @Sendable () -> Void - + init(interval: TimeInterval, queue: DispatchQueue?, callback: @escaping @Sendable () -> Void) { self.interval = interval self.queue = queue self.callback = callback } } - + // MARK: Properties @Atomic private var foregroundTimers = [String: Client]() @Atomic private(set) var isPaused = false - + private let pauseLock = NSLock() - + deinit { -// DispatchQueue.main.async { -// for (_, timer) in foregroundTimers { -// timer.invalidate() -// } -// } + // DispatchQueue.main.async { + // for (_, timer) in foregroundTimers { + // timer.invalidate() + // } + // } } - + /// Register a function to call each seconds on specified queue. /// /// - Parameters: @@ -46,66 +46,66 @@ final class RepeaterService { /// - call: function to call func registerForegroundCall(label: String, interval: TimeInterval, queue: DispatchQueue?, callback: @escaping @Sendable () -> Void) { let client = Client(interval: interval, queue: queue, callback: callback) - + if let t = foregroundTimers[label]?.timer { t.invalidate() } - + foregroundTimers[label] = client - + // Start timer pauseLock.lock() defer { pauseLock.unlock() } - + if !isPaused { let timer = Timer(timeInterval: interval, target: self, selector: #selector(timerFired), userInfo: client, repeats: true) client.timer = timer - + RunLoop.main.add(timer, forMode: .common) } } - + func unregisterForegroundCall(label: String) { if let client = foregroundTimers[label] { client.timer?.invalidate() foregroundTimers.removeValue(forKey: label) } } - + func pauseAll() { pauseLock.lock() defer { pauseLock.unlock() } - + if isPaused { return } - + for (_, client) in self.foregroundTimers { client.timer?.invalidate() client.timer = nil } - + isPaused = true } - + func resumeAll() { pauseLock.lock() defer { pauseLock.unlock() } - + if !isPaused { return } - + for (_, client) in self.foregroundTimers { let timer = Timer(timeInterval: client.interval, target: self, selector: #selector(timerFired), userInfo: client, repeats: true) client.timer = timer - + RunLoop.main.add(timer, forMode: .common) } - + isPaused = false } - + @objc private func timerFired(timer: Timer) { if let client = timer.userInfo as? Client { let queue = client.queue ?? DispatchQueue.main diff --git a/Adamant/Services/RichTransactionReactService/AdamantRichTransactionReactService.swift b/Adamant/Services/RichTransactionReactService/AdamantRichTransactionReactService.swift index fcbaf64e8..321a7ac28 100644 --- a/Adamant/Services/RichTransactionReactService/AdamantRichTransactionReactService.swift +++ b/Adamant/Services/RichTransactionReactService/AdamantRichTransactionReactService.swift @@ -6,21 +6,21 @@ // Copyright © 2023 Adamant. All rights reserved. // -import CoreData import Combine import CommonKit +import CoreData actor AdamantRichTransactionReactService: NSObject, RichTransactionReactService { private let coreDataStack: CoreDataStack private let apiService: AdamantApiServiceProtocol private let adamantCore: AdamantCore private let accountService: AccountService - + private lazy var richController = getRichTransactionsController() private lazy var transferController = getTransferController() private lazy var messageController = getMessageController() private let unknownErrorMessage = String.adamant.reply.shortUnknownMessageError - + private var reactions: [String: Set] = [:] init( @@ -35,14 +35,14 @@ actor AdamantRichTransactionReactService: NSObject, RichTransactionReactService self.accountService = accountService super.init() } - + func startObserving() { richController.delegate = self try? richController.performFetch() - + transferController.delegate = self try? transferController.performFetch() - + messageController.delegate = self try? messageController.performFetch() } @@ -57,27 +57,29 @@ extension AdamantRichTransactionReactService: NSFetchedResultsControllerDelegate newIndexPath _: IndexPath? ) { if let transaction = object as? RichMessageTransaction, - transaction.additionalType == .reaction { + transaction.additionalType == .reaction + { Task { await processReactionCoreDataChange(type: type_, transaction: transaction) } } - + if let transaction = object as? RichMessageTransaction, - transaction.additionalType != .reaction { + transaction.additionalType != .reaction + { Task { await processCoreDataChange(type: type_, transaction: transaction) } } - + if let transaction = object as? TransferTransaction { Task { await processCoreDataChange(type: type_, transaction: transaction) } } - + if let transaction = object as? MessageTransaction { Task { await processCoreDataChange(type: type_, transaction: transaction) } } } } -private extension AdamantRichTransactionReactService { - func processReaction(transaction: RichMessageTransaction) { +extension AdamantRichTransactionReactService { + fileprivate func processReaction(transaction: RichMessageTransaction) { guard let id = transaction.getRichValue( for: RichContentKeys.react.reactto_id @@ -88,25 +90,26 @@ private extension AdamantRichTransactionReactService { else { return } - + var reactions = reactions[id] ?? [] let lastReactionForSender = reactions.first( where: { $0.sender == transaction.senderAddress } ) - + let lastReactionSentDate: Date = lastReactionForSender?.sentDate ?? .init(timeIntervalSince1970: .zero) - - let currentReactionSentDate = transaction.sentDate == nil - ? .now - : transaction.sentDate ?? .now - + + let currentReactionSentDate = + transaction.sentDate == nil + ? .now + : transaction.sentDate ?? .now + guard lastReactionSentDate < currentReactionSentDate else { return } - + if let index = reactions.firstIndex(where: { $0.sender == transaction.senderAddress }) { reactions.remove(at: index) } - + reactions.update( with: .init( sender: transaction.senderAddress, @@ -115,11 +118,11 @@ private extension AdamantRichTransactionReactService { sentDate: transaction.sentDate ?? .now ) ) - + self.reactions[id] = reactions - + let baseTransaction = getTransactionFromDB(id: id) - + switch baseTransaction { case let trs as MessageTransaction: update(transaction: trs) @@ -131,44 +134,44 @@ private extension AdamantRichTransactionReactService { break } } - - func update(transaction: RichMessageTransaction) { + + fileprivate func update(transaction: RichMessageTransaction) { let reactions = reactions[transaction.transactionId] ?? [] let savedReactions = transaction.richContent?[RichContentKeys.react.reactions] as? Set - + guard savedReactions != reactions else { return } - + setReact( to: transaction, reactions: reactions ) } - - func update(transaction: TransferTransaction) { + + fileprivate func update(transaction: TransferTransaction) { let reactions = reactions[transaction.transactionId] ?? [] let savedReactions = transaction.reactions - + guard savedReactions != reactions else { return } - + setReact( to: transaction, reactions: reactions ) } - - func update(transaction: MessageTransaction) { + + fileprivate func update(transaction: MessageTransaction) { let reactions = reactions[transaction.transactionId] ?? [] let savedReactions = transaction.reactions - + guard savedReactions != reactions else { return } - + setReact( to: transaction, reactions: reactions ) } - - func setReact( + + fileprivate func setReact( to transaction: RichMessageTransaction, reactions: Set ) { @@ -177,15 +180,16 @@ private extension AdamantRichTransactionReactService { ) privateContext.parent = coreDataStack.container.viewContext - - let transaction = privateContext.object(with: transaction.objectID) + + let transaction = + privateContext.object(with: transaction.objectID) as? RichMessageTransaction - + transaction?.richContent?[RichContentKeys.react.reactions] = reactions try? privateContext.save() } - - func setReact( + + fileprivate func setReact( to transaction: TransferTransaction, reactions: Set ) { @@ -194,14 +198,15 @@ private extension AdamantRichTransactionReactService { ) privateContext.parent = coreDataStack.container.viewContext - - let transaction = privateContext.object(with: transaction.objectID) + + let transaction = + privateContext.object(with: transaction.objectID) as? TransferTransaction transaction?.reactions = reactions try? privateContext.save() } - - func setReact( + + fileprivate func setReact( to transaction: MessageTransaction, reactions: Set ) { @@ -210,8 +215,9 @@ private extension AdamantRichTransactionReactService { ) privateContext.parent = coreDataStack.container.viewContext - - let transaction = privateContext.object(with: transaction.objectID) + + let transaction = + privateContext.object(with: transaction.objectID) as? MessageTransaction transaction?.reactions = reactions try? privateContext.save() @@ -220,22 +226,22 @@ private extension AdamantRichTransactionReactService { // MARK: Core Data -private extension AdamantRichTransactionReactService { +extension AdamantRichTransactionReactService { /// Search transaction in local storage /// /// - Parameter id: Transacton ID /// - Returns: Transaction, if found - func getTransactionFromDB(id: String) -> BaseTransaction? { + fileprivate func getTransactionFromDB(id: String) -> BaseTransaction? { let privateContext = NSManagedObjectContext( concurrencyType: .privateQueueConcurrencyType ) privateContext.parent = coreDataStack.container.viewContext - + let request = NSFetchRequest(entityName: "BaseTransaction") request.predicate = NSPredicate(format: "transactionId == %@", String(id)) request.fetchLimit = 1 - + do { let result = try privateContext.fetch(request) return result.first @@ -243,8 +249,8 @@ private extension AdamantRichTransactionReactService { return nil } } - - func processReactionCoreDataChange(type: NSFetchedResultsChangeType, transaction: RichMessageTransaction) { + + fileprivate func processReactionCoreDataChange(type: NSFetchedResultsChangeType, transaction: RichMessageTransaction) { switch type { case .insert, .update: processReaction(transaction: transaction) @@ -256,8 +262,8 @@ private extension AdamantRichTransactionReactService { break } } - - func processCoreDataChange(type: NSFetchedResultsChangeType, transaction: AnyObject) { + + fileprivate func processCoreDataChange(type: NSFetchedResultsChangeType, transaction: AnyObject) { switch type { case .insert, .update: switch transaction { @@ -278,8 +284,8 @@ private extension AdamantRichTransactionReactService { break } } - - func getRichTransactionsController() -> NSFetchedResultsController { + + fileprivate func getRichTransactionsController() -> NSFetchedResultsController { let request: NSFetchRequest = NSFetchRequest( entityName: RichMessageTransaction.entityName ) @@ -292,8 +298,8 @@ private extension AdamantRichTransactionReactService { cacheName: nil ) } - - func getTransferController() -> NSFetchedResultsController { + + fileprivate func getTransferController() -> NSFetchedResultsController { let request: NSFetchRequest = NSFetchRequest( entityName: TransferTransaction.entityName ) @@ -306,8 +312,8 @@ private extension AdamantRichTransactionReactService { cacheName: nil ) } - - func getMessageController() -> NSFetchedResultsController { + + fileprivate func getMessageController() -> NSFetchedResultsController { let request: NSFetchRequest = NSFetchRequest( entityName: MessageTransaction.entityName ) diff --git a/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift b/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift index 0769b02be..78f0bf25a 100644 --- a/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift +++ b/Adamant/Services/RichTransactionReplyService/AdamantRichTransactionReplyService.swift @@ -6,9 +6,9 @@ // Copyright © 2023 Adamant. All rights reserved. // -import CoreData import Combine import CommonKit +import CoreData actor AdamantRichTransactionReplyService: NSObject, RichTransactionReplyService { private let coreDataStack: CoreDataStack @@ -16,7 +16,7 @@ actor AdamantRichTransactionReplyService: NSObject, RichTransactionReplyService private let adamantCore: AdamantCore private let accountService: AccountService private let walletServiceCompose: WalletServiceCompose - + private lazy var richController = getRichTransactionsController() private lazy var transferController = getTransferController() private let unknownErrorMessage = String.adamant.reply.shortUnknownMessageError @@ -33,18 +33,18 @@ actor AdamantRichTransactionReplyService: NSObject, RichTransactionReplyService self.adamantCore = adamantCore self.accountService = accountService self.walletServiceCompose = walletServiceCompose - + super.init() } - + func startObserving() { richController.delegate = self try? richController.performFetch() - richController.fetchedObjects?.forEach( update(transaction:) ) - + richController.fetchedObjects?.forEach(update(transaction:)) + transferController.delegate = self try? transferController.performFetch() - transferController.fetchedObjects?.forEach( update(transaction:) ) + transferController.fetchedObjects?.forEach(update(transaction:)) } } @@ -57,27 +57,29 @@ extension AdamantRichTransactionReplyService: NSFetchedResultsControllerDelegate newIndexPath _: IndexPath? ) { if let transaction = object as? RichMessageTransaction, - transaction.additionalType == .reply { + transaction.additionalType == .reply + { Task { await processCoreDataChange(type: type_, transaction: transaction) } } - + if let transaction = object as? TransferTransaction, - transaction.replyToId != nil { + transaction.replyToId != nil + { Task { await processCoreDataChange(type: type_, transaction: transaction) } } } } -private extension AdamantRichTransactionReplyService { - func update(transaction: TransferTransaction) { +extension AdamantRichTransactionReplyService { + fileprivate func update(transaction: TransferTransaction) { Task { do { guard let id = transaction.replyToId, - transaction.decodedReplyMessage == nil + transaction.decodedReplyMessage == nil else { return } - + let message = try await getReplyMessage(from: id) - + setReplyMessage( for: transaction, message: message @@ -90,16 +92,16 @@ private extension AdamantRichTransactionReplyService { } } } - - func update(transaction: RichMessageTransaction) { + + fileprivate func update(transaction: RichMessageTransaction) { Task { do { guard let id = transaction.getRichValue(for: RichContentKeys.reply.replyToId), - transaction.getRichValue(for: RichContentKeys.reply.decodedReplyMessage) == nil + transaction.getRichValue(for: RichContentKeys.reply.decodedReplyMessage) == nil else { return } - + let message = try await getReplyMessage(from: id) - + setReplyMessage( for: transaction, message: message @@ -112,182 +114,200 @@ private extension AdamantRichTransactionReplyService { } } } - - func getReplyMessage(from id: String) async throws -> String { + + fileprivate func getReplyMessage(from id: String) async throws -> String { if let baseTransaction = getTransactionFromDB(id: id) { return try getReplyMessage(from: baseTransaction) } - + let transactionReply = try await getTransactionFromAPI(by: UInt64(id) ?? 0) return try getReplyMessage(from: transactionReply) } - - func getTransactionFromAPI(by id: UInt64) async throws -> Transaction { + + fileprivate func getTransactionFromAPI(by id: UInt64) async throws -> Transaction { try await apiService.getTransaction(id: id, withAsset: true).get() } - - func getReplyMessage(from transaction: Transaction) throws -> String { + + fileprivate func getReplyMessage(from transaction: Transaction) throws -> String { guard let address = accountService.account?.address, - let privateKey = accountService.keypair?.privateKey + let privateKey = accountService.keypair?.privateKey else { throw ApiServiceError.accountNotFound } - + let isOut = transaction.senderId == address - - let publicKey: String? = isOut - ? transaction.recipientPublicKey - : transaction.senderPublicKey - - let transactionStatus = isOut - ? String.adamant.chat.transactionSent - : String.adamant.chat.transactionReceived - + + let publicKey: String? = + isOut + ? transaction.recipientPublicKey + : transaction.senderPublicKey + + let transactionStatus = + isOut + ? String.adamant.chat.transactionSent + : String.adamant.chat.transactionReceived + guard let chat = transaction.asset.chat else { let message = "\(transactionStatus) \(AdmWalletService.currencySymbol) \(transaction.amount)" return message } - + guard let publicKey = publicKey else { return unknownErrorMessage } - + let decodedMessage = adamantCore.decodeMessage( rawMessage: chat.message, rawNonce: chat.ownMessage, senderPublicKey: publicKey, privateKey: privateKey )?.trimmingCharacters(in: .whitespacesAndNewlines) - + guard let decodedMessage = decodedMessage else { return unknownErrorMessage } - + var message: String - + switch chat.type { case .message, .messageOld, .signal, .unknown: - let comment = !decodedMessage.isEmpty - ? ": \(decodedMessage)" - : "" - - message = transaction.amount > 0 - ? "\(transactionStatus) \(transaction.amount) \(AdmWalletService.currencySymbol)\(comment)" - : decodedMessage - + let comment = + !decodedMessage.isEmpty + ? ": \(decodedMessage)" + : "" + + message = + transaction.amount > 0 + ? "\(transactionStatus) \(transaction.amount) \(AdmWalletService.currencySymbol)\(comment)" + : decodedMessage + case .richMessage: if let data = decodedMessage.data(using: String.Encoding.utf8), - let richContent = RichMessageTools.richContent(from: data), - transaction.amount > 0 { + let richContent = RichMessageTools.richContent(from: data), + transaction.amount > 0 + { let commentContent = (richContent[RichContentKeys.reply.replyMessage] as? String) ?? "" - let comment = !commentContent.isEmpty - ? ": \(commentContent)" - : "" - + let comment = + !commentContent.isEmpty + ? ": \(commentContent)" + : "" + let humanType = AdmWalletService.currencySymbol - + message = "\(transactionStatus) \(transaction.amount) \(humanType)\(comment)" break } - + if let data = decodedMessage.data(using: String.Encoding.utf8), - let richContent = RichMessageTools.richContent(from: data), - let transfer = RichMessageTransfer(content: richContent) { - let comment = !transfer.comments.isEmpty - ? ": \(transfer.comments)" - : "" - - let humanType = walletServiceCompose.getWallet( - by: transfer.type - )?.core.tokenSymbol ?? transfer.type - + let richContent = RichMessageTools.richContent(from: data), + let transfer = RichMessageTransfer(content: richContent) + { + let comment = + !transfer.comments.isEmpty + ? ": \(transfer.comments)" + : "" + + let humanType = + walletServiceCompose.getWallet( + by: transfer.type + )?.core.tokenSymbol ?? transfer.type + message = "\(transactionStatus) \(transfer.amount) \(humanType)\(comment)" break } - + if let data = decodedMessage.data(using: String.Encoding.utf8), - let richContent = RichMessageTools.richContent(from: data), - let replyMessage = richContent[RichContentKeys.reply.replyMessage] as? String { - + let richContent = RichMessageTools.richContent(from: data), + let replyMessage = richContent[RichContentKeys.reply.replyMessage] as? String + { + message = replyMessage break } - + if let data = decodedMessage.data(using: String.Encoding.utf8), - let richContent = RichMessageTools.richContent(from: data), - let replyMessage = richContent[RichContentKeys.reply.replyMessage] as? [String: Any], - replyMessage[RichContentKeys.file.files] is [[String: Any]] { + let richContent = RichMessageTools.richContent(from: data), + let replyMessage = richContent[RichContentKeys.reply.replyMessage] as? [String: Any], + replyMessage[RichContentKeys.file.files] is [[String: Any]] + { message = FilePresentationHelper.getFilePresentationText(richContent) break } - + if let data = decodedMessage.data(using: String.Encoding.utf8), - let richContent = RichMessageTools.richContent(from: data), - richContent[RichContentKeys.file.files] is [[String: Any]] { + let richContent = RichMessageTools.richContent(from: data), + richContent[RichContentKeys.file.files] is [[String: Any]] + { message = FilePresentationHelper.getFilePresentationText(richContent) break } - + message = decodedMessage } - + return MessageProcessHelper.process(message) } - - func getReplyMessage(from transaction: BaseTransaction) throws -> String { + + fileprivate func getReplyMessage(from transaction: BaseTransaction) throws -> String { guard let address = accountService.account?.address else { throw ApiServiceError.accountNotFound } - + let isOut = transaction.senderId == address - - let transactionStatus = isOut - ? String.adamant.chat.transactionSent - : String.adamant.chat.transactionReceived - + + let transactionStatus = + isOut + ? String.adamant.chat.transactionSent + : String.adamant.chat.transactionReceived + var message: String - + switch transaction { case let trs as MessageTransaction: message = trs.message ?? "" case let trs as TransferTransaction: let trsComment = trs.comment ?? "" - let comment = !trsComment.isEmpty - ? ": \(trsComment)" - : "" - + let comment = + !trsComment.isEmpty + ? ": \(trsComment)" + : "" + message = "\(transactionStatus) \(trs.amount ?? 0.0) \(AdmWalletService.currencySymbol)\(comment)" case let trs as RichMessageTransaction: if let replyMessage = trs.getRichValue(for: RichContentKeys.reply.replyMessage) { message = replyMessage break } - + if let richContent = trs.richContent, - let transfer = RichMessageTransfer(content: richContent) { - let comment = !transfer.comments.isEmpty - ? ": \(transfer.comments)" - : "" - - let humanType = walletServiceCompose.getWallet( - by: transfer.type - )?.core.tokenSymbol ?? transfer.type - + let transfer = RichMessageTransfer(content: richContent) + { + let comment = + !transfer.comments.isEmpty + ? ": \(transfer.comments)" + : "" + + let humanType = + walletServiceCompose.getWallet( + by: transfer.type + )?.core.tokenSymbol ?? transfer.type + message = "\(transactionStatus) \(transfer.amount) \(humanType)\(comment)" break } - + if let richContent = trs.richContent, - let _: [[String: Any]] = trs.getRichValue(for: RichContentKeys.file.files) { + trs.getRichValue(for: RichContentKeys.file.files) != nil + { message = FilePresentationHelper.getFilePresentationText(richContent) break } - + message = unknownErrorMessage default: message = unknownErrorMessage } - + return MessageProcessHelper.process(message) } - - func setReplyMessage( + + fileprivate func setReplyMessage( for transaction: RichMessageTransaction, message: String ) { @@ -296,14 +316,15 @@ private extension AdamantRichTransactionReplyService { ) privateContext.parent = coreDataStack.container.viewContext - - let transaction = privateContext.object(with: transaction.objectID) + + let transaction = + privateContext.object(with: transaction.objectID) as? RichMessageTransaction transaction?.richContent?[RichContentKeys.reply.decodedReplyMessage] = message try? privateContext.save() } - - func setReplyMessage( + + fileprivate func setReplyMessage( for transaction: TransferTransaction, message: String ) { @@ -312,8 +333,9 @@ private extension AdamantRichTransactionReplyService { ) privateContext.parent = coreDataStack.container.viewContext - - let transaction = privateContext.object(with: transaction.objectID) + + let transaction = + privateContext.object(with: transaction.objectID) as? TransferTransaction transaction?.decodedReplyMessage = message try? privateContext.save() @@ -322,22 +344,22 @@ private extension AdamantRichTransactionReplyService { // MARK: Core Data -private extension AdamantRichTransactionReplyService { +extension AdamantRichTransactionReplyService { /// Search transaction in local storage /// /// - Parameter id: Transacton ID /// - Returns: Transaction, if found - func getTransactionFromDB(id: String) -> BaseTransaction? { + fileprivate func getTransactionFromDB(id: String) -> BaseTransaction? { let privateContext = NSManagedObjectContext( concurrencyType: .privateQueueConcurrencyType ) privateContext.parent = coreDataStack.container.viewContext - + let request = NSFetchRequest(entityName: "BaseTransaction") request.predicate = NSPredicate(format: "transactionId == %@", String(id)) request.fetchLimit = 1 - + do { let result = try privateContext.fetch(request) return result.first @@ -345,8 +367,8 @@ private extension AdamantRichTransactionReplyService { return nil } } - - func processCoreDataChange(type: NSFetchedResultsChangeType, transaction: TransferTransaction) { + + fileprivate func processCoreDataChange(type: NSFetchedResultsChangeType, transaction: TransferTransaction) { switch type { case .insert, .update: update(transaction: transaction) @@ -358,8 +380,8 @@ private extension AdamantRichTransactionReplyService { break } } - - func processCoreDataChange(type: NSFetchedResultsChangeType, transaction: RichMessageTransaction) { + + fileprivate func processCoreDataChange(type: NSFetchedResultsChangeType, transaction: RichMessageTransaction) { switch type { case .insert, .update: update(transaction: transaction) @@ -371,8 +393,8 @@ private extension AdamantRichTransactionReplyService { break } } - - func getRichTransactionsController() -> NSFetchedResultsController { + + fileprivate func getRichTransactionsController() -> NSFetchedResultsController { let request: NSFetchRequest = NSFetchRequest( entityName: RichMessageTransaction.entityName ) @@ -385,8 +407,8 @@ private extension AdamantRichTransactionReplyService { cacheName: nil ) } - - func getTransferController() -> NSFetchedResultsController { + + fileprivate func getTransferController() -> NSFetchedResultsController { let request: NSFetchRequest = NSFetchRequest( entityName: TransferTransaction.entityName ) diff --git a/Adamant/Services/SecretWalletsFactory.swift b/Adamant/Services/SecretWalletsFactory.swift new file mode 100644 index 000000000..054b3696f --- /dev/null +++ b/Adamant/Services/SecretWalletsFactory.swift @@ -0,0 +1,66 @@ +// +// SecretWalletsFactory.swift +// Adamant +// +// Created by Dmitrij Meidus on 20.02.25. +// Copyright © 2025 Adamant. All rights reserved. +// + +import CommonKit + +struct SecretWalletsFactory { + private let visibleWalletsService: VisibleWalletsService + private let accountService: AccountService + private let SecureStore: SecureStore + + init( + visibleWalletsService: VisibleWalletsService, + accountService: AccountService, + SecureStore: SecureStore + ) { + self.visibleWalletsService = visibleWalletsService + self.accountService = accountService + self.SecureStore = SecureStore + } + + func makeSecretWallet(withPassword password: String) -> WalletStoreServiceProtocol { + var wallets: [WalletCoreProtocol] = [ + AdmWalletService(), + BtcWalletService(), + EthWalletService(), + KlyWalletService(), + DogeWalletService(), + DashWalletService() + ] + + let erc20WalletServices = ERC20Token.supportedTokens.map { + ERC20WalletService(token: $0) + } + wallets.append(contentsOf: erc20WalletServices) + let walletServiceCompose = AdamantWalletServiceCompose(wallets: wallets) + Task.detached(priority: .userInitiated) { + await initWallets(withPass: password, for: walletServiceCompose) + } + let wallet = AdamantWalletStoreService(visibleWalletsService: visibleWalletsService, walletServiceCompose: walletServiceCompose) + + return wallet + } + + private func initWallets(withPass password: String, for walletService: WalletServiceCompose) async { + guard let passphrase: String = SecureStore.get(StoreKey.accountService.passphrase) else { + print("No passphrase found") + return + } + + await withTaskGroup(of: Void.self) { taskGroup in + for wallet in walletService.getWallets() { + taskGroup.addTask { + _ = try? await wallet.core.initWallet( + withPassphrase: passphrase, + withPassword: password + ) + } + } + } + } +} diff --git a/Adamant/Services/SecretWalletsService.swift b/Adamant/Services/SecretWalletsService.swift new file mode 100644 index 000000000..81fd3e7c3 --- /dev/null +++ b/Adamant/Services/SecretWalletsService.swift @@ -0,0 +1,76 @@ +// +// AdamantSecretWalletsManager.swift +// Adamant +// +// Created by Dmitrij Meidus on 19.02.25. +// Copyright © 2025 Adamant. All rights reserved. +// + +import CommonKit +import Foundation +import Swinject + +extension AdamantSecretWalletsManager { + struct State: SecretWalletsManagerStateProtocol { + var currentWallet: WalletStoreServiceProtocol + let defaultWallet: WalletStoreServiceProtocol + var secretWallets: [WalletStoreServiceProtocol] = [] + } +} + +final class AdamantSecretWalletsManager: SecretWalletsManagerProtocol { + private let secretWalletsFactory: SecretWalletsFactory + private let lock = NSLock() + + init( + walletsStoreService: WalletStoreServiceProtocol, + secretWalletsFactory: SecretWalletsFactory + ) { + self.state = State( + currentWallet: walletsStoreService, + defaultWallet: walletsStoreService + ) + self.secretWalletsFactory = secretWalletsFactory + } + + @ObservableValue private var state: SecretWalletsManagerStateProtocol + var statePublisher: AnyObservable { + $state.eraseToAnyPublisher() + } + + // MARK: - Manage state + func createSecretWallet(withPassword password: String) { + let wallet = secretWalletsFactory.makeSecretWallet(withPassword: password) + lock.lock() + defer { lock.unlock() } + state.secretWallets.append(wallet) + } + + func removeSecretWallet(at index: Int) -> WalletStoreServiceProtocol? { + lock.lock() + defer { lock.unlock() } + guard state.secretWallets.indices.contains(index) else { return nil } + return state.secretWallets.remove(at: index) + } + + func getCurrentWallet() -> WalletStoreServiceProtocol { + state.currentWallet + } + + func getSecretWallets() -> [WalletStoreServiceProtocol] { + state.secretWallets + } + + func activateSecretWallet(at index: Int) { + lock.lock() + defer { lock.unlock() } + guard index < state.secretWallets.count else { return } + state.currentWallet = state.secretWallets[index] + } + + func activateDefaultWallet() { + lock.lock() + defer { lock.unlock() } + state.currentWallet = state.defaultWallet + } +} diff --git a/Adamant/Services/SocketService/AdamantSocketService.swift b/Adamant/Services/SocketService/AdamantSocketService.swift index 66f5b500f..cefbe4306 100644 --- a/Adamant/Services/SocketService/AdamantSocketService.swift +++ b/Adamant/Services/SocketService/AdamantSocketService.swift @@ -6,26 +6,26 @@ // Copyright © 2022 Adamant. All rights reserved. // +import Combine +import CommonKit import Foundation import SocketIO -import CommonKit -import Combine final class AdamantSocketService: SocketService, @unchecked Sendable { private let nodesStorage: NodesStorageProtocol private let nodesAdditionalParamsStorage: NodesAdditionalParamsStorageProtocol - + // MARK: - Properties - + @Atomic private(set) var currentNode: Node? { didSet { currentUrl = currentNode?.asSocketURL() - + guard oldValue?.id != currentNode?.id else { return } sendCurrentNodeUpdateNotification() } } - + @Atomic private var currentUrl: URL? { didSet { guard @@ -35,68 +35,68 @@ final class AdamantSocketService: SocketService, @unchecked Sendable { else { return } - + connect(address: address, handler: handler) } } - + @Atomic private var manager: SocketManager? @Atomic private var socket: SocketIOClient? @Atomic private var currentAddress: String? @Atomic private var currentHandler: (@Sendable (ApiServiceResult) -> Void)? @Atomic private var subscriptions = Set() - + let defaultResponseDispatchQueue = DispatchQueue( label: "com.adamant.response-queue", qos: .utility ) - + init( nodesStorage: NodesStorageProtocol, nodesAdditionalParamsStorage: NodesAdditionalParamsStorageProtocol ) { self.nodesAdditionalParamsStorage = nodesAdditionalParamsStorage self.nodesStorage = nodesStorage - + nodesStorage .getNodesPublisher(group: .adm) .combineLatest(nodesAdditionalParamsStorage.fastestNodeMode(group: .adm)) .sink { [weak self] in self?.updateCurrentNode(nodes: $0.0, fastestNode: $0.1) } .store(in: &subscriptions) } - + // MARK: - Tools - + func connect(address: String, handler: @escaping @Sendable (ApiServiceResult) -> Void) { disconnect() currentAddress = address currentHandler = handler - + guard let currentUrl = currentUrl else { return } manager = SocketManager( socketURL: currentUrl, config: [.log(false), .compress] ) - + socket = manager?.defaultSocket socket?.on(clientEvent: .connect) { [weak self] _, _ in self?.socket?.emit("address", with: [address], completion: nil) } - + socket?.on("newTrans") { [weak self] data, _ in self?.handleTransaction(data: data) } - + socket?.connect() } - + func disconnect() { socket?.disconnect() socket = nil manager = nil } - + private func handleTransaction(data: [Any]) { guard let data = data.first, @@ -109,12 +109,12 @@ final class AdamantSocketService: SocketService, @unchecked Sendable { else { return } - + defaultResponseDispatchQueue.async { [currentHandler] in currentHandler?(.success(trans)) } } - + private func sendCurrentNodeUpdateNotification() { NotificationCenter.default.post( name: Notification.Name.SocketService.currentNodeUpdate, @@ -122,23 +122,23 @@ final class AdamantSocketService: SocketService, @unchecked Sendable { userInfo: nil ) } - + private func updateCurrentNode(nodes: [Node], fastestNode: Bool) { let allowedNodes = nodes.getAllowedNodes( sortedBySpeedDescending: fastestNode, needWS: true ) - + guard !fastestNode else { currentNode = allowedNodes.first return } - + guard let previousNode = currentNode else { currentNode = allowedNodes.randomElement() return } - + currentNode = allowedNodes.first { $0.isSame(previousNode) } ?? allowedNodes.randomElement() } } diff --git a/Adamant/SharedViews/AdamantSecureField.swift b/Adamant/SharedViews/AdamantSecureField.swift index a73991a0e..637210e52 100644 --- a/Adamant/SharedViews/AdamantSecureField.swift +++ b/Adamant/SharedViews/AdamantSecureField.swift @@ -11,7 +11,7 @@ import SwiftUI struct AdamantSecureField: View { let placeholder: String? let text: Binding - + var body: some View { GeometryReader { geometry in _AdamantSecureField(placeholder: placeholder, text: text) @@ -23,9 +23,9 @@ struct AdamantSecureField: View { private struct _AdamantSecureField: UIViewRepresentable { let placeholder: String? let text: Binding - + func makeUIView(context _: Context) -> _View { .init() } - + func updateUIView(_ view: _View, context _: Context) { view.text = text.wrappedValue view.placeholder = placeholder @@ -36,19 +36,19 @@ private struct _AdamantSecureField: UIViewRepresentable { extension _AdamantSecureField { final class _View: UITextField { var onChanged: (String?) -> Void = { _ in } - + override var intrinsicContentSize: CGSize { .init( width: UIView.noIntrinsicMetric, height: super.intrinsicContentSize.height ) } - + override init(frame: CGRect) { super.init(frame: frame) configure() } - + required init?(coder: NSCoder) { super.init(coder: coder) configure() @@ -56,14 +56,14 @@ extension _AdamantSecureField { } } -private extension _AdamantSecureField._View { - func configure() { +extension _AdamantSecureField._View { + fileprivate func configure() { isSecureTextEntry = true enablePasswordToggle() addTarget(self, action: #selector(_onChanged), for: .editingChanged) } - - @objc func _onChanged() { + + @objc fileprivate func _onChanged() { onChanged(text) } } diff --git a/Adamant/SharedViews/ButtonsStripeView.swift b/Adamant/SharedViews/ButtonsStripeView.swift index f3d7767b1..5af56d513 100644 --- a/Adamant/SharedViews/ButtonsStripeView.swift +++ b/Adamant/SharedViews/ButtonsStripeView.swift @@ -6,9 +6,9 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import MyLittlePinpad import CommonKit +import MyLittlePinpad +import UIKit // MARK: - Button types enum StripeButtonType: Int, Equatable { @@ -17,21 +17,21 @@ enum StripeButtonType: Int, Equatable { case faceID = 999 case qrCameraReader = 777 case qrPhotoReader = 1111 - + var image: UIImage { switch self { case .pinpad: return .asset(named: "Stripe_Pinpad") ?? .init() - + case .touchID: return .asset(named: "Stripe_TouchID") ?? .init() - + case .faceID: return .asset(named: "Stripe_FaceID") ?? .init() - + case .qrCameraReader: return .asset(named: "Stripe_QRCamera") ?? .init() - + case .qrPhotoReader: return .asset(named: "Stripe_QRLibrary") ?? .init() } @@ -43,10 +43,10 @@ extension BiometryType { switch self { case .touchID: return .touchID - + case .faceID: return .faceID - + case .none: return nil } @@ -65,53 +65,53 @@ protocol ButtonsStripeViewDelegate: AnyObject { final class ButtonsStripeView: UIView { // MARK: IBOutlet @IBOutlet weak var stripeStackView: UIStackView! - + // MARK: Properties weak var delegate: ButtonsStripeViewDelegate? private var buttons: [RoundedButton]? - + var stripe: Stripe? { didSet { for aView in stripeStackView.subviews { aView.removeFromSuperview() } - + guard let stripe = stripe else { stripeStackView.addArrangedSubview(UIView()) buttons = nil return } - + buttons = [RoundedButton]() - + for aButton in stripe { let button = RoundedButton() button.tag = aButton.rawValue button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside) - + button.setImage(aButton.image, for: .normal) button.imageView?.tintColor = UIColor.adamant.tableRowIcons button.imageView!.contentMode = .scaleAspectFit button.clipsToBounds = true - + button.normalBackgroundColor = buttonsNormalColor button.highlightedBackgroundColor = buttonsHighlightedColor button.backgroundColor = buttonsNormalColor - + button.roundingMode = buttonsRoundingMode button.layer.borderWidth = buttonsBorderWidth button.layer.borderColor = buttonsBorderColor?.cgColor - + button.heightAnchor.constraint(equalToConstant: buttonsSize).isActive = true button.widthAnchor.constraint(equalToConstant: buttonsSize).isActive = true - button.constraints.forEach({$0.identifier = "wh"}) - + button.constraints.forEach({ $0.identifier = "wh" }) + stripeStackView.addArrangedSubview(button) buttons?.append(button) } } } - + // MARK: Buttons properties var buttonsBorderColor: UIColor? { didSet { @@ -120,7 +120,7 @@ final class ButtonsStripeView: UIView { } } } - + var buttonsBorderWidth: CGFloat = 0.0 { didSet { if let buttons = buttons { @@ -128,7 +128,7 @@ final class ButtonsStripeView: UIView { } } } - + var buttonsRoundingMode: RoundingMode = .none { didSet { if let buttons = buttons { @@ -136,7 +136,7 @@ final class ButtonsStripeView: UIView { } } } - + var buttonsNormalColor: UIColor? { didSet { if let buttons = buttons { @@ -144,7 +144,7 @@ final class ButtonsStripeView: UIView { } } } - + var buttonsHighlightedColor: UIColor? { didSet { if let buttons = buttons { @@ -152,13 +152,13 @@ final class ButtonsStripeView: UIView { } } } - + var buttonsSize: CGFloat = 50.0 { didSet { buttons?.flatMap({ $0.constraints }).filter({ $0.identifier == "wh" }).forEach({ $0.constant = buttonsSize }) } } - + // MARK: Delegate @objc private func buttonTapped(_ sender: UIButton) { if let button = StripeButtonType(rawValue: sender.tag) { @@ -170,12 +170,12 @@ final class ButtonsStripeView: UIView { // MARK: Adamant config extension ButtonsStripeView { static let adamantDefaultHeight: CGFloat = 85 - + static func adamantConfigured() -> ButtonsStripeView { guard let view = UINib(nibName: "ButtonsStripe", bundle: nil).instantiate(withOwner: nil, options: nil).first as? ButtonsStripeView else { fatalError("Can't get UINib") } - + view.buttonsBorderColor = UIColor.adamant.primary view.buttonsBorderWidth = 1 view.buttonsSize = 50 diff --git a/Adamant/SharedViews/CheckmarkRowView.swift b/Adamant/SharedViews/CheckmarkRowView.swift index 6cf6bb64f..099d4c386 100644 --- a/Adamant/SharedViews/CheckmarkRowView.swift +++ b/Adamant/SharedViews/CheckmarkRowView.swift @@ -14,7 +14,7 @@ final class CheckmarkRowView: UIView { private let titleLabel = makeTitleLabel() private let subtitleLabel = makeSubtitleLabel() private let captionLabel = makeCaptionLabel() - + private lazy var horizontalStack: UIStackView = { let stack = UIStackView(arrangedSubviews: [captionLabel, subtitleLabel]) stack.axis = .horizontal @@ -22,96 +22,96 @@ final class CheckmarkRowView: UIView { stack.spacing = 6 return stack }() - + var title: String? { get { titleLabel.text } set { titleLabel.text = newValue } } - + var titleColor: UIColor { get { titleLabel.textColor } set { titleLabel.textColor = newValue } } - + var subtitle: String? { get { subtitleLabel.text } set { subtitleLabel.text = newValue } } - + var subtitleColor: UIColor { get { subtitleLabel.textColor } set { subtitleLabel.textColor = newValue } } - + var caption: String? { get { captionLabel.text } set { captionLabel.text = newValue } } - + var captionColor: UIColor { get { captionLabel.textColor } set { captionLabel.textColor = newValue } } - + var onCheckmarkTap: (() -> Void)? { get { checkmarkView.onCheckmarkTap } set { checkmarkView.onCheckmarkTap = newValue } } - + var checkmarkImage: UIImage? { get { checkmarkView.image } set { checkmarkView.image = newValue } } - + var isChecked: Bool { checkmarkView.isChecked } - + var isUpdating: Bool { checkmarkView.isUpdating } - + var checkmarkImageBorderColor: CGColor? { get { checkmarkView.imageBorderColor } set { checkmarkView.imageBorderColor = newValue } } - + var checkmarkImageTintColor: UIColor? { get { checkmarkView.imageTintColor } set { checkmarkView.imageTintColor = newValue } } - + init() { super.init(frame: .zero) setupView() } - + required init?(coder: NSCoder) { super.init(coder: coder) setupView() } - + func setIsChecked(_ isChecked: Bool, animated: Bool) { checkmarkView.setIsChecked(isChecked, animated: animated) } - + func setIsUpdating(_ isUpdating: Bool, animated: Bool) { checkmarkView.setIsUpdating(isUpdating, animated: animated) } - + private func setupView() { addSubview(checkmarkView) checkmarkView.snp.makeConstraints { $0.size.equalTo(44) $0.top.leading.bottom.equalToSuperview().inset(2) } - + addSubview(titleLabel) titleLabel.snp.makeConstraints { $0.top.equalTo(checkmarkView) $0.leading.equalTo(checkmarkView.snp.trailing).offset(4) } - + addSubview(horizontalStack) horizontalStack.snp.makeConstraints { $0.top.equalTo(titleLabel.snp.bottom).offset(2) diff --git a/Adamant/SharedViews/CheckmarkView.swift b/Adamant/SharedViews/CheckmarkView.swift index ab19bb2b2..16bee3492 100644 --- a/Adamant/SharedViews/CheckmarkView.swift +++ b/Adamant/SharedViews/CheckmarkView.swift @@ -6,37 +6,37 @@ // Copyright © 2022 Adamant. All rights reserved. // -import UIKit -import SnapKit import CommonKit +import SnapKit +import UIKit final class CheckmarkView: UIView { var image: UIImage? { get { imageView.image } set { imageView.image = newValue } } - + var onCheckmarkTap: (() -> Void)? private(set) var isChecked = false private(set) var isUpdating = false - + var imageBorderColor: CGColor? { get { imageBackgroundView.layer.borderColor } set { imageBackgroundView.layer.borderColor = newValue } } - + var imageTintColor: UIColor? { get { imageView.tintColor } set { imageView.tintColor = newValue } } - + private lazy var spinner: UIActivityIndicatorView = { let view = UIActivityIndicatorView(style: .gray) view.isHidden = true view.color = .adamant.textColor return view }() - + private lazy var imageBackgroundView: UIView = { let view = UIView() view.layer.borderWidth = 1 @@ -44,37 +44,37 @@ final class CheckmarkView: UIView { view.layer.borderColor = UIColor.adamant.secondary.cgColor return view }() - + private lazy var imageView: UIImageView = { let view = UIImageView() view.tintColor = .adamant.primary return view }() - + private lazy var checkmarkContainer = UIView() - + override init(frame: CGRect) { super.init(frame: frame) setupView() } - + required init?(coder: NSCoder) { super.init(coder: coder) setupView() } - + override func draw(_ rect: CGRect) { super.draw(rect) updateImage(animated: false) } - + func setIsChecked(_ isChecked: Bool, animated: Bool) { guard self.isChecked != isChecked else { return } - + self.isChecked = isChecked updateImage(animated: animated) } - + func setIsUpdating(_ isUpdating: Bool, animated: Bool) { self.isUpdating = isUpdating if isUpdating { @@ -87,36 +87,36 @@ final class CheckmarkView: UIView { stopAnimating() } } - + private func setupView() { addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap))) - + addSubview(checkmarkContainer) checkmarkContainer.snp.makeConstraints { $0.size.equalTo(checkmarkSize) $0.center.equalToSuperview() } - + checkmarkContainer.addSubview(imageBackgroundView) imageBackgroundView.snp.makeConstraints { $0.directionalEdges.equalToSuperview() } - + checkmarkContainer.addSubview(imageView) imageView.snp.makeConstraints { $0.directionalEdges.equalToSuperview() } - + checkmarkContainer.addSubview(spinner) spinner.snp.makeConstraints { $0.directionalEdges.equalToSuperview() } } - + @objc private func onTap() { onCheckmarkTap?() } - + private func updateImage(animated: Bool) { if isChecked { showImage(animated: animated) @@ -124,16 +124,16 @@ final class CheckmarkView: UIView { hideImage(animated: animated) } } - + private func showImage(animated: Bool) { imageView.isHidden = false - + guard animated else { imageView.transform = CGAffineTransform.identity imageBackgroundView.alpha = .zero return } - + UIView.animate( withDuration: 0.15, delay: .zero, @@ -144,7 +144,7 @@ final class CheckmarkView: UIView { } ) } - + private func hideImage(animated: Bool) { guard animated else { imageView.isHidden = true @@ -152,7 +152,7 @@ final class CheckmarkView: UIView { imageBackgroundView.alpha = 1 return } - + UIView.animate( withDuration: 0.15, delay: .zero, @@ -168,11 +168,11 @@ final class CheckmarkView: UIView { } ) } - + private func startAnimating() { spinner.startAnimating() } - + private func stopAnimating() { spinner.stopAnimating() } diff --git a/Adamant/SharedViews/DoubleDetailsTableViewCell.swift b/Adamant/SharedViews/DoubleDetailsTableViewCell.swift index 9bc6e655c..55ac7341f 100644 --- a/Adamant/SharedViews/DoubleDetailsTableViewCell.swift +++ b/Adamant/SharedViews/DoubleDetailsTableViewCell.swift @@ -6,8 +6,8 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit import Eureka +import UIKit public struct DoubleDetail: Equatable { let first: String @@ -15,17 +15,17 @@ public struct DoubleDetail: Equatable { } public final class DoubleDetailsTableViewCell: Cell, CellType { - + // MARK: Constants static let compactHeight: CGFloat = 50.0 static let fullHeight: CGFloat = 65.0 - + // MARK: IBOutlets @IBOutlet weak var stackView: UIStackView! @IBOutlet var titleLabel: UILabel! @IBOutlet var detailsLabel: UILabel! @IBOutlet var secondDetailsLabel: UILabel! - + // MARK: Properties var secondValue: String? { get { @@ -38,14 +38,14 @@ public final class DoubleDetailsTableViewCell: Cell, CellType { } } } - + public override func update() { super.update() - + if let value = row.value { detailsLabel.text = value.first secondDetailsLabel.text = value.second - + stackView.spacing = value.second == nil ? 0 : 1 } else { detailsLabel.text = nil diff --git a/Adamant/SharedViews/EurekaAlertLabelRow.swift b/Adamant/SharedViews/EurekaAlertLabelRow.swift index ad7516067..fcc7a2ffa 100644 --- a/Adamant/SharedViews/EurekaAlertLabelRow.swift +++ b/Adamant/SharedViews/EurekaAlertLabelRow.swift @@ -6,45 +6,47 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit import Eureka import FreakingSimpleRoundImageView +import UIKit public final class AlertLabelCell: Cell, CellType { var inCellAccessoryView: UIView? private(set) var alertLabel: RoundedLabel? - + required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - + if style == .value1, let detailTextLabel = detailTextLabel { let label = RoundedLabel() label.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(label) contentView.addConstraints(AlertLabelCell.alertConstraints(for: label, relativeTo: detailTextLabel)) - + alertLabel = label } - + alertLabel = nil } - + required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) - + if let detailTextLabel = detailTextLabel { let label = RoundedLabel() label.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(label) contentView.addConstraints(AlertLabelCell.alertConstraints(for: label, relativeTo: detailTextLabel)) - + alertLabel = label } } - + private static func alertConstraints(for item: UIView, relativeTo toItem: UIView) -> [NSLayoutConstraint] { - return [NSLayoutConstraint(item: item, attribute: .trailing, relatedBy: .equal, toItem: toItem, attribute: .leading, multiplier: 1, constant: -8), - NSLayoutConstraint(item: item, attribute: .centerY, relatedBy: .equal, toItem: toItem, attribute: .centerY, multiplier: 1, constant: 0)] + return [ + NSLayoutConstraint(item: item, attribute: .trailing, relatedBy: .equal, toItem: toItem, attribute: .leading, multiplier: 1, constant: -8), + NSLayoutConstraint(item: item, attribute: .centerY, relatedBy: .equal, toItem: toItem, attribute: .centerY, multiplier: 1, constant: 0) + ] } } diff --git a/Adamant/SharedViews/EurekaQRRow.swift b/Adamant/SharedViews/EurekaQRRow.swift index 3cb2b14bd..c2ffe3f9e 100644 --- a/Adamant/SharedViews/EurekaQRRow.swift +++ b/Adamant/SharedViews/EurekaQRRow.swift @@ -6,14 +6,14 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit import Eureka +import UIKit public class QrCell: Cell, CellType { @IBOutlet weak var qrImageView: UIImageView! @IBOutlet weak var tipLabel: UILabel! @IBOutlet weak var bottomConstrain: NSLayoutConstraint! - + var tipLabelIsHidden: Bool = false { didSet { if tipLabelIsHidden { @@ -25,7 +25,7 @@ public class QrCell: Cell, CellType { } } } - + public override func update() { super.update() qrImageView.image = row.value diff --git a/Adamant/SharedViews/FullscreenAlertView.swift b/Adamant/SharedViews/FullscreenAlertView.swift index f49f57a6b..99d09de11 100644 --- a/Adamant/SharedViews/FullscreenAlertView.swift +++ b/Adamant/SharedViews/FullscreenAlertView.swift @@ -6,58 +6,58 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit import CommonKit import SnapKit +import UIKit final class FullscreenAlertView: UIView { var message: String = .empty { didSet { messageLabel.text = message } } - + private let containerView: UIView = .init() private let imageView = UIImageView(image: warningImage) - + private let messageLabel = UILabel( font: .systemFont(ofSize: 15), textColor: .adamant.textColor, numberOfLines: .zero, alignment: .center ) - + private lazy var verticalStack: UIStackView = { let stack = UIStackView(arrangedSubviews: [imageView, messageLabel]) stack.axis = .vertical stack.spacing = 15 return stack }() - + override init(frame: CGRect) { super.init(frame: frame) configure() } - + required init?(coder: NSCoder) { super.init(coder: coder) configure() } } -private extension FullscreenAlertView { - func configure() { +extension FullscreenAlertView { + fileprivate func configure() { backgroundColor = .black.withAlphaComponent(0.4) containerView.backgroundColor = .adamant.cellColor containerView.layer.cornerRadius = 15 imageView.tintColor = .adamant.primary imageView.contentMode = .scaleAspectFit - + addSubview(containerView) containerView.snp.makeConstraints { $0.center.equalToSuperview() $0.top.leading.greaterThanOrEqualToSuperview().inset(15) $0.bottom.trailing.lessThanOrEqualToSuperview().inset(15) } - + containerView.addSubview(verticalStack) verticalStack.snp.makeConstraints { $0.directionalEdges.equalToSuperview().inset(15) diff --git a/Adamant/SharedViews/LoadingView.swift b/Adamant/SharedViews/LoadingView.swift index fa6bc6e85..b724e468e 100644 --- a/Adamant/SharedViews/LoadingView.swift +++ b/Adamant/SharedViews/LoadingView.swift @@ -6,41 +6,41 @@ // Copyright © 2022 Adamant. All rights reserved. // -import UIKit -import SnapKit import CommonKit +import SnapKit +import UIKit final class LoadingView: UIView { private let logo = UIImageView(image: .asset(named: "Adamant-logo")) private let spinner = UIActivityIndicatorView(style: .whiteLarge) - + init() { super.init(frame: .zero) setupView() } - + required init?(coder: NSCoder) { super.init(coder: coder) setupView() } - + func startAnimating() { spinner.startAnimating() } - + func stopAnimating() { spinner.stopAnimating() } - + private func setupView() { backgroundColor = .adamant.background - + addSubview(logo) logo.snp.makeConstraints { $0.size.equalTo(100) $0.center.equalToSuperview() } - + addSubview(spinner) spinner.snp.makeConstraints { $0.center.equalToSuperview() diff --git a/Adamant/SharedViews/ReplyView.swift b/Adamant/SharedViews/ReplyView.swift index 89d0adf74..a47eba29d 100644 --- a/Adamant/SharedViews/ReplyView.swift +++ b/Adamant/SharedViews/ReplyView.swift @@ -6,19 +6,19 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit -import SnapKit import CommonKit +import SnapKit +import UIKit final class ReplyView: UIView { - + private let messageLabel = UILabel(font: messageFont, textColor: .adamant.textColor, numberOfLines: 1) - + private lazy var replyView: UIView = { let view = UIView() let colorView = UIView() colorView.backgroundColor = .adamant.active - + view.addSubview(colorView) view.addSubview(messageLabel) @@ -32,23 +32,23 @@ final class ReplyView: UIView { } return view }() - + private var replyIV: UIImageView = { let iv = UIImageView( image: UIImage( systemName: "arrowshape.turn.up.left" )?.withTintColor(.adamant.active) ) - + iv.tintColor = .adamant.active iv.snp.makeConstraints { make in make.height.equalTo(30) make.width.equalTo(24) } - + return iv }() - + private lazy var closeBtn: UIButton = { let btn = UIButton() btn.setImage( @@ -56,36 +56,36 @@ final class ReplyView: UIView { for: .normal ) btn.addTarget(self, action: #selector(didTapCloseBtn), for: .touchUpInside) - + btn.snp.makeConstraints { make in make.height.width.equalTo(30) } return btn }() - + private lazy var horizontalStack: UIStackView = { let stack = UIStackView(arrangedSubviews: [replyIV, replyView, closeBtn]) stack.axis = .horizontal stack.spacing = horizontalStackSpacing return stack }() - + // MARK: Proprieties - + var closeAction: (() -> Void)? - + // MARK: Init - + override init(frame: CGRect) { super.init(frame: frame) configure() } - + required init?(coder: NSCoder) { super.init(coder: coder) configure() } - + func configure() { addSubview(horizontalStack) horizontalStack.snp.makeConstraints { @@ -93,23 +93,23 @@ final class ReplyView: UIView { $0.horizontalEdges.equalToSuperview().inset(horizontalInsets) } } - + // MARK: Actions - + @objc private func didTapCloseBtn() { closeAction?() } } - + extension ReplyView { func update(with model: MessageModel) { backgroundColor = .clear var text = model.makeReplyContent().resolveLinkColor() text = MessageProcessHelper.process(attributedText: text) - + let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineBreakMode = .byTruncatingTail - + text.addAttribute( .paragraphStyle, value: paragraphStyle, diff --git a/Adamant/SharedViews/SpinnerView.swift b/Adamant/SharedViews/SpinnerView.swift index 4a2157602..4a79c2e4b 100644 --- a/Adamant/SharedViews/SpinnerView.swift +++ b/Adamant/SharedViews/SpinnerView.swift @@ -6,9 +6,9 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit -import SnapKit import CommonKit +import SnapKit +import UIKit final class SpinnerView: UIView { static let size = CGSize(squareSize: 50) @@ -42,8 +42,8 @@ extension SpinnerView: ReusableView { } } -private extension SpinnerView { - func configure() { +extension SpinnerView { + fileprivate func configure() { addSubview(spinner) spinner.snp.makeConstraints { $0.center.equalToSuperview() diff --git a/Adamant/SharedViews/UISuffixTextField.swift b/Adamant/SharedViews/UISuffixTextField.swift index e6d7d5494..d7e12a80d 100644 --- a/Adamant/SharedViews/UISuffixTextField.swift +++ b/Adamant/SharedViews/UISuffixTextField.swift @@ -6,33 +6,33 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit import Eureka +import UIKit public final class SuffixTextRow: FieldRow, RowType { required public init(tag: String?) { super.init(tag: tag) } - + public override func updateCell() { super.updateCell() } } public class SuffixTextCell: _FieldCell, CellType { - + public var suffix: String? - + required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) self.initSetup() } - + required public init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) self.initSetup() } - + func initSetup() { self.textField.removeFromSuperview() let textField = UISuffixTextField() @@ -40,20 +40,20 @@ public class SuffixTextCell: _FieldCell, CellType { textField.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(textField) } - + open override func setup() { super.setup() textField.autocorrectionType = .default textField.autocapitalizationType = .sentences textField.keyboardType = .default } - + public override func update() { super.update() (self.textField as! UISuffixTextField).suffix = suffix self.textField.setNeedsDisplay() } - + public override func textFieldDidChange(_ textField: UITextField) { super.textFieldDidChange(textField) self.textField.setNeedsDisplay() @@ -61,36 +61,36 @@ public class SuffixTextCell: _FieldCell, CellType { } class UISuffixTextField: UITextField { - + open var suffix: String? - + override init(frame: CGRect) { super.init(frame: frame) } - + required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } - + override func draw(_ rect: CGRect) { if let suffix = suffix { let color = (textColor ?? UIColor.black) color.setFill() var x: CGFloat = 0 let font = self.font ?? UIFont.systemFont(ofSize: 14) - + if let text = text { - let textSize = text.size(withAttributes: [.font : font]) - + let textSize = text.size(withAttributes: [.font: font]) + if textAlignment == NSTextAlignment.center { x = (frame.size.width / 2) + textSize.width } else { x = textSize.width } } - - let suffixSize = suffix.size(withAttributes: [.font : font]) - suffix.draw(in: CGRect(x: x, y: 0, width: suffixSize.width, height: suffixSize.height), withAttributes: [.font : font, .foregroundColor: color]) + + let suffixSize = suffix.size(withAttributes: [.font: font]) + suffix.draw(in: CGRect(x: x, y: 0, width: suffixSize.width, height: suffixSize.height), withAttributes: [.font: font, .foregroundColor: color]) } } } diff --git a/Adamant/SharedViews/UpdatingIndicatorView.swift b/Adamant/SharedViews/UpdatingIndicatorView.swift index 5bea04ef0..9888b921a 100644 --- a/Adamant/SharedViews/UpdatingIndicatorView.swift +++ b/Adamant/SharedViews/UpdatingIndicatorView.swift @@ -6,14 +6,14 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit -import SnapKit import CommonKit +import SnapKit +import UIKit final class UpdatingIndicatorView: UIView { - + private lazy var imageView = UIImageView(image: nil) - + private lazy var titleLabel: UILabel = { let label = UILabel() label.text = title @@ -22,14 +22,14 @@ final class UpdatingIndicatorView: UIView { label.font = titleType.font return label }() - + private lazy var spinner: UIActivityIndicatorView = { let view = UIActivityIndicatorView(style: .medium) view.isHidden = true view.color = .adamant.textColor return view }() - + private lazy var userDataStackView: UIStackView = { let stackView = UIStackView() stackView.axis = .horizontal @@ -47,11 +47,11 @@ final class UpdatingIndicatorView: UIView { }() // MARK: Proprieties - + enum TitleType { case small case medium - + var font: UIFont { switch self { case .small: return .preferredFont(forTextStyle: .headline) @@ -59,7 +59,7 @@ final class UpdatingIndicatorView: UIView { } } } - + private var title: String private var titleType: TitleType private var image: UIImage? { @@ -67,13 +67,13 @@ final class UpdatingIndicatorView: UIView { updateImageViewSize() } } - + private var imageSize: CGFloat { image != nil ? 25 : .zero } - + // MARK: Init - + init(title: String, titleType: TitleType = .medium) { self.title = title self.titleType = titleType @@ -81,18 +81,18 @@ final class UpdatingIndicatorView: UIView { setupView() } - + required init?(coder: NSCoder) { self.title = "" self.titleType = .small super.init(coder: coder) setupView() } - + private func setupView() { addSubview(userDataStackView) addSubview(spinner) - + userDataStackView.snp.makeConstraints { make in make.centerY.leading.trailing.equalToSuperview() } @@ -101,31 +101,31 @@ final class UpdatingIndicatorView: UIView { make.centerY.equalToSuperview() } } - + @MainActor private func updateImageViewSize() { imageView.snp.updateConstraints { make in make.size.equalTo(imageSize) } } - + // MARK: Actions - + func startAnimate() { imageView.alpha = 0 spinner.startAnimating() } - + func stopAnimate() { spinner.stopAnimating() imageView.alpha = 1 } - + func updateTitle(title: String?) { self.title = title ?? "" titleLabel.text = title } - + func updateImage(image: UIImage?) { self.image = image imageView.image = image diff --git a/Adamant/SharedViews/VersionFooterView.swift b/Adamant/SharedViews/VersionFooterView.swift index 1cfbfeac7..6a32a477e 100644 --- a/Adamant/SharedViews/VersionFooterView.swift +++ b/Adamant/SharedViews/VersionFooterView.swift @@ -6,31 +6,31 @@ // Copyright © 2024 Adamant. All rights reserved. // -import UIKit import SnapKit +import UIKit final class VersionFooterView: UIView { struct Model { let version: String let commit: String? - + static let `default` = Self(version: .empty, commit: nil) } - + var model: Model = .default { didSet { update() } } - + private let versionLabel = UILabel( font: .adamantPrimary(ofSize: fontSize), textColor: .adamant.primary ) - + private let commitLabel = UILabel( font: .adamantPrimary(ofSize: fontSize), textColor: .adamant.primary ) - + private lazy var labelsStack: UIStackView = { let stack = UIStackView(arrangedSubviews: [versionLabel]) stack.alignment = .center @@ -38,7 +38,7 @@ final class VersionFooterView: UIView { stack.spacing = labelsGap return stack }() - + private lazy var containerView: UIView = { let view = UIView() view.addSubview(labelsStack) @@ -46,20 +46,20 @@ final class VersionFooterView: UIView { $0.directionalHorizontalEdges.top.equalToSuperview() $0.bottom.equalToSuperview().inset(bottomInset) } - + return view }() - + override init(frame: CGRect) { super.init(frame: frame) configure() } - + required init?(coder: NSCoder) { super.init(coder: coder) configure() } - + override func sizeThatFits(_ size: CGSize) -> CGSize { .init( width: size.width, @@ -68,18 +68,18 @@ final class VersionFooterView: UIView { } } -private extension VersionFooterView { - func configure() { +extension VersionFooterView { + fileprivate func configure() { addSubview(containerView) containerView.snp.makeConstraints { $0.directionalEdges.equalToSuperview() } } - - func update() { + + fileprivate func update() { versionLabel.text = model.version commitLabel.text = model.commit - + switch (model.commit, commitLabel.superview) { case (.some, .none): labelsStack.addArrangedSubview(commitLabel) diff --git a/Adamant/Utilities/AdamantCoinTools.swift b/Adamant/Utilities/AdamantCoinTools.swift index 85bdc276a..f83f75fbd 100644 --- a/Adamant/Utilities/AdamantCoinTools.swift +++ b/Adamant/Utilities/AdamantCoinTools.swift @@ -17,15 +17,15 @@ enum QQAddressParam { case amount(String) case recipient(String) case klyrMessage(String) - + init?(raw: String) { let keyValue = raw.split(separator: "=") - + guard keyValue.count == 2 else { return nil } - + let key = keyValue[0] let value = String(keyValue[1]) - + switch key { case "amount": self = .amount(value) @@ -42,45 +42,45 @@ enum QQAddressParam { final class AdamantCoinTools { static func decode(uri: String, qqPrefix: String) -> QQAddressInformation? { let url = URLComponents(string: uri) - + guard !uri.isEmpty, - let url = url + let url = url else { return nil } - + let array = uri.split(separator: ":") - + guard array.count > 1, - let prefix = array.first + let prefix = array.first else { return parseAdress(url: url) } - + guard prefix.caseInsensitiveCompare(qqPrefix) == .orderedSame else { return nil } - + return parseAdress(url: url) } - + private class func parseAdress(url: URLComponents) -> QQAddressInformation { - + let params = url.queryItems?.compactMap { QQAddressParam(raw: String($0.description)) } - + var recipient: String? - + params?.forEach({ param in guard case .recipient(let address) = param else { return } recipient = address }) - + let addressRaw = recipient ?? url.path - + return QQAddressInformation(address: addressRaw, params: params) } } diff --git a/Adamant/Utilities/AdamantQRTools.swift b/Adamant/Utilities/AdamantQRTools.swift index 57725b279..7e18a3a7f 100644 --- a/Adamant/Utilities/AdamantQRTools.swift +++ b/Adamant/Utilities/AdamantQRTools.swift @@ -6,9 +6,9 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit -import EFQRCode import CommonKit +import EFQRCode +import UIKit enum QRToolGenerateResult { case success(UIImage) @@ -22,7 +22,7 @@ enum QRToolDecodeResult { } final class AdamantQRTools { - static func generateQrFrom(string: String, withLogo: Bool = false ) -> QRToolGenerateResult { + static func generateQrFrom(string: String, withLogo: Bool = false) -> QRToolGenerateResult { let generator = EFQRCodeGenerator( content: string, size: EFIntSize(width: 600, height: 600) @@ -33,27 +33,27 @@ final class AdamantQRTools { let logoSize = hasAdm ? EFIntSize(width: 156, height: 156) : EFIntSize(width: 138, height: 138) generator.withIcon(UIImage.asset(named: "logo")?.cgImage, size: logoSize) } - + if let qr = generator.generate() { let image = UIImage(cgImage: qr) return .success(image) } - + return .failure(error: AdamantError(message: "Failed to generate QR from: \(string)")) } - + static func readQR(_ qr: UIImage) -> QRToolDecodeResult { guard let image = qr.cgImage else { print("Failed to get image?") return .none } - + if let result = EFQRCode.recognize(image).first { return .success(result) } - + return .none } - + private init() {} } diff --git a/Adamant/Utilities/AdamantUriTools.swift b/Adamant/Utilities/AdamantUriTools.swift index 3208d785a..98479fe3e 100644 --- a/Adamant/Utilities/AdamantUriTools.swift +++ b/Adamant/Utilities/AdamantUriTools.swift @@ -6,8 +6,8 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation import CommonKit +import Foundation enum AdamantUri { case passphrase(passphrase: String) @@ -20,13 +20,13 @@ enum AdamantAddressParam { case label(String) case message(String) case amount(Double) - + init?(raw: String) { let keyValue = raw.split(separator: "=") if keyValue.count != 2 { return nil } - + switch keyValue[0] { case "address": self = .address(String(keyValue[1])) @@ -41,7 +41,7 @@ enum AdamantAddressParam { return nil } } - + var encoded: String { switch self { case .address(let value): @@ -59,10 +59,10 @@ enum AdamantAddressParam { final class AdamantUriTools { static func encode(request: AdamantUri) -> String { switch request { - case .passphrase(passphrase: let passphrase): + case .passphrase(let passphrase): return passphrase - - case .address(address: let address, params: let params): + + case .address(let address, let params): var components = URLComponents() components.scheme = "https" components.host = "msg.adamant.im" @@ -82,16 +82,16 @@ final class AdamantUriTools { components.queryItems?.append(.init(name: "amount", value: String(value))) } } - + guard let uri = components.url?.absoluteString else { return "" } return uri - case .addressLegacy(address: let address, params: let params): + case .addressLegacy(let address, let params): var components = URLComponents() components.scheme = AdmWalletService.qqPrefix components.host = address components.queryItems = (params?.count ?? .zero) > .zero ? [] : nil - + params?.forEach { switch $0 { case .address: @@ -104,50 +104,50 @@ final class AdamantUriTools { components.queryItems?.append(.init(name: "amount", value: String(value))) } } - + guard let uri = components.url?.absoluteString.replacingOccurrences(of: "://", with: ":") else { return "" } return uri } } - + static func decode(uri: String) -> AdamantUri? { if uri.count == 0 { return nil } - + if AdamantUtilities.validateAdamantPassphrase(passphrase: uri) { return AdamantUri.passphrase(passphrase: uri) } - + let request = uri.split(separator: ":") guard request.count == 2, - request[0].caseInsensitiveCompare(AdmWalletService.qqPrefix) == .orderedSame + request[0].caseInsensitiveCompare(AdmWalletService.qqPrefix) == .orderedSame else { return nil } - + let addressAndParams = request[1].split(separator: "?") guard let addressRaw = addressAndParams.first else { return nil } - + let address = String(addressRaw) switch AdamantUtilities.validateAdamantAddress(address: address) { case .valid: break - + case .system, .invalid: return nil } - + let params: [AdamantAddressParam]? if addressAndParams.count > 1 { var p = [AdamantAddressParam]() - + for param in addressAndParams[1].split(separator: "&").compactMap({ AdamantAddressParam(raw: String($0)) }) { p.append(param) } - + if p.count > 0 { params = p } else { @@ -156,9 +156,9 @@ final class AdamantUriTools { } else { params = nil } - + return AdamantUri.address(address: address, params: params) } - + private init() {} } diff --git a/Adamant/Utilities/AdamantUtilities+extended.swift b/Adamant/Utilities/AdamantUtilities+extended.swift index fe216a9d4..945759777 100644 --- a/Adamant/Utilities/AdamantUtilities+extended.swift +++ b/Adamant/Utilities/AdamantUtilities+extended.swift @@ -6,29 +6,30 @@ // Copyright © 2018 Adamant. All rights reserved. // +import CommonKit import Foundation import SafariServices -import CommonKit extension AdamantUtilities { // MARK: Application version static let applicationVersion: String = { if let infoDictionary = Bundle.main.infoDictionary, let version = infoDictionary["CFBundleShortVersionString"] as? String, - let build = infoDictionary["CFBundleVersion"] as? String { + let build = infoDictionary["CFBundleVersion"] as? String + { return "\(version) (\(build))" } - + return "" }() - + // MARK: Device model static var deviceModelCode: String { isMacOS ? macModelCode : phoneModelCode } - + // MARK: Device info @MainActor static let deviceInfo: String = { @@ -49,13 +50,13 @@ extension AdamantUtilities { static let passphraseRegexString = "^([a-z]* ){11}([a-z]*)$" static let passphraseRegex = try! NSRegularExpression(pattern: passphraseRegexString, options: []) static let addressRegex = try! NSRegularExpression(pattern: addressRegexString, options: []) - + enum AddressValidationResult { case valid case system case invalid } - + /// Rules are simple: /// /// - Leading uppercase U @@ -71,7 +72,7 @@ extension AdamantUtilities { return .invalid } } - + /// Rules are simple: /// /// - No leading and/or trailing whitespaces @@ -80,28 +81,28 @@ extension AdamantUtilities { /// - No -$%èçïäł- caracters /// - 12 words, splitted by a single whitespace /// - a-z - + static func validateAdamantPassphrase(_ passphrase: String) -> Bool { validateAdamantPassphrase(passphrase: passphrase) } - + static func validateAdamantPassphrase(passphrase: String) -> Bool { guard validate(string: passphrase, with: passphraseRegex) else { return false } - + for word in passphrase.split(separator: " ") { if !WordList.english.contains(word) { return false } } - + return true } - + private static func validate(string: String, with regex: NSRegularExpression) -> Bool { let matches = regex.matches(in: string, options: [], range: NSRange(location: 0, length: string.count)) - + return matches.count == 1 } } @@ -110,12 +111,12 @@ extension AdamantUtilities { extension AdamantUtilities { static func getHexString(from bytes: [UInt8]) -> String { if bytes.count > 0 { - return Data(bytes).reduce("") {$0 + String(format: "%02x", $1)} + return Data(bytes).reduce("") { $0 + String(format: "%02x", $1) } } else { return "" } } - + static func getBytes(from hex: String) -> [UInt8] { let hexa = Array(hex) return stride(from: 0, to: hex.count, by: 2).compactMap { UInt8(String(hexa[$0..<$0.advanced(by: 2)]), radix: 16) } @@ -124,7 +125,7 @@ extension AdamantUtilities { // MARK: - JSON extension AdamantUtilities { - static func json(from object:Any) -> String? { + static func json(from object: Any) -> String? { do { let data = try JSONSerialization.data(withJSONObject: object, options: []) return String(data: data, encoding: String.Encoding.utf8) @@ -133,7 +134,7 @@ extension AdamantUtilities { } return nil } - + static func toArray(text: String) -> [String]? { if let data = text.data(using: .utf8) { do { @@ -151,18 +152,18 @@ extension AdamantUtilities { @MainActor static func openEmailApp(recipient: String, subject: String?, body: String?) { guard var urlComponents = URLComponents(string: "mailto:\(recipient)") else { return } - + urlComponents.queryItems = [ .init(name: "subject", value: subject), .init(name: "body", value: body) ] - + urlComponents.url.map { UIApplication.shared.open($0) } } } -private extension AdamantUtilities { - static var phoneModelCode: String { +extension AdamantUtilities { + fileprivate static var phoneModelCode: String { var systemInfo = utsname() uname(&systemInfo) let modelCode = withUnsafePointer(to: &systemInfo.machine) { @@ -172,8 +173,8 @@ private extension AdamantUtilities { } return modelCode ?? "Unknown" } - - static var macModelCode: String { + + fileprivate static var macModelCode: String { var size = 0 sysctlbyname("hw.model", nil, &size, nil, 0) diff --git a/Adamant/Utilities/Mnemonic+extended.swift b/Adamant/Utilities/Mnemonic+extended.swift index 198027913..63165f96a 100644 --- a/Adamant/Utilities/Mnemonic+extended.swift +++ b/Adamant/Utilities/Mnemonic+extended.swift @@ -6,18 +6,18 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation -import CryptoSwift import CommonKit +import CryptoSwift +import Foundation -enum MnemonicError : Error { +enum MnemonicError: Error { case randomBytesError } extension Mnemonic { - + // MARK: - Generating passphrases - + static func generate() throws -> [String] { let byteCount = 16 var bytes = Data(count: byteCount) @@ -25,24 +25,24 @@ extension Mnemonic { guard status == errSecSuccess else { throw MnemonicError.randomBytesError } return generate(entropy: bytes) } - - static func generate(entropy : Data) -> [String] { - var bin = String(entropy.flatMap { ("00000000" + String($0, radix:2)).suffix(8) }) - + + static func generate(entropy: Data) -> [String] { + var bin = String(entropy.flatMap { ("00000000" + String($0, radix: 2)).suffix(8) }) + let hash = entropy.sha256() let bits = entropy.count * 8 let cs = bits / 32 - - let hashbits = String(hash.flatMap { ("00000000" + String($0, radix:2)).suffix(8) }) + + let hashbits = String(hash.flatMap { ("00000000" + String($0, radix: 2)).suffix(8) }) let checksum = String(hashbits.prefix(cs)) bin += checksum - + var mnemonic = [String]() for i in 0..<(bin.count / 11) { let wi = Int(bin[bin.index(bin.startIndex, offsetBy: i * 11).. Node { let node = Node(scheme: .default, host: "", port: nil) node.connectionStatus = connectionStatus diff --git a/AdamantTests/AdamantUriBuilding.swift b/AdamantTests/DisabledTests/AdamantUriBuilding.swift similarity index 89% rename from AdamantTests/AdamantUriBuilding.swift rename to AdamantTests/DisabledTests/AdamantUriBuilding.swift index b98bfa96e..c78f24711 100644 --- a/AdamantTests/AdamantUriBuilding.swift +++ b/AdamantTests/DisabledTests/AdamantUriBuilding.swift @@ -7,6 +7,7 @@ // import XCTest + @testable import Adamant class AdamantUriBuilding: XCTestCase { @@ -14,89 +15,89 @@ class AdamantUriBuilding: XCTestCase { func testEncodingPassphrase() { let passphrase = "safe cabin draw case loud enlist toy smooth exchange chef clean whale" let encoded = AdamantUriTools.encode(request: AdamantUri.passphrase(passphrase: passphrase)) - + XCTAssertEqual(passphrase, encoded) } - + func testDecodingPassphrase() { let encoded = "safe cabin draw case loud enlist toy smooth exchange chef clean whale" - + guard let decoded = AdamantUriTools.decode(uri: encoded) else { XCTFail("Decoding failed") return } - + switch decoded { - case .passphrase(passphrase: let passphrase): + case .passphrase(let passphrase): XCTAssertEqual(passphrase, encoded) - + default: XCTFail("Something wrong here") } } - + // MARK: - Addresses func testEncodeAddress() { let address = "U123456789012345" let encoded = "\(AdmWalletService.qqPrefix):\(address)" let freshlyEncoded = AdamantUriTools.encode(request: AdamantUri.address(address: address, params: nil)) - + XCTAssertEqual(encoded, freshlyEncoded) } - + func testEncodeAddressWithParams() { let address = "U123456789012345" let label = "Kingsize pineapple pizza" let encoded = "\(AdmWalletService.qqPrefix):\(address)?label=\(label.replacingOccurrences(of: " ", with: "+"))" let freshlyEncoded = AdamantUriTools.encode(request: AdamantUri.address(address: address, params: [AdamantAddressParam.label(label)])) - + XCTAssertEqual(encoded, freshlyEncoded) } - + func testDecodeAddress() { let address = "U123456789012345" let encoded = "\(AdmWalletService.qqPrefix):\(address)" - + guard let decoded = AdamantUriTools.decode(uri: encoded) else { XCTFail("Failed to decode.") return } - + switch decoded { - case .address(address: let addressDecoded, params: let params): + case .address(address: let addressDecoded, let params): XCTAssertEqual(address, addressDecoded) XCTAssertNil(params) - + default: XCTFail("Something bad here") } } - + func testDecodeAddressWithParams() { let address = "U123456789012345" let value = "Kingsize pineapple pizza" let encoded = "\(AdmWalletService.qqPrefix):\(address)?label=\(value.replacingOccurrences(of: " ", with: "+"))" - + guard let decoded = AdamantUriTools.decode(uri: encoded) else { XCTFail("failed to decode.") return } - + switch decoded { - case .address(address: let addressDecoded, params: let params): + case .address(address: let addressDecoded, let params): XCTAssertEqual(address, addressDecoded) guard let params = params, params.count == 1, let label = params.first else { XCTFail("Failed to decode params") return } - + switch label { case .label(let valueDecoded): XCTAssertEqual(value, valueDecoded) case .address, .message: XCTFail("Incorrect case") } - + default: XCTFail("Something bad there") } diff --git a/AdamantTests/AddressGeneratorTests.swift b/AdamantTests/DisabledTests/AddressGeneratorTests.swift similarity index 98% rename from AdamantTests/AddressGeneratorTests.swift rename to AdamantTests/DisabledTests/AddressGeneratorTests.swift index 2036f5e9b..dbdc7e817 100644 --- a/AdamantTests/AddressGeneratorTests.swift +++ b/AdamantTests/DisabledTests/AddressGeneratorTests.swift @@ -7,6 +7,7 @@ // import XCTest + @testable import Adamant private struct PublicKeyAndAddress { @@ -18,39 +19,39 @@ class AddressGeneratorTests: XCTestCase { func test_0() { test(index: 0) } - + func test_1() { test(index: 1) } - + func test_2() { test(index: 2) } - + func test_3() { test(index: 3) } - + func test_4() { test(index: 4) } - + func test_5() { test(index: 5) } - + func test_6() { test(index: 6) } - + func test_7() { test(index: 7) } - + func test_8() { test(index: 8) } - + func test_9() { test(index: 9) } diff --git a/AdamantTests/AddressValidationTests.swift b/AdamantTests/DisabledTests/AddressValidationTests.swift similarity index 98% rename from AdamantTests/AddressValidationTests.swift rename to AdamantTests/DisabledTests/AddressValidationTests.swift index 8f0bc40c7..1312a6dee 100644 --- a/AdamantTests/AddressValidationTests.swift +++ b/AdamantTests/DisabledTests/AddressValidationTests.swift @@ -7,55 +7,56 @@ // import XCTest + @testable import Adamant class AddressValidationTests: XCTestCase { - + func testValidAddress() { let address = "U1234567890123456" XCTAssertEqual(AdamantUtilities.validateAdamantAddress(address: address), AdamantUtilities.AddressValidationResult.valid) } - + func testMustBeLongerThanSixDigits() { let address = "U12345" XCTAssertEqual(AdamantUtilities.validateAdamantAddress(address: address), AdamantUtilities.AddressValidationResult.invalid) } - + func testMustHaveLeadingU() { let address1 = "B12345678910" let address2 = "12345678910" let address3 = "1U2345678910" - + XCTAssertEqual(AdamantUtilities.validateAdamantAddress(address: address1), AdamantUtilities.AddressValidationResult.invalid) XCTAssertEqual(AdamantUtilities.validateAdamantAddress(address: address2), AdamantUtilities.AddressValidationResult.invalid) XCTAssertEqual(AdamantUtilities.validateAdamantAddress(address: address3), AdamantUtilities.AddressValidationResult.invalid) } - + func testOnlyNumbers() { let address1 = "U12345d67890" let address2 = "U12345d7890_" - + XCTAssertEqual(AdamantUtilities.validateAdamantAddress(address: address1), AdamantUtilities.AddressValidationResult.invalid) XCTAssertEqual(AdamantUtilities.validateAdamantAddress(address: address2), AdamantUtilities.AddressValidationResult.invalid) } - + func testCapitalU() { let address = "u12345d67890" XCTAssertEqual(AdamantUtilities.validateAdamantAddress(address: address), AdamantUtilities.AddressValidationResult.invalid) } - + func testNoWhitespaces() { let address1 = " U12345d67890" let address2 = "U12345d67890 " - + XCTAssertEqual(AdamantUtilities.validateAdamantAddress(address: address1), AdamantUtilities.AddressValidationResult.invalid) XCTAssertEqual(AdamantUtilities.validateAdamantAddress(address: address2), AdamantUtilities.AddressValidationResult.invalid) } - + func testSystemAddresses() { let bounty = AdamantContacts.adamantBountyWallet.name let ico = AdamantContacts.adamantIco.name - + XCTAssertEqual(AdamantUtilities.validateAdamantAddress(address: bounty), AdamantUtilities.AddressValidationResult.system) XCTAssertEqual(AdamantUtilities.validateAdamantAddress(address: ico), AdamantUtilities.AddressValidationResult.system) } diff --git a/AdamantTests/Core/JSAdamantCore.swift b/AdamantTests/DisabledTests/Core/JSAdamantCore.swift similarity index 82% rename from AdamantTests/Core/JSAdamantCore.swift rename to AdamantTests/DisabledTests/Core/JSAdamantCore.swift index 6211ce8d9..9859628ca 100644 --- a/AdamantTests/Core/JSAdamantCore.swift +++ b/AdamantTests/DisabledTests/Core/JSAdamantCore.swift @@ -8,6 +8,7 @@ import Foundation import JavaScriptCore + @testable import Adamant // MARK: - Functions @@ -18,7 +19,7 @@ private enum JsFunction: String { case encodeMessage = "encodeMessage" case decodeMessage = "decodeMessage" case generatePassphrase = "generatePassphrase" - + var key: String { return self.rawValue } @@ -34,58 +35,58 @@ enum AdamantCoreError: Error { // MARK: - AdamantCore /// You must load JavaScript before calling any methods. -class JSAdamantCore : AdamantCore { - func encodeValue(_ value: [String : Any], privateKey: String) -> (message: String, nonce: String)? { +class JSAdamantCore: AdamantCore { + func encodeValue(_ value: [String: Any], privateKey: String) -> (message: String, nonce: String)? { return nil } - + func decodeValue(rawMessage: String, rawNonce: String, privateKey: String) -> String? { return nil } - + enum Result { case success case error(error: Error) } - + private var context: JSContext? private var loadingGroup: DispatchGroup? - + /// Load JSCore func loadJs(from url: URL, queue: DispatchQueue, completion: @escaping (Result) -> Void) { let loadingGroup = DispatchGroup() self.loadingGroup = loadingGroup loadingGroup.enter() - + queue.async { defer { loadingGroup.leave() self.loadingGroup = nil } - + do { // MARK: 1. Load JavaScript let core = try! String(contentsOf: url) - + // MARK: 2. Create context. guard let context = JSContext() else { throw AdamantCoreError.errorLoadingJS(reason: "Can't create JSContext!") } - + // MARK: 3. Catch JS errors var jsError: JSValue? context.exceptionHandler = { _, value in print("JSError: \(String(describing: value?.toString()))") jsError = value } - + // MARK: 4. Integrate logger context.evaluateScript("var console = { log: function(message) { _consoleLog(message) } }") let consoleLog: @convention(block) (String) -> Void = { message in print("JSCore: " + message) } context.setObject(unsafeBitCast(consoleLog, to: AnyObject.self), forKeyedSubscript: "_consoleLog" as (NSCopying & NSObjectProtocol)) - + // MARK: 5. Integrate PRNG, because Webpacks strips out crypto libraries. let crypto: @convention(block) (Int) -> JSValue = { count in let contextRef = context.jsGlobalContextRef @@ -93,17 +94,17 @@ class JSAdamantCore : AdamantCore { let buffer = JSObjectGetTypedArrayBuffer(contextRef, array, nil)! let bytes = JSObjectGetArrayBufferBytesPtr(contextRef, buffer, nil)! _ = SecRandomCopyBytes(kSecRandomDefault, count, bytes) - + return JSValue(jsValueRef: array, in: context) } context.setObject(unsafeBitCast(crypto, to: AnyObject.self), forKeyedSubscript: "_randombytes" as NSCopying & NSObjectProtocol) - + // MARK: 6. Evaluate Core script context.evaluateScript(core) if let jsError = jsError { throw AdamantCoreError.errorLoadingJS(reason: jsError.toString()) } - + // MARK: 7. Cleanup context.exceptionHandler = nil self.context = context @@ -118,37 +119,38 @@ class JSAdamantCore : AdamantCore { // MARK: - Working with JS runtime extension JSAdamantCore { private func get(function: JsFunction) -> JSValue? { - if let group = loadingGroup { // Wait for context loading. + if let group = loadingGroup { // Wait for context loading. group.wait() } - + guard let context = context else { print("Context not loaded!") return nil } - + var jsError: JSValue? context.exceptionHandler = { _, value in print("JSError: \(String(describing: value?.toString()))") jsError = value } - + let jsFunc: JSValue? if let core = context.objectForKeyedSubscript("adamant_core"), let adamant = core.objectForKeyedSubscript("Adamant"), let f = adamant.objectForKeyedSubscript(function.key), - !f.isUndefined, jsError == nil { + !f.isUndefined, jsError == nil + { jsFunc = f } else { jsFunc = nil } - + context.exceptionHandler = nil return jsFunc } - + private func call(function: JsFunction, with arguments: [Any]) -> JSValue? { - if let group = loadingGroup { // Wait for context loading. + if let group = loadingGroup { // Wait for context loading. group.wait() } @@ -160,10 +162,10 @@ extension JSAdamantCore { fatalError("Failed to get function: \(function.key)") } -// var jsError: JSValue? = nil + // var jsError: JSValue? = nil context.exceptionHandler = { _, exc in print("JSError: \(String(describing: exc?.toString()))") -// jsError = exc + // jsError = exc } let jsValue = jsFunction.call(withArguments: arguments) @@ -176,29 +178,29 @@ extension JSAdamantCore { // MARK: - Hash converters extension JSAdamantCore { private func convertToJsHash(_ hash: [UInt8]) -> JSValue { - if let group = loadingGroup { // Wait for context loading. + if let group = loadingGroup { // Wait for context loading. group.wait() } - + let count = hash.count if count == 0 { return JSValue(newArrayIn: context) } - + let contextRef = context!.jsGlobalContextRef let jsArray = JSObjectMakeTypedArray(contextRef, kJSTypedArrayTypeUint8Array, count, nil)! let jsBuffer = JSObjectGetTypedArrayBuffer(contextRef, jsArray, nil)! let jsBytes = JSObjectGetArrayBufferBytesPtr(contextRef, jsBuffer, nil)! - + let typedPointer = jsBytes.bindMemory(to: UInt8.self, capacity: count) - + let buffer = UnsafeMutableBufferPointer.init(start: typedPointer, count: count) let data = Data(bytes: hash) _ = data.copyBytes(to: buffer) - + return JSValue(jsValueRef: jsArray, in: context) } - + private func convertFromJsHash(_ jsHash: JSValue) -> [UInt8]? { return jsHash.toArray() as? [UInt8] } @@ -208,49 +210,53 @@ extension JSAdamantCore { extension JSAdamantCore { func createKeypairFor(rawHash: [UInt8]) -> Keypair? { let jsHash = convertToJsHash(rawHash) - + if let keypairRaw = call(function: .makeKeypair, with: [jsHash]), keypairRaw.hasProperty("publicKey") && keypairRaw.hasProperty("privateKey"), let publicKeyHash = self.convertFromJsHash(keypairRaw.forProperty("publicKey")), - let privateKeyHash = self.convertFromJsHash(keypairRaw.forProperty("privateKey")) { - let keypair = Keypair(publicKey: AdamantUtilities.getHexString(from: publicKeyHash), - privateKey: AdamantUtilities.getHexString(from: privateKeyHash)) + let privateKeyHash = self.convertFromJsHash(keypairRaw.forProperty("privateKey")) + { + let keypair = Keypair( + publicKey: AdamantUtilities.getHexString(from: publicKeyHash), + privateKey: AdamantUtilities.getHexString(from: privateKeyHash) + ) return keypair } else { return nil } } - + func createKeypairFor(passphrase: String) -> Keypair? { guard let hash = createHashFor(passphrase: passphrase), hash.count > 0 else { return nil } - + return createKeypairFor(rawHash: AdamantUtilities.getBytes(from: hash)) } - + func createHashFor(passphrase: String) -> String? { let hash: String? - + if let jsHash = call(function: .createPassPhraseHash, with: [passphrase]), !jsHash.isUndefined, - let hashRaw = convertFromJsHash(jsHash) { + let hashRaw = convertFromJsHash(jsHash) + { hash = AdamantUtilities.getHexString(from: hashRaw) } else { hash = nil } - + return hash } - + func generateNewPassphrase() -> String { let passphrase: String - + if let jsPassphrase = call(function: .generatePassphrase, with: []), !jsPassphrase.isUndefined, let p = jsPassphrase.toString() { passphrase = p } else { fatalError("Can't generate new passphrase") } - + return passphrase } } @@ -268,32 +274,35 @@ extension JSAdamantCore { if let vote = t.asset.votes { asset.votes = vote.votes } - - let jsTransaction = JSTransaction(id: 0, - height: 0, - blockId: 0, - type: t.type.rawValue, - timestamp: t.timestamp, - senderPublicKey: t.senderPublicKey, - senderId: senderId, - recipientId: t.recipientId, - recipientPublicKey: t.requesterPublicKey, - amount: (t.amount.shiftedToAdamant() as NSDecimalNumber).uint64Value, - fee: 0, - signature: "", - confirmations: 0, - asset: asset) - + + let jsTransaction = JSTransaction( + id: 0, + height: 0, + blockId: 0, + type: t.type.rawValue, + timestamp: t.timestamp, + senderPublicKey: t.senderPublicKey, + senderId: senderId, + recipientId: t.recipientId, + recipientPublicKey: t.requesterPublicKey, + amount: (t.amount.shiftedToAdamant() as NSDecimalNumber).uint64Value, + fee: 0, + signature: "", + confirmations: 0, + asset: asset + ) + let jsKeypair = JSKeypair(keypair: keypair) - + let signature: String? if let jsSignature = call(function: .transactionSign, with: [jsTransaction, jsKeypair]), - !jsSignature.isUndefined { + !jsSignature.isUndefined + { signature = jsSignature.toString() } else { signature = nil } - + return signature } } @@ -302,32 +311,34 @@ extension JSAdamantCore { extension JSAdamantCore { func encodeMessage(_ message: String, recipientPublicKey publicKey: String, privateKey privateKeyHex: String) -> (message: String, nonce: String)? { let privateKey = AdamantUtilities.getBytes(from: privateKeyHex) - + let encodedMessage: (String, String)? if let jsMessage = call(function: .encodeMessage, with: [message, publicKey, privateKey]), !jsMessage.isUndefined, - let m = jsMessage.forProperty("message").toString(), let o = jsMessage.forProperty("own_message").toString() { + let m = jsMessage.forProperty("message").toString(), let o = jsMessage.forProperty("own_message").toString() + { encodedMessage = (message: m, nonce: o) } else { encodedMessage = nil } - + return encodedMessage } - + func decodeMessage(rawMessage: String, rawNonce: String, senderPublicKey senderKeyHex: String, privateKey privateKeyHex: String) -> String? { let message = convertToJsHash(AdamantUtilities.getBytes(from: rawMessage)) let nonce = convertToJsHash(AdamantUtilities.getBytes(from: rawNonce)) let senderKey = convertToJsHash(AdamantUtilities.getBytes(from: senderKeyHex)) let privateKey = convertToJsHash(AdamantUtilities.getBytes(from: privateKeyHex)) - + let decodedMessage: String? if let jsMessage = call(function: .decodeMessage, with: [message, nonce, senderKey, privateKey]), - !jsMessage.isUndefined, let m = jsMessage.toString() { + !jsMessage.isUndefined, let m = jsMessage.toString() + { decodedMessage = m } else { decodedMessage = nil } - + return decodedMessage } } diff --git a/AdamantTests/Core/JSAdamantCoreTests.swift b/AdamantTests/DisabledTests/Core/JSAdamantCoreTests.swift similarity index 66% rename from AdamantTests/Core/JSAdamantCoreTests.swift rename to AdamantTests/DisabledTests/Core/JSAdamantCoreTests.swift index 0405b93f8..af93f72f5 100644 --- a/AdamantTests/Core/JSAdamantCoreTests.swift +++ b/AdamantTests/DisabledTests/Core/JSAdamantCoreTests.swift @@ -7,132 +7,142 @@ // import XCTest + @testable import Adamant class JSAdamantCoreTests: XCTestCase { var core: AdamantCore! - + override func setUp() { super.setUp() - + guard let jsCore = Bundle(for: type(of: self)).url(forResource: "adamant-core", withExtension: "js") else { - fatalError("Can't load system resources!") + fatalError("Can't load system resources!") } - + let core = JSAdamantCore() core.loadJs(from: jsCore, queue: DispatchQueue.global(qos: .utility)) { (result) in if case .error = result { fatalError() } } - + self.core = core } - + func testHashForPassphrase() { let passphrase = "process gospel angry height between flat always clock suit refuse shove verb" let hash = "9001490b166816af75a15a3e2b0174bfe3be3dfaa63147b4f780ed3ab90ffeab" - + let freshHash = core.createHashFor(passphrase: passphrase) XCTAssertEqual(hash, freshHash) } - + func testKeypairForPassphrase() { let passphrase = "process gospel angry height between flat always clock suit refuse shove verb" let publicKey = "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f" - let privateKey = "9001490b166816af75a15a3e2b0174bfe3be3dfaa63147b4f780ed3ab90ffeab8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f" - + let privateKey = "9001490b166816af75a15a3e2b0174bfe3be3dfaa63147b4f780ed3ab90ffeab8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f" + let freshKeypair = core.createKeypairFor(passphrase: passphrase) XCTAssertEqual(publicKey, freshKeypair?.publicKey) XCTAssertEqual(privateKey, freshKeypair?.privateKey) } - + func testGeneratePassphrase() { let passphrase = core.generateNewPassphrase() - + XCTAssert(passphrase.split(separator: " ").count == 12) } - + func testSignTransaction() { - let transaction = NormalizedTransaction(type: TransactionType.send, - amount: 60000000, - senderPublicKey: "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f", - requesterPublicKey: nil, - timestamp: 13131802, - recipientId: "U7038846184609740192", - asset: TransactionAsset()) + let transaction = NormalizedTransaction( + type: TransactionType.send, + amount: 60_000_000, + senderPublicKey: "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f", + requesterPublicKey: nil, + timestamp: 13_131_802, + recipientId: "U7038846184609740192", + asset: TransactionAsset() + ) let senderId = "U2279741505997340299" - let keypair = Keypair(publicKey: "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f", - privateKey: "9001490b166816af75a15a3e2b0174bfe3be3dfaa63147b4f780ed3ab90ffeab8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f") - + let keypair = Keypair( + publicKey: "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f", + privateKey: "9001490b166816af75a15a3e2b0174bfe3be3dfaa63147b4f780ed3ab90ffeab8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f" + ) + let signature = "cdde6db8cfa9ebbca67f4625b0fdded5a130f01b4300423c4446e7b8ed79f95447be8b4dfd5d67b849d47bd9d834ddff3942499d350673e129f15ba2c1005807" - + let freshSignature = core.sign(transaction: transaction, senderId: senderId, keypair: keypair) XCTAssertEqual(signature, freshSignature) } - + func testEncodeMessage() { let message = "common" let aPublicKey = "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f" let aPrivateKey = "9001490b166816af75a15a3e2b0174bfe3be3dfaa63147b4f780ed3ab90ffeab8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f" let bPublicKey = "9f895a201fd92cc60ef02d2117d53f00dc2981903cb64b2f214777269b882209" let bPrivateKey = "e91ee8e6a23ac5ff9452a15a3fbd14098dc2c6a5abf6b12464b09eb033580b6d9f895a201fd92cc60ef02d2117d53f00dc2981903cb64b2f214777269b882209" - + guard let encoded = core.encodeMessage(message, recipientPublicKey: bPublicKey, privateKey: aPrivateKey) else { XCTFail() return } - - guard let decoded = core.decodeMessage(rawMessage: encoded.message, rawNonce: encoded.nonce, senderPublicKey: aPublicKey, privateKey: bPrivateKey) else { + + guard let decoded = core.decodeMessage(rawMessage: encoded.message, rawNonce: encoded.nonce, senderPublicKey: aPublicKey, privateKey: bPrivateKey) + else { XCTFail() return } - + XCTAssertEqual(message, decoded) } - + func testDecodeMessage() { let publicKey = "9f895a201fd92cc60ef02d2117d53f00dc2981903cb64b2f214777269b882209" let privateKey = "9001490b166816af75a15a3e2b0174bfe3be3dfaa63147b4f780ed3ab90ffeab8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f" let message = "09af1ce7e5ed484ddca3c6d1410cbf4f793ea19210e7" let nonce = "31caaee2d35dcbd8b614e9d6bf6095393cb5baed259e7e37" let decodedMessage = "common" - + let freshMessage = core.decodeMessage(rawMessage: message, rawNonce: nonce, senderPublicKey: publicKey, privateKey: privateKey) - + XCTAssertEqual(freshMessage, decodedMessage) } - + // MARK: - Performance - + func testPerformanceHashForPassphrase() { let passphrase = "process gospel angry height between flat always clock suit refuse shove verb" - + self.measure { _ = core.createHashFor(passphrase: passphrase) } } - + func testPerformanceKeypairForPassphrase() { let passphrase = "process gospel angry height between flat always clock suit refuse shove verb" - + self.measure { _ = core.createKeypairFor(passphrase: passphrase) } } - + func testPerformanceSignTransaction() { - let transaction = NormalizedTransaction(type: TransactionType.send, - amount: 50000000, - senderPublicKey: "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f", - requesterPublicKey: nil, - timestamp: 11325525, - recipientId: "U48484848484848484848484", - asset: TransactionAsset()) + let transaction = NormalizedTransaction( + type: TransactionType.send, + amount: 50_000_000, + senderPublicKey: "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f", + requesterPublicKey: nil, + timestamp: 11_325_525, + recipientId: "U48484848484848484848484", + asset: TransactionAsset() + ) let senderId = "U2279741505997340299" - let keypair = Keypair(publicKey: "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f", - privateKey: "9001490b166816af75a15a3e2b0174bfe3be3dfaa63147b4f780ed3ab90ffeab8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f") - + let keypair = Keypair( + publicKey: "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f", + privateKey: "9001490b166816af75a15a3e2b0174bfe3be3dfaa63147b4f780ed3ab90ffeab8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f" + ) + self.measure { _ = core.sign(transaction: transaction, senderId: senderId, keypair: keypair) } diff --git a/AdamantTests/Core/JSModels.swift b/AdamantTests/DisabledTests/Core/JSModels.swift similarity index 90% rename from AdamantTests/Core/JSModels.swift rename to AdamantTests/DisabledTests/Core/JSModels.swift index 189e61263..ab002a806 100644 --- a/AdamantTests/Core/JSModels.swift +++ b/AdamantTests/DisabledTests/Core/JSModels.swift @@ -8,6 +8,7 @@ import Foundation import JavaScriptCore + @testable import Adamant // MARK: Keypair @@ -20,12 +21,12 @@ import JavaScriptCore @objc class JSKeypair: NSObject, JSKeypairProtocol { dynamic var publicKey: String dynamic var privateKey: String - + init(publicKey: String, privateKey: String) { self.publicKey = publicKey self.privateKey = privateKey } - + init(keypair: Keypair) { self.publicKey = keypair.publicKey self.privateKey = keypair.privateKey @@ -58,7 +59,7 @@ import JavaScriptCore dynamic var message: String dynamic var own_message: String dynamic var type: Int - + init(type: Int, message: String, own_message: String) { self.message = message self.own_message = own_message @@ -78,7 +79,7 @@ import JavaScriptCore dynamic var key: String dynamic var value: String dynamic var type: Int - + init(key: String, value: String, type: Int) { self.key = key self.value = value @@ -121,7 +122,22 @@ import JavaScriptCore dynamic var confirmations: UInt64 dynamic var asset: JSAsset - init(id: UInt64, height: Int, blockId: UInt64, type: Int, timestamp: UInt64, senderPublicKey: String?, senderId: String?, recipientId: String?, recipientPublicKey: String?, amount: UInt64, fee: UInt64, signature: String?, confirmations: UInt64, asset: JSAsset) { + init( + id: UInt64, + height: Int, + blockId: UInt64, + type: Int, + timestamp: UInt64, + senderPublicKey: String?, + senderId: String?, + recipientId: String?, + recipientPublicKey: String?, + amount: UInt64, + fee: UInt64, + signature: String?, + confirmations: UInt64, + asset: JSAsset + ) { self.id = id self.height = height self.blockId = blockId diff --git a/AdamantTests/Core/NativeCoreTests.swift b/AdamantTests/DisabledTests/Core/NativeCoreTests.swift similarity index 75% rename from AdamantTests/Core/NativeCoreTests.swift rename to AdamantTests/DisabledTests/Core/NativeCoreTests.swift index b673f41d9..f8b36a591 100644 --- a/AdamantTests/Core/NativeCoreTests.swift +++ b/AdamantTests/DisabledTests/Core/NativeCoreTests.swift @@ -7,189 +7,201 @@ // import XCTest + @testable import Adamant class NativeCoreTests: XCTestCase { var core: AdamantCore! - + override func setUp() { super.setUp() - + core = NativeAdamantCore() } func testHashForPassphrase() { let passphrase = "process gospel angry height between flat always clock suit refuse shove verb" let hash = "9001490b166816af75a15a3e2b0174bfe3be3dfaa63147b4f780ed3ab90ffeab" - + let freshHash = core.createHashFor(passphrase: passphrase) XCTAssertEqual(hash, freshHash) } - + func testKeypairForPassphrase() { let passphrase = "process gospel angry height between flat always clock suit refuse shove verb" let publicKey = "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f" - let privateKey = "9001490b166816af75a15a3e2b0174bfe3be3dfaa63147b4f780ed3ab90ffeab8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f" - + let privateKey = "9001490b166816af75a15a3e2b0174bfe3be3dfaa63147b4f780ed3ab90ffeab8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f" + let freshKeypair = core.createKeypairFor(passphrase: passphrase) XCTAssertEqual(publicKey, freshKeypair?.publicKey) XCTAssertEqual(privateKey, freshKeypair?.privateKey) } - + func testGeneratePassphrase() { let passphrase = core.generateNewPassphrase() - + XCTAssert(passphrase.split(separator: " ").count == 12) } - + func testSignTransaction() { - let transaction = NormalizedTransaction(type: TransactionType.send, - amount: 60000000, - senderPublicKey: "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f", - requesterPublicKey: nil, - timestamp: 13131802, - recipientId: "U7038846184609740192", - asset: TransactionAsset()) + let transaction = NormalizedTransaction( + type: TransactionType.send, + amount: 60_000_000, + senderPublicKey: "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f", + requesterPublicKey: nil, + timestamp: 13_131_802, + recipientId: "U7038846184609740192", + asset: TransactionAsset() + ) let senderId = "U2279741505997340299" - let keypair = Keypair(publicKey: "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f", - privateKey: "9001490b166816af75a15a3e2b0174bfe3be3dfaa63147b4f780ed3ab90ffeab8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f") - + let keypair = Keypair( + publicKey: "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f", + privateKey: "9001490b166816af75a15a3e2b0174bfe3be3dfaa63147b4f780ed3ab90ffeab8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f" + ) + let signature = "cdde6db8cfa9ebbca67f4625b0fdded5a130f01b4300423c4446e7b8ed79f95447be8b4dfd5d67b849d47bd9d834ddff3942499d350673e129f15ba2c1005807" - + let freshSignature = core.sign(transaction: transaction, senderId: senderId, keypair: keypair) XCTAssertEqual(signature, freshSignature) } - + func testEncodeMessage() { let message = "common" let aPublicKey = "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f" let aPrivateKey = "9001490b166816af75a15a3e2b0174bfe3be3dfaa63147b4f780ed3ab90ffeab8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f" let bPublicKey = "9f895a201fd92cc60ef02d2117d53f00dc2981903cb64b2f214777269b882209" let bPrivateKey = "e91ee8e6a23ac5ff9452a15a3fbd14098dc2c6a5abf6b12464b09eb033580b6d9f895a201fd92cc60ef02d2117d53f00dc2981903cb64b2f214777269b882209" - + guard let encoded = core.encodeMessage(message, recipientPublicKey: bPublicKey, privateKey: aPrivateKey) else { XCTFail() return } - - guard let decoded = core.decodeMessage(rawMessage: encoded.message, rawNonce: encoded.nonce, senderPublicKey: aPublicKey, privateKey: bPrivateKey) else { + + guard let decoded = core.decodeMessage(rawMessage: encoded.message, rawNonce: encoded.nonce, senderPublicKey: aPublicKey, privateKey: bPrivateKey) + else { XCTFail() return } - + XCTAssertEqual(message, decoded) } - + func testDecodeMessage() { let publicKey = "9f895a201fd92cc60ef02d2117d53f00dc2981903cb64b2f214777269b882209" let privateKey = "9001490b166816af75a15a3e2b0174bfe3be3dfaa63147b4f780ed3ab90ffeab8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f" let message = "09af1ce7e5ed484ddca3c6d1410cbf4f793ea19210e7" let nonce = "31caaee2d35dcbd8b614e9d6bf6095393cb5baed259e7e37" let decodedMessage = "common" - + let freshMessage = core.decodeMessage(rawMessage: message, rawNonce: nonce, senderPublicKey: publicKey, privateKey: privateKey) - + XCTAssertEqual(freshMessage, decodedMessage) } - + // MARK: - JS to Native - + func testDecodeJsEncodedMessage() { let message = "common" let aPublicKey = "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f" let aPrivateKey = "9001490b166816af75a15a3e2b0174bfe3be3dfaa63147b4f780ed3ab90ffeab8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f" let bPublicKey = "9f895a201fd92cc60ef02d2117d53f00dc2981903cb64b2f214777269b882209" let bPrivateKey = "e91ee8e6a23ac5ff9452a15a3fbd14098dc2c6a5abf6b12464b09eb033580b6d9f895a201fd92cc60ef02d2117d53f00dc2981903cb64b2f214777269b882209" - + guard let js = Bundle(for: type(of: self)).url(forResource: "adamant-core", withExtension: "js") else { fatalError("Can't load system resources!") } - + let jsCore = JSAdamantCore() jsCore.loadJs(from: js, queue: DispatchQueue.global(qos: .utility)) { (result) in if case .error = result { fatalError() } } - + // Encode with JS guard let encoded = jsCore.encodeMessage(message, recipientPublicKey: bPublicKey, privateKey: aPrivateKey) else { XCTFail() return } - + // Decode with Native - guard let decoded = core.decodeMessage(rawMessage: encoded.message, rawNonce: encoded.nonce, senderPublicKey: aPublicKey, privateKey: bPrivateKey) else { + guard let decoded = core.decodeMessage(rawMessage: encoded.message, rawNonce: encoded.nonce, senderPublicKey: aPublicKey, privateKey: bPrivateKey) + else { XCTFail() return } - + XCTAssertEqual(message, decoded) } - + func testEncodeMessageForJs() { let message = "common" let aPublicKey = "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f" let aPrivateKey = "9001490b166816af75a15a3e2b0174bfe3be3dfaa63147b4f780ed3ab90ffeab8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f" let bPublicKey = "9f895a201fd92cc60ef02d2117d53f00dc2981903cb64b2f214777269b882209" let bPrivateKey = "e91ee8e6a23ac5ff9452a15a3fbd14098dc2c6a5abf6b12464b09eb033580b6d9f895a201fd92cc60ef02d2117d53f00dc2981903cb64b2f214777269b882209" - + guard let js = Bundle(for: type(of: self)).url(forResource: "adamant-core", withExtension: "js") else { fatalError("Can't load system resources!") } - + let jsCore = JSAdamantCore() jsCore.loadJs(from: js, queue: DispatchQueue.global(qos: .utility)) { (result) in if case .error = result { fatalError() } } - + // Encode with native guard let encoded = core.encodeMessage(message, recipientPublicKey: bPublicKey, privateKey: aPrivateKey) else { XCTFail() return } - + // Decode with JS - guard let decoded = jsCore.decodeMessage(rawMessage: encoded.message, rawNonce: encoded.nonce, senderPublicKey: aPublicKey, privateKey: bPrivateKey) else { + guard let decoded = jsCore.decodeMessage(rawMessage: encoded.message, rawNonce: encoded.nonce, senderPublicKey: aPublicKey, privateKey: bPrivateKey) + else { XCTFail() return } - + XCTAssertEqual(message, decoded) } - + // MARK: - Performance - + func testPerformanceHashForPassphrase() { let passphrase = "process gospel angry height between flat always clock suit refuse shove verb" - + self.measure { _ = core.createHashFor(passphrase: passphrase) } } - + func testPerformanceKeypairForPassphrase() { let passphrase = "process gospel angry height between flat always clock suit refuse shove verb" - + self.measure { _ = core.createKeypairFor(passphrase: passphrase) } } - + func testPerformanceSignTransaction() { - let transaction = NormalizedTransaction(type: TransactionType.send, - amount: 50000000, - senderPublicKey: "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f", - requesterPublicKey: nil, - timestamp: 11325525, - recipientId: "U48484848484848484848484", - asset: TransactionAsset()) + let transaction = NormalizedTransaction( + type: TransactionType.send, + amount: 50_000_000, + senderPublicKey: "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f", + requesterPublicKey: nil, + timestamp: 11_325_525, + recipientId: "U48484848484848484848484", + asset: TransactionAsset() + ) let senderId = "U2279741505997340299" - let keypair = Keypair(publicKey: "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f", - privateKey: "9001490b166816af75a15a3e2b0174bfe3be3dfaa63147b4f780ed3ab90ffeab8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f") - + let keypair = Keypair( + publicKey: "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f", + privateKey: "9001490b166816af75a15a3e2b0174bfe3be3dfaa63147b4f780ed3ab90ffeab8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f" + ) + self.measure { _ = core.sign(transaction: transaction, senderId: senderId, keypair: keypair) } diff --git a/AdamantTests/Core/adamant-core.js b/AdamantTests/DisabledTests/Core/adamant-core.js similarity index 100% rename from AdamantTests/Core/adamant-core.js rename to AdamantTests/DisabledTests/Core/adamant-core.js diff --git a/AdamantTests/CurrencyFormatterTests.swift b/AdamantTests/DisabledTests/CurrencyFormatterTests.swift similarity index 95% rename from AdamantTests/CurrencyFormatterTests.swift rename to AdamantTests/DisabledTests/CurrencyFormatterTests.swift index a4991ad1c..d61cc6d9b 100644 --- a/AdamantTests/CurrencyFormatterTests.swift +++ b/AdamantTests/DisabledTests/CurrencyFormatterTests.swift @@ -7,15 +7,16 @@ // import XCTest + @testable import Adamant class CurrencyFormatterTests: XCTestCase { var decimalSeparator: String = Locale.current.decimalSeparator! - + func testInt() { let number = Decimal(123) let symbol = AdmWalletService.currencySymbol - + let format = "123 \(symbol)" let formatted = AdamantBalanceFormat.full.format(number, withCurrencySymbol: symbol) @@ -25,7 +26,7 @@ class CurrencyFormatterTests: XCTestCase { func testFracted() { let number = Decimal(123.5) let symbol = AdmWalletService.currencySymbol - + let format = "123\(decimalSeparator)5 \(symbol)" let formatted = AdamantBalanceFormat.full.format(number, withCurrencySymbol: symbol) @@ -35,7 +36,7 @@ class CurrencyFormatterTests: XCTestCase { func testZeroFracted() { let number = Decimal(0.53) let symbol = AdmWalletService.currencySymbol - + let format = "0\(decimalSeparator)53 \(symbol)" let formatted = AdamantBalanceFormat.full.format(number, withCurrencySymbol: symbol) @@ -45,37 +46,37 @@ class CurrencyFormatterTests: XCTestCase { func testVerySmallFracted() { let number = Decimal(0.00000007) let symbol = AdmWalletService.currencySymbol - + let format = "0\(decimalSeparator)00000007 \(symbol)" let formatted = AdamantBalanceFormat.full.format(number, withCurrencySymbol: symbol) XCTAssertEqual(format, formatted) } - + func testTooSmallFracted() { let number = Decimal(0.0000000699) let symbol = AdmWalletService.currencySymbol - + let format = "0\(decimalSeparator)00000006 \(symbol)" let formatted = AdamantBalanceFormat.full.format(number, withCurrencySymbol: symbol) - + XCTAssertEqual(format, formatted) } func testLargeInt() { - let number = Decimal(34903483984) + let number = Decimal(34_903_483_984) let symbol = AdmWalletService.currencySymbol - + let format = "34903483984 \(symbol)" let formatted = AdamantBalanceFormat.full.format(number, withCurrencySymbol: symbol) - + XCTAssertEqual(format, formatted) } func testLargeNumber() { let number = Decimal(9342034.5848984) let symbol = AdmWalletService.currencySymbol - + let format = "9342034\(decimalSeparator)5848984 \(symbol)" let formatted = AdamantBalanceFormat.full.format(number, withCurrencySymbol: symbol) @@ -85,7 +86,7 @@ class CurrencyFormatterTests: XCTestCase { func testNegative() { let number = Decimal(-34.504) let symbol = AdmWalletService.currencySymbol - + let format = "-34\(decimalSeparator)504 \(symbol)" let formatted = AdamantBalanceFormat.full.format(number, withCurrencySymbol: symbol) diff --git a/AdamantTests/DisabledTests/FeeTests.swift b/AdamantTests/DisabledTests/FeeTests.swift new file mode 100644 index 000000000..5b90d80e9 --- /dev/null +++ b/AdamantTests/DisabledTests/FeeTests.swift @@ -0,0 +1,91 @@ +// +// FeeTests.swift +// AdamantTests +// +// Created by Anokhov Pavel on 16.01.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import XCTest + +@testable import Adamant + +class FeeTests: XCTestCase { + func testTransferFee() { + let estimatedFee = Decimal(0.5) + XCTAssertEqual(estimatedFee, AdamantTransfersProvider.transferFee) + } + + func testShortMessageFee() { + let message = "A quick brown fox bought bitcoins in 2009. Good for you, mr fox. You quick brown mother fucker." + let estimatedFee = Decimal(0.001) + + XCTAssertEqual(estimatedFee, AdamantMessage.text(message).fee) + } + + func testLongMessageFee() { + let message = """ + The sperm whale's cerebrum is the largest in all mammalia, both in absolute and relative terms. + The olfactory system is reduced, suggesting that the sperm whale has a poor sense of taste and smell. + By contrast, the auditory system is enlarged. + The pyramidal tract is poorly developed, reflecting the reduction of its limbs. + """ + let estimatedFee = Decimal(0.002) + + XCTAssertEqual(estimatedFee, AdamantMessage.text(message).fee) + } + + func testVeryLongMessageFee() { + let message = """ + Lift you up again + Give you to the trees + All sound and visions are + What they ask of me + + Let's run fast through the fields + Over mountaintops + Let's swim through ocean water + And we'll never stop + + Just close your eyes + And pretend that everything's fine + Just close your eyes + I'll tell you when + + Can you show me + Where to find the stream? + I've been told before + That the water's clean + + Will you come with me? + Two of us can drink + Move quick, we've got to hurry + There's no time to think + + Just close your eyes + And pretend that everything's fine + Just close your eyes + I'll tell you when + + We didn't come this far + Just to turn around + We didn't come this far + Just to run away + Just ahead + We will hear the sound + The sound that gives us + A brand new day + + Just close your eyes + And pretend that everything's fine + Just close your eyes + I'll tell you when + + Mastodon / The Hunter / All The Heavy Lifting + Brann Timothy Dailor / Troy Jayson Sanders / William Breen Kelliher / William Brent Hinds + """ + let estimatedFee = Decimal(0.004) + + XCTAssertEqual(estimatedFee, AdamantMessage.text(message).fee) + } +} diff --git a/AdamantTests/HexAndBytesUtilitiesTest.swift b/AdamantTests/DisabledTests/HexAndBytesUtilitiesTest.swift similarity index 76% rename from AdamantTests/HexAndBytesUtilitiesTest.swift rename to AdamantTests/DisabledTests/HexAndBytesUtilitiesTest.swift index 26a989d73..8f2134c7b 100644 --- a/AdamantTests/HexAndBytesUtilitiesTest.swift +++ b/AdamantTests/DisabledTests/HexAndBytesUtilitiesTest.swift @@ -7,17 +7,20 @@ // import XCTest + @testable import Adamant class HexAndBytesUtilitiesTest: XCTestCase { - let bytes: [UInt8] = [144, 1, 73, 11, 22, 104, 22, 175, 117, 161, 90, 62, 43, 1, 116, 191, 227, 190, 61, 250, 166, 49, 71, 180, 247, 128, 237, 58, 185, 15, 254, 171] + let bytes: [UInt8] = [ + 144, 1, 73, 11, 22, 104, 22, 175, 117, 161, 90, 62, 43, 1, 116, 191, 227, 190, 61, 250, 166, 49, 71, 180, 247, 128, 237, 58, 185, 15, 254, 171 + ] let hex = "9001490b166816af75a15a3e2b0174bfe3be3dfaa63147b4f780ed3ab90ffeab" - + func testBytesToHex() { let freshHex = AdamantUtilities.getHexString(from: bytes) XCTAssertEqual(hex, freshHex) } - + func testHexToBytes() { let freshBytes = AdamantUtilities.getBytes(from: hex) XCTAssertEqual(bytes, freshBytes) diff --git a/AdamantTests/NodesAllowanceTests.swift b/AdamantTests/DisabledTests/NodesAllowanceTests.swift similarity index 91% rename from AdamantTests/NodesAllowanceTests.swift rename to AdamantTests/DisabledTests/NodesAllowanceTests.swift index 86c83d21a..cb99aa4be 100644 --- a/AdamantTests/NodesAllowanceTests.swift +++ b/AdamantTests/DisabledTests/NodesAllowanceTests.swift @@ -7,103 +7,104 @@ // import XCTest + @testable import Adamant class NodesAllowanceTests: XCTest { var nodes = [Node]() - + override func setUp() { super.setUp() nodes = [] } - + // MARK: - Allowed nodes tests without WS support - + func testOneAllowedNode() { let node = makeTestNode(connectionStatus: .allowed) nodes = [node] - + XCTAssertEqual([node], allowedNodes(ws: false)) } - + func testOneNodeWithoutConnectionStatusIsAllowed() { let node = makeTestNode() nodes = [node] - + XCTAssertEqual([node], allowedNodes(ws: false)) } - + func testOneDisabledNodeIsNotAllowed() { let node = makeTestNode() node.isEnabled = false nodes = [node] - + XCTAssert(allowedNodes(ws: false).isEmpty) } - + func testOneOfflineNodeIsAllowed() { let node = makeTestNode(connectionStatus: .offline) nodes = [node] - + XCTAssertEqual([node], allowedNodes(ws: false)) } - + func testManyAllowedNodesSortedBySpeedDescending() { - let nodes: [Node] = (0 ..< 100).map { ping in + let nodes: [Node] = (0..<100).map { ping in let node = makeTestNode(connectionStatus: .allowed) node.status = .init(ping: TimeInterval(ping), wsEnabled: false, height: nil, version: nil) return node } - + self.nodes = nodes.shuffled() - + XCTAssertEqual(nodes, allowedNodes(ws: false)) } - + // MARK: - Allowed nodes tests with WS support - + func testOneAllowedNodeWithoutWSIsNotAllowedWS() { let node = makeTestNode(connectionStatus: .allowed) node.status = .init(ping: .zero, wsEnabled: false, height: nil, version: nil) self.nodes = [node] - + XCTAssert(allowedNodes(ws: true).isEmpty) } - + func testOneAllowedWSNodeIsAllowedWS() { let node = makeTestNode(connectionStatus: .allowed) node.status = .init(ping: .zero, wsEnabled: true, height: nil, version: nil) self.nodes = [node] - + XCTAssertEqual([node], allowedNodes(ws: true)) } - + func testOneWSNodeWithoutConnectionStatusIsNotAllowedWS() { let node = makeTestNode() node.status = .init(ping: .zero, wsEnabled: true, height: nil, version: nil) self.nodes = [node] - + XCTAssert(allowedNodes(ws: true).isEmpty) } - + func testManyAllowedNodesSortedBySpeedDescendingWS() { - let nodes: [Node] = (0 ..< 100).map { ping in + let nodes: [Node] = (0..<100).map { ping in let node = makeTestNode(connectionStatus: .allowed) node.status = .init(ping: TimeInterval(ping), wsEnabled: true, height: nil, version: nil) return node } - + self.nodes = nodes.shuffled() - + XCTAssertEqual(nodes, allowedNodes(ws: true)) } - + // MARK: - Helpers - + private func allowedNodes(ws: Bool) -> [Node] { nodes.getAllowedNodes(sortedBySpeedDescending: true, needWS: ws) } - + private func makeTestNode(connectionStatus: NodeConnectionStatus = .synchronizing) -> Node { let node = Node(scheme: .default, host: "", port: nil) node.connectionStatus = connectionStatus diff --git a/AdamantTests/Parsing/Account.json b/AdamantTests/DisabledTests/Parsing/Account.json similarity index 100% rename from AdamantTests/Parsing/Account.json rename to AdamantTests/DisabledTests/Parsing/Account.json diff --git a/AdamantTests/Parsing/Chat.json b/AdamantTests/DisabledTests/Parsing/Chat.json similarity index 100% rename from AdamantTests/Parsing/Chat.json rename to AdamantTests/DisabledTests/Parsing/Chat.json diff --git a/AdamantTests/Parsing/NormalizedTransaction.json b/AdamantTests/DisabledTests/Parsing/NormalizedTransaction.json similarity index 100% rename from AdamantTests/Parsing/NormalizedTransaction.json rename to AdamantTests/DisabledTests/Parsing/NormalizedTransaction.json diff --git a/AdamantTests/Parsing/ParsingModelsTests.swift b/AdamantTests/DisabledTests/Parsing/ParsingModelsTests.swift similarity index 88% rename from AdamantTests/Parsing/ParsingModelsTests.swift rename to AdamantTests/DisabledTests/Parsing/ParsingModelsTests.swift index bf38b3d1b..c783a28ec 100644 --- a/AdamantTests/Parsing/ParsingModelsTests.swift +++ b/AdamantTests/DisabledTests/Parsing/ParsingModelsTests.swift @@ -7,17 +7,18 @@ // import XCTest + @testable import Adamant class ParsingModelsTests: XCTestCase { func testTransactionSend() { let t: Transaction = TestTools.LoadJsonAndDecode(filename: "TransactionSend") - - XCTAssertEqual(t.id, 1873173140086400619) + + XCTAssertEqual(t.id, 1_873_173_140_086_400_619) XCTAssertEqual(t.height, 777336) XCTAssertEqual(t.blockId, "10172499053153614044") XCTAssertEqual(t.type, TransactionType.send) - XCTAssertEqual(t.timestamp, 10724447) + XCTAssertEqual(t.timestamp, 10_724_447) XCTAssertEqual(t.senderPublicKey, "cdab95b082b9774bd975677c868261618c7ce7bea97d02e0f56d483e30c077b6") XCTAssertNil(t.requesterPublicKey) XCTAssertEqual(t.senderId, "U15423595369615486571") @@ -25,7 +26,10 @@ class ParsingModelsTests: XCTestCase { XCTAssertEqual(t.recipientPublicKey, "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f") XCTAssertEqual(t.amount, Decimal(0.49)) XCTAssertEqual(t.fee, Decimal(0.5)) - XCTAssertEqual(t.signature, "539f80c8a71abc8d4d31e5bd0d0ddb1ea98499c1d43fe5ab07faec8d376cd12357cf17bca36dc7a561085cbd615e64c523f2b17807d3f4da787baaa657aa450a") + XCTAssertEqual( + t.signature, + "539f80c8a71abc8d4d31e5bd0d0ddb1ea98499c1d43fe5ab07faec8d376cd12357cf17bca36dc7a561085cbd615e64c523f2b17807d3f4da787baaa657aa450a" + ) XCTAssertNil(t.signSignature) XCTAssert(t.signatures.count == 0) XCTAssertEqual(t.confirmations, 148388) @@ -35,11 +39,11 @@ class ParsingModelsTests: XCTestCase { func testTransactionChat() { let t: Transaction = TestTools.LoadJsonAndDecode(filename: "TransactionChat") - XCTAssertEqual(t.id, 16214962152767034408) + XCTAssertEqual(t.id, 16_214_962_152_767_034_408) XCTAssertEqual(t.height, 857385) XCTAssertEqual(t.blockId, "11054360802486546958") XCTAssertEqual(t.type, TransactionType.chatMessage) - XCTAssertEqual(t.timestamp, 11138999) + XCTAssertEqual(t.timestamp, 11_138_999) XCTAssertEqual(t.senderPublicKey, "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f") XCTAssertNil(t.requesterPublicKey) XCTAssertEqual(t.senderId, "U2279741505997340299") @@ -47,7 +51,10 @@ class ParsingModelsTests: XCTestCase { XCTAssertNil(t.recipientPublicKey) XCTAssertEqual(t.amount, 0) XCTAssertEqual(t.fee, Decimal(0.005)) - XCTAssertEqual(t.signature, "7c58921d29beb5fbc7886053d81b37d8495db53848ebe04a8847f06dbcb810d8d675ea6501b1fe9b5ce7fbf9d7660a09895ac915dc82e6e8878fd0e919538c0e") + XCTAssertEqual( + t.signature, + "7c58921d29beb5fbc7886053d81b37d8495db53848ebe04a8847f06dbcb810d8d675ea6501b1fe9b5ce7fbf9d7660a09895ac915dc82e6e8878fd0e919538c0e" + ) XCTAssertNil(t.signSignature) XCTAssert(t.signatures.count == 0) XCTAssertEqual(t.confirmations, 0) @@ -55,14 +62,14 @@ class ParsingModelsTests: XCTestCase { XCTAssertEqual(t.asset.chat!.ownMessage, "898e0bd7d8008fb0396195a911d19a24a7234d2e2a00cdf9") XCTAssertEqual(t.asset.chat!.type, ChatType.message) } - + func testEncodingTransactionChat() { let t: Transaction = TestTools.LoadJsonAndDecode(filename: "TransactionChat") - + let rawTransaction = try! JSONEncoder().encode(t) - + let newT = try! JSONDecoder().decode(Transaction.self, from: rawTransaction) - + XCTAssertEqual(t.id, newT.id) XCTAssertEqual(t.height, newT.height) XCTAssertEqual(t.blockId, newT.blockId) @@ -109,12 +116,12 @@ class ParsingModelsTests: XCTestCase { func testNormalizedTransaction() { let t: NormalizedTransaction = TestTools.LoadJsonAndDecode(filename: "NormalizedTransaction") - + XCTAssertEqual(t.type, TransactionType.send) XCTAssertEqual(t.amount, Decimal(505.05050505)) XCTAssertEqual(t.senderPublicKey, "8007a01493bb4b21ec67265769898eb19514d9427bd7b701f96bc9880a6e209f") XCTAssertNil(t.requesterPublicKey) - XCTAssertEqual(t.timestamp, 11236791) + XCTAssertEqual(t.timestamp, 11_236_791) XCTAssertNil(t.asset.chat) XCTAssertEqual(t.recipientId, "U2279741505997340299") } diff --git a/AdamantTests/Parsing/TransactionChat.json b/AdamantTests/DisabledTests/Parsing/TransactionChat.json similarity index 100% rename from AdamantTests/Parsing/TransactionChat.json rename to AdamantTests/DisabledTests/Parsing/TransactionChat.json diff --git a/AdamantTests/Parsing/TransactionSend.json b/AdamantTests/DisabledTests/Parsing/TransactionSend.json similarity index 100% rename from AdamantTests/Parsing/TransactionSend.json rename to AdamantTests/DisabledTests/Parsing/TransactionSend.json diff --git a/AdamantTests/PassphraseValidation.swift b/AdamantTests/DisabledTests/PassphraseValidation.swift similarity index 98% rename from AdamantTests/PassphraseValidation.swift rename to AdamantTests/DisabledTests/PassphraseValidation.swift index 8a2639c77..d612e8041 100644 --- a/AdamantTests/PassphraseValidation.swift +++ b/AdamantTests/DisabledTests/PassphraseValidation.swift @@ -7,20 +7,21 @@ // import XCTest + @testable import Adamant class PassphraseValidation: XCTestCase { - + func testValidPassphrase() { let passphrase = "bring hurry funny hamster fever observe cat property crawl mule course lizard" XCTAssertTrue(AdamantUtilities.validateAdamantPassphrase(passphrase: passphrase)) } - + func testTwelveWords() { let eleven = "one two three four five six seven eight nine ten eleven" let twelve = "bring hurry funny hamster fever observe cat property crawl mule course lizard" let thirteen = "one two three four five six seven eight nine ten eleven twelve thirteen" - + XCTAssertFalse(AdamantUtilities.validateAdamantPassphrase(passphrase: eleven)) XCTAssertTrue(AdamantUtilities.validateAdamantPassphrase(passphrase: twelve)) XCTAssertFalse(AdamantUtilities.validateAdamantPassphrase(passphrase: thirteen)) diff --git a/AdamantTests/Stubs/ApiServiceStub.swift b/AdamantTests/DisabledTests/Stubs/ApiServiceStub.swift similarity index 76% rename from AdamantTests/Stubs/ApiServiceStub.swift rename to AdamantTests/DisabledTests/Stubs/ApiServiceStub.swift index 6dbb32c78..5a3fe3e1f 100644 --- a/AdamantTests/Stubs/ApiServiceStub.swift +++ b/AdamantTests/DisabledTests/Stubs/ApiServiceStub.swift @@ -6,66 +6,94 @@ // Copyright © 2022 Adamant. All rights reserved. // -import Foundation import Alamofire +import Foundation + @testable import Adamant final class ApiServiceStub: ApiService { - func sendRequest(url: Alamofire.URLConvertible, method: Alamofire.HTTPMethod, parameters: Alamofire.Parameters?) async throws -> Output where Output : Decodable { + func sendRequest(url: Alamofire.URLConvertible, method: Alamofire.HTTPMethod, parameters: Alamofire.Parameters?) async throws -> Output + where Output: Decodable { return Output.self as! Output } - + func getAccount(byAddress address: String) async throws -> Adamant.AdamantAccount { - return Adamant.AdamantAccount(address: "", unconfirmedBalance: 1, balance: 1, publicKey: "", unconfirmedSignature: 1, secondSignature: 1, secondPublicKey: "", multisignatures: nil, uMultisignatures: nil) + return Adamant.AdamantAccount( + address: "", + unconfirmedBalance: 1, + balance: 1, + publicKey: "", + unconfirmedSignature: 1, + secondSignature: 1, + secondPublicKey: "", + multisignatures: nil, + uMultisignatures: nil + ) } - + let defaultResponseDispatchQueue: DispatchQueue = .default let lastRequestTimeDelta: TimeInterval? = nil let currentNodes: [Node] = [] - + func getNodeVersion(url: URL, completion: @escaping (ApiServiceResult) -> Void) {} - + func getNodeStatus(url: URL, completion: @escaping (ApiServiceResult) -> Void) -> DataRequest? { nil } - + func getAccount(byPassphrase passphrase: String, completion: @escaping (ApiServiceResult) -> Void) {} - + func getAccount(byPublicKey publicKey: String, completion: @escaping (ApiServiceResult) -> Void) {} - + func getAccount(byAddress address: String, completion: @escaping (ApiServiceResult) -> Void) {} - + func getPublicKey(byAddress address: String, completion: @escaping (ApiServiceResult) -> Void) {} - + func getTransaction(id: UInt64, completion: @escaping (ApiServiceResult) -> Void) {} - - func getTransactions(forAccount: String, type: TransactionType, fromHeight: Int64?, offset: Int?, limit: Int?, completion: @escaping (ApiServiceResult<[Transaction]>) -> Void) {} - + + func getTransactions( + forAccount: String, + type: TransactionType, + fromHeight: Int64?, + offset: Int?, + limit: Int?, + completion: @escaping (ApiServiceResult<[Transaction]>) -> Void + ) {} + func getChatRooms(address: String, offset: Int?, completion: @escaping (ApiServiceResult) -> Void) {} - + func getChatMessages(address: String, addressRecipient: String, offset: Int?, completion: @escaping (ApiServiceResult) -> Void) {} - + func transferFunds(sender: String, recipient: String, amount: Decimal, keypair: Keypair, completion: @escaping (ApiServiceResult) -> Void) {} - + func store(key: String, value: String, type: StateType, sender: String, keypair: Keypair, completion: @escaping (ApiServiceResult) -> Void) {} - + func get(key: String, sender: String, completion: @escaping (ApiServiceResult) -> Void) {} - + func getMessageTransactions(address: String, height: Int64?, offset: Int?, completion: @escaping (ApiServiceResult<[Transaction]>) -> Void) {} - + func getMessageTransactions(address: String, height: Int64?, offset: Int?) async throws -> [Transaction] { return [] } - - func sendMessage(senderId: String, recipientId: String, keypair: Keypair, message: String, type: ChatType, nonce: String, amount: Decimal?, completion: @escaping (ApiServiceResult) -> Void) -> UnregisteredTransaction? { nil } - + + func sendMessage( + senderId: String, + recipientId: String, + keypair: Keypair, + message: String, + type: ChatType, + nonce: String, + amount: Decimal?, + completion: @escaping (ApiServiceResult) -> Void + ) -> UnregisteredTransaction? { nil } + func sendTransaction(path: String, transaction: UnregisteredTransaction, completion: @escaping (ApiServiceResult) -> Void) {} - + func getDelegates(limit: Int, completion: @escaping (ApiServiceResult<[Delegate]>) -> Void) {} - + func getDelegatesWithVotes(for address: String, limit: Int, completion: @escaping (ApiServiceResult<[Delegate]>) -> Void) {} - + func getForgedByAccount(publicKey: String, completion: @escaping (ApiServiceResult) -> Void) {} - + func getForgingTime(for delegate: Delegate, completion: @escaping (ApiServiceResult) -> Void) {} - + func voteForDelegates(from address: String, keypair: Keypair, votes: [DelegateVote], completion: @escaping (ApiServiceResult) -> Void) {} } diff --git a/AdamantTests/TestTools.swift b/AdamantTests/DisabledTests/TestTools.swift similarity index 97% rename from AdamantTests/TestTools.swift rename to AdamantTests/DisabledTests/TestTools.swift index e44aecc95..3f96ceaad 100644 --- a/AdamantTests/TestTools.swift +++ b/AdamantTests/DisabledTests/TestTools.swift @@ -11,14 +11,14 @@ import Foundation class TestTools { static func LoadJsonAndDecode(filename: String) -> T { let rawJson = TestTools.LoadJson(named: filename) - + return try! JSONDecoder().decode(T.self, from: rawJson) } - + static func LoadJson(named filename: String) -> Data { return TestTools.LoadResource(filename: filename, withExtension: "json") } - + static func LoadResource(filename: String, withExtension ext: String) -> Data { let url = Bundle(for: self).url(forResource: filename, withExtension: ext) return try! Data(contentsOf: url!) diff --git a/AdamantTests/Extensions/Actor+Extensions.swift b/AdamantTests/Extensions/Actor+Extensions.swift new file mode 100644 index 000000000..076eb923b --- /dev/null +++ b/AdamantTests/Extensions/Actor+Extensions.swift @@ -0,0 +1,13 @@ +// +// Actor+Extensions.swift +// Adamant +// +// Created by Christian Benua on 10.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +extension Actor { + func isolated(_ closure: (isolated Self) -> T) -> T { + return closure(self) + } +} diff --git a/AdamantTests/Extensions/BitcoinKitTransaction+Equatable.swift b/AdamantTests/Extensions/BitcoinKitTransaction+Equatable.swift new file mode 100644 index 000000000..243355bec --- /dev/null +++ b/AdamantTests/Extensions/BitcoinKitTransaction+Equatable.swift @@ -0,0 +1,33 @@ +// +// BitcoinKitTransaction+Equatable.swift +// Adamant +// +// Created by Christian Benua on 10.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import BitcoinKit + +extension BitcoinKit.Transaction: Equatable { + public static func == (lhs: BitcoinKit.Transaction, rhs: BitcoinKit.Transaction) -> Bool { + lhs.version == rhs.version && lhs.inputs == rhs.inputs && lhs.outputs == rhs.outputs && lhs.lockTime == rhs.lockTime + } +} + +extension BitcoinKit.TransactionInput: Equatable { + public static func == (lhs: BitcoinKit.TransactionInput, rhs: BitcoinKit.TransactionInput) -> Bool { + lhs.previousOutput == rhs.previousOutput && lhs.signatureScript == rhs.signatureScript && lhs.sequence == rhs.sequence + } +} + +extension BitcoinKit.TransactionOutPoint: Equatable { + public static func == (lhs: BitcoinKit.TransactionOutPoint, rhs: BitcoinKit.TransactionOutPoint) -> Bool { + lhs.hash == rhs.hash && lhs.index == rhs.index + } +} + +extension BitcoinKit.TransactionOutput: Equatable { + public static func == (lhs: BitcoinKit.TransactionOutput, rhs: BitcoinKit.TransactionOutput) -> Bool { + lhs.value == rhs.value && lhs.lockingScript == rhs.lockingScript + } +} diff --git a/AdamantTests/Extensions/Data+Extensions.swift b/AdamantTests/Extensions/Data+Extensions.swift new file mode 100644 index 000000000..05f55f59f --- /dev/null +++ b/AdamantTests/Extensions/Data+Extensions.swift @@ -0,0 +1,19 @@ +// +// Data+Extensions.swift +// Adamant +// +// Created by Christian Benua on 03.02.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import Foundation + +private final class BundleTag {} + +extension Data { + static func readResource(name: String, withExtension extensionName: String?) -> Data? { + Bundle(for: BundleTag.self) + .url(forResource: name, withExtension: extensionName) + .flatMap { try? Data(contentsOf: $0) } + } +} diff --git a/AdamantTests/Extensions/MockURLProtocol.swift b/AdamantTests/Extensions/MockURLProtocol.swift new file mode 100644 index 000000000..8821d797b --- /dev/null +++ b/AdamantTests/Extensions/MockURLProtocol.swift @@ -0,0 +1,76 @@ +// +// URLProtocolMock.swift +// Adamant +// +// Created by Christian Benua on 15.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import Foundation +import XCTest + +final class MockURLProtocol: URLProtocol { + typealias Handler = (URLRequest) throws -> (HTTPURLResponse, Data)? + + override class func canInit(with request: URLRequest) -> Bool { + true + } + + override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + static var requestHandler: Handler? + + override func startLoading() { + guard let handler = MockURLProtocol.requestHandler else { + client?.urlProtocolDidFinishLoading(self) + XCTFail("No request handler provided") + return + } + + do { + guard let (response, data) = try handler(request) else { + client?.urlProtocolDidFinishLoading(self) + XCTFail("No request handler provided") + return + } + + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + client?.urlProtocol(self, didLoad: data) + client?.urlProtocolDidFinishLoading(self) + } catch { + XCTFail("Error handling request, url: \(String(describing: request.url)): error \(error)") + } + } + + override func stopLoading() {} +} + +extension Data { + init(reading input: InputStream) { + self.init() + input.open() + + let bufferSize = 1024 + let buffer = UnsafeMutablePointer.allocate(capacity: bufferSize) + while input.hasBytesAvailable { + let read = input.read(buffer, maxLength: bufferSize) + if read == 0 { + break // added + } + self.append(buffer, count: read) + } + buffer.deallocate() + + input.close() + } +} + +extension MockURLProtocol { + static func combineHandlers(_ prevHandler: Handler?, _ new: @escaping Handler) -> Handler { + { request in + return try new(request) ?? prevHandler?(request) + } + } +} diff --git a/AdamantTests/Extensions/NodeOrigin+Extensions.swift b/AdamantTests/Extensions/NodeOrigin+Extensions.swift new file mode 100644 index 000000000..8d0aa982b --- /dev/null +++ b/AdamantTests/Extensions/NodeOrigin+Extensions.swift @@ -0,0 +1,14 @@ +// +// NodeOrigin+Extensions.swift +// Adamant +// +// Created by Christian Benua on 23.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import CommonKit +import Foundation + +extension NodeOrigin { + static let mock = NodeOrigin(url: URL(string: "http://samplenodeorigin.com")!) +} diff --git a/AdamantTests/Extensions/Result+Extensions.swift b/AdamantTests/Extensions/Result+Extensions.swift new file mode 100644 index 000000000..129af7f46 --- /dev/null +++ b/AdamantTests/Extensions/Result+Extensions.swift @@ -0,0 +1,38 @@ +// +// Result+Extensions.swift +// Adamant +// +// Created by Christian Benua on 10.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +extension Result { + var error: Failure? { + switch self { + case let .failure(error): + return error + case .success: + return nil + } + } + + var value: Success? { + switch self { + case .failure: + return nil + case let .success(value): + return value + } + } +} + +extension Result where Failure == Error { + init(catchingAsync run: @escaping () async throws -> Success) async { + do { + let value = try await run() + self = .success(value) + } catch { + self = .failure(error) + } + } +} diff --git a/AdamantTests/Extensions/URLSessionSwizzlingMock.swift b/AdamantTests/Extensions/URLSessionSwizzlingMock.swift new file mode 100644 index 000000000..d9f8f0ad4 --- /dev/null +++ b/AdamantTests/Extensions/URLSessionSwizzlingMock.swift @@ -0,0 +1,33 @@ +// +// URLSessionSwizzlingMock.swift +// Adamant +// +// Created by Christian Benua on 22.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import Foundation + +final class URLSessionSwizzlingHolder { + static var _stubbedUrlSessionConfiguration: URLSessionConfiguration? +} + +extension URLSession { + + // Perform the swizzling + static func swizzleInitializer() { + let originalSelector = #selector(URLSession.init(configuration:delegate:delegateQueue:)) + let swizzledSelector = #selector(URLSession.swizzledInit) + + guard let originalMethod = class_getClassMethod(URLSession.self, originalSelector), + let swizzledMethod = class_getClassMethod(URLSession.self, swizzledSelector) + else { return } + + method_exchangeImplementations(originalMethod, swizzledMethod) + } + + // Swizzled init with custom configuration + @objc class func swizzledInit(configuration: URLSessionConfiguration, delegate: URLSessionDelegate?, delegateQueue queue: OperationQueue?) -> URLSession { + swizzledInit(configuration: URLSessionSwizzlingHolder._stubbedUrlSessionConfiguration ?? configuration, delegate: delegate, delegateQueue: queue) + } +} diff --git a/AdamantTests/Extensions/UnspentTransaction+Equatable.swift b/AdamantTests/Extensions/UnspentTransaction+Equatable.swift new file mode 100644 index 000000000..bd7910992 --- /dev/null +++ b/AdamantTests/Extensions/UnspentTransaction+Equatable.swift @@ -0,0 +1,15 @@ +// +// UnspentTransaction+Equatable.swift +// Adamant +// +// Created by Christian Benua on 11.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import BitcoinKit + +extension UnspentTransaction: Equatable { + public static func == (lhs: UnspentTransaction, rhs: UnspentTransaction) -> Bool { + lhs.output == rhs.output && lhs.outpoint == rhs.outpoint + } +} diff --git a/AdamantTests/Extensions/WalletServiceError+Equatable.swift b/AdamantTests/Extensions/WalletServiceError+Equatable.swift new file mode 100644 index 000000000..cc86f17f0 --- /dev/null +++ b/AdamantTests/Extensions/WalletServiceError+Equatable.swift @@ -0,0 +1,34 @@ +// +// WalletServiceError+Equatable.swift +// Adamant +// +// Created by Christian Benua on 10.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +@testable import Adamant + +extension WalletServiceError: Equatable { + public static func == (lhs: Adamant.WalletServiceError, rhs: Adamant.WalletServiceError) -> Bool { + switch (lhs, rhs) { + case (.notLogged, .notLogged), (.notEnoughMoney, .notEnoughMoney), (.networkError, .networkError), + (.accountNotFound, .accountNotFound), (.walletNotInitiated, .walletNotInitiated), + (.requestCancelled, .requestCancelled), (.dustAmountError, .dustAmountError): + return true + case let (.invalidAmount(lhsValue), invalidAmount(rhsValue)): + return lhsValue == rhsValue + case let (.transactionNotFound(lhsValue), transactionNotFound(rhsValue)): + return lhsValue == rhsValue + case (.apiError, .apiError): + return true + case let (.remoteServiceError(lhsValue, _), .remoteServiceError(rhsValue, _)): + return lhsValue == rhsValue + case let (.internalError(lhsValue, _), .internalError(rhsValue, _)): + return lhsValue == rhsValue + case (.notLogged, _), (.notEnoughMoney, _), (.networkError, _), (.accountNotFound, _), (.walletNotInitiated, _), + (.requestCancelled, _), (.dustAmountError, _), (.invalidAmount, _), (.transactionNotFound, _), + (.apiError, _), (.remoteServiceError, _), (.internalError, _): + return false + } + } +} diff --git a/AdamantTests/FeeTests.swift b/AdamantTests/FeeTests.swift deleted file mode 100644 index 6f26bc352..000000000 --- a/AdamantTests/FeeTests.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// FeeTests.swift -// AdamantTests -// -// Created by Anokhov Pavel on 16.01.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import XCTest -@testable import Adamant - -class FeeTests: XCTestCase { - func testTransferFee() { - let estimatedFee = Decimal(0.5) - XCTAssertEqual(estimatedFee, AdamantTransfersProvider.transferFee) - } - - func testShortMessageFee() { - let message = "A quick brown fox bought bitcoins in 2009. Good for you, mr fox. You quick brown mother fucker." - let estimatedFee = Decimal(0.001) - - XCTAssertEqual(estimatedFee, AdamantMessage.text(message).fee) - } - - func testLongMessageFee() { - let message = """ -The sperm whale's cerebrum is the largest in all mammalia, both in absolute and relative terms. -The olfactory system is reduced, suggesting that the sperm whale has a poor sense of taste and smell. -By contrast, the auditory system is enlarged. -The pyramidal tract is poorly developed, reflecting the reduction of its limbs. -""" - let estimatedFee = Decimal(0.002) - - XCTAssertEqual(estimatedFee, AdamantMessage.text(message).fee) - } - - func testVeryLongMessageFee() { - let message = """ -Lift you up again -Give you to the trees -All sound and visions are -What they ask of me - -Let's run fast through the fields -Over mountaintops -Let's swim through ocean water -And we'll never stop - -Just close your eyes -And pretend that everything's fine -Just close your eyes -I'll tell you when - -Can you show me -Where to find the stream? -I've been told before -That the water's clean - -Will you come with me? -Two of us can drink -Move quick, we've got to hurry -There's no time to think - -Just close your eyes -And pretend that everything's fine -Just close your eyes -I'll tell you when - -We didn't come this far -Just to turn around -We didn't come this far -Just to run away -Just ahead -We will hear the sound -The sound that gives us -A brand new day - -Just close your eyes -And pretend that everything's fine -Just close your eyes -I'll tell you when - -Mastodon / The Hunter / All The Heavy Lifting -Brann Timothy Dailor / Troy Jayson Sanders / William Breen Kelliher / William Brent Hinds -""" - let estimatedFee = Decimal(0.004) - - XCTAssertEqual(estimatedFee, AdamantMessage.text(message).fee) - } -} diff --git a/AdamantTests/Modules/Wallets/AdmWalletServiceTests.swift b/AdamantTests/Modules/Wallets/AdmWalletServiceTests.swift new file mode 100644 index 000000000..750f7c500 --- /dev/null +++ b/AdamantTests/Modules/Wallets/AdmWalletServiceTests.swift @@ -0,0 +1,650 @@ +// +// AdmWalletServiceTests.swift +// Adamant +// +// Created by Christian Benua on 28.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import CommonKit +import XCTest + +@testable import Adamant + +final class AdmWalletServiceTests: XCTestCase { + + var sut: AdmWalletService! + var transfersProvider: AdamantTransfersProvider! + var accountServiceMock: AccountServiceMock! + var accountsProviderMock: AccountsProviderMock! + var admApiServiceMock: AdamantApiServiceProtocolMock! + var chatProviderMock: ChatsProviderMock! + var stack: InMemoryCoreDataStack! + var adamantCoreMock: AdamantCoreMock! + + override func setUp() async throws { + try await super.setUp() + + accountServiceMock = AccountServiceMock() + accountsProviderMock = await AccountsProviderMock() + admApiServiceMock = AdamantApiServiceProtocolMock() + chatProviderMock = ChatsProviderMock() + adamantCoreMock = AdamantCoreMock() + stack = try InMemoryCoreDataStack(modelUrl: AdamantResources.coreDataModel) + transfersProvider = AdamantTransfersProvider( + apiService: admApiServiceMock, + stack: stack, + adamantCore: adamantCoreMock, + accountService: accountServiceMock, + accountsProvider: accountsProviderMock, + SecureStore: SecureStoreMock(), + transactionService: ChatTransactionServiceMock(), + chatsProvider: chatProviderMock + ) + sut = AdmWalletService() + sut.transfersProvider = transfersProvider + } + + override func tearDown() async throws { + sut = nil + transfersProvider = nil + accountServiceMock = nil + accountsProviderMock = nil + admApiServiceMock = nil + chatProviderMock = nil + stack = nil + adamantCoreMock = nil + + try await super.tearDown() + } + + func test_sendMoney_isNotLoggedInThrowsError() async throws { + // GIVEN + accountServiceMock.given(.account(getter: nil)) + + // WHEN + let result = await Result { + try await self.sut.sendMoney( + recipient: Constants.recipientAddress, + amount: Constants.sendAmount, + comments: Constants.comment, + replyToMessageId: nil + ) + } + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .notLogged) + } + + func test_sendMoney_noKeyPairThrowsError() async throws { + // GIVEN + accountServiceMock.given(.account(getter: makeAccount())) + accountServiceMock.given(.keypair(getter: nil)) + + // WHEN + let result = await Result { + try await self.sut.sendMoney( + recipient: Constants.recipientAddress, + amount: Constants.sendAmount, + comments: Constants.comment, + replyToMessageId: nil + ) + } + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .notLogged) + } + + func test_sendMoney_notEnoughMoneyThrowsError() async throws { + // GIVEN + setupAccountService() + + // WHEN + let result = await Result { + try await self.sut.sendMoney( + recipient: Constants.recipientAddress, + amount: 20, + comments: Constants.comment, + replyToMessageId: nil + ) + } + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .notEnoughMoney) + } + + func test_sendMoney_invalidRecipientThrowsError() async throws { + // GIVEN + setupAccountService() + await MainActor.run { + accountsProviderMock.given(.getAccount(byAddress: .any, willThrow: AccountsProviderError.notFound(address: ""))) + } + + // WHEN + let result = await Result { + try await self.sut.sendMoney( + recipient: Constants.recipientAddress, + amount: Constants.sendAmount, + comments: Constants.comment, + replyToMessageId: nil + ) + } + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .accountNotFound) + } + + func test_sendMoney_invalidRecipientPublicKeyThrowsError() async throws { + // GIVEN + setupAccountService() + await MainActor.run { + accountsProviderMock.given(.getAccount(byAddress: .any, willReturn: createCoreDataAccount())) + } + + // WHEN + let result = await Result { + try await self.sut.sendMoney( + recipient: Constants.recipientAddress, + amount: Constants.sendAmount, + comments: Constants.comment, + replyToMessageId: nil + ) + } + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .accountNotFound) + } + + func test_sendMoney_emptyRecipientChatroomThrowsError() async throws { + // GIVEN + setupAccountService() + await MainActor.run { + accountsProviderMock.given(.getAccount(byAddress: .any, willReturn: createCoreDataAccount(publicKey: "public key"))) + } + + // WHEN + let result = await Result { + try await self.sut.sendMoney( + recipient: Constants.recipientAddress, + amount: Constants.sendAmount, + comments: Constants.comment, + replyToMessageId: nil + ) + } + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .accountNotFound) + } + + func test_sendMoney_correctRecipientBadMessageEncodeThrowsError() async throws { + // GIVEN + setupAccountService() + let (room, account) = setupCoreDataEntities(accountPublicKey: Constants.recipientPublicKeyAddress) + await MainActor.run { + accountsProviderMock.given(.getAccount(byAddress: .any, willReturn: account)) + } + adamantCoreMock.given(.encodeMessage(.any, recipientPublicKey: .any, privateKey: .any, willReturn: nil)) + + // WHEN + let result = await Result { + try await self.sut.sendMoney( + recipient: Constants.recipientAddress, + amount: Constants.sendAmount, + comments: Constants.comment, + replyToMessageId: nil + ) + } + + // THEN + let privateKey = try XCTUnwrap(accountServiceMock.keypair) + adamantCoreMock.verify( + .encodeMessage( + .value(Constants.comment), + recipientPublicKey: .value(Constants.recipientPublicKeyAddress), + privateKey: .value(privateKey.privateKey) + ), + count: 1 + ) + + switch result.error as? WalletServiceError { + case .internalError: + break + default: + XCTFail("Expected '.internalError', but got \(String(describing: result.error))") + } + } + + func test_sendMoney_signTransactionFailureThrowsError() async throws { + // GIVEN + setupAccountService() + let (room, account) = setupCoreDataEntities(accountPublicKey: Constants.recipientPublicKeyAddress) + await MainActor.run { + accountsProviderMock.given(.getAccount(byAddress: .any, willReturn: account)) + } + adamantCoreMock.given(.encodeMessage(.any, recipientPublicKey: .any, privateKey: .any, willReturn: ("message", "nonce"))) + adamantCoreMock.given(.sign(transaction: .any, senderId: .any, keypair: .any, willReturn: nil)) + + // WHEN + let result = await Result { + try await self.sut.sendMoney( + recipient: Constants.recipientAddress, + amount: Constants.sendAmount, + comments: Constants.comment, + replyToMessageId: nil + ) + } + + // THEN + let keyPair = try XCTUnwrap(accountServiceMock.keypair) + adamantCoreMock.verify( + .sign( + transaction: .matching { + $0.type == .chatMessage + && $0.amount == Constants.sendAmount + && $0.recipientId == Constants.recipientAddress + }, + senderId: .value(Constants.accountAddress), + keypair: .value(keyPair) + ), + count: 1 + ) + + switch result.error as? WalletServiceError { + case .internalError: + break + default: + XCTFail("Expected '.internalError', but got \(String(describing: result.error))") + } + } + + func test_sendMoney_sendTransactionFailureThrowsError() async throws { + // GIVEN + setupAccountService() + let (room, account) = setupCoreDataEntities(accountPublicKey: Constants.recipientPublicKeyAddress) + await MainActor.run { + accountsProviderMock.given(.getAccount(byAddress: .any, willReturn: account)) + } + adamantCoreMock.given(.sign(transaction: .any, senderId: .any, keypair: .any, willReturn: "signature")) + adamantCoreMock.given(.encodeMessage(.any, recipientPublicKey: .any, privateKey: .any, willReturn: ("message", "nonce"))) + admApiServiceMock.given(.sendMessageTransaction(transaction: .any, willReturn: .failure(.accountNotFound))) + + // WHEN + let result = await Result { + try await self.sut.sendMoney( + recipient: Constants.recipientAddress, + amount: Constants.sendAmount, + comments: Constants.comment, + replyToMessageId: nil + ) + } + + // THEN + + admApiServiceMock.verify( + .sendMessageTransaction( + transaction: .matching { + $0.type == .chatMessage + && $0.senderPublicKey == "8eefafa8d2f6a51bde207bcdc9029f3725f5d6aaa8f9b8fe3cd6d65d1f315a54" + && $0.senderId == Constants.accountAddress + && $0.recipientId == Constants.recipientAddress + && $0.amount == Constants.sendAmount + && $0.signature == "signature" + } + ), + count: 1 + ) + + let transactions: [TransferTransaction] = try stack.container.viewContext.fetch(TransferTransaction.fetchRequest()) + XCTAssertEqual(transactions.count, 1) + XCTAssertEqual(transactions.first?.statusEnum, .failed) + + switch result.error as? WalletServiceError { + case .remoteServiceError: + break + default: + XCTFail("Expected '.remoteServiceError', but got \(String(describing: result.error))") + } + } + + func test_sendMoney_sendTransactionSuccess() async throws { + // GIVEN + setupAccountService() + let (room, account) = setupCoreDataEntities(accountPublicKey: Constants.recipientPublicKeyAddress) + await MainActor.run { + accountsProviderMock.given(.getAccount(byAddress: .any, willReturn: account)) + } + adamantCoreMock.given(.sign(transaction: .any, senderId: .any, keypair: .any, willReturn: "signature")) + adamantCoreMock.given(.encodeMessage(.any, recipientPublicKey: .any, privateKey: .any, willReturn: ("message", "nonce"))) + admApiServiceMock.given(.sendMessageTransaction(transaction: .any, willReturn: .success(1234))) + + // WHEN + let result = await Result { + try await self.sut.sendMoney( + recipient: Constants.recipientAddress, + amount: Constants.sendAmount, + comments: Constants.comment, + replyToMessageId: nil + ) + } + + // THEN + let transaction = try XCTUnwrap(result.value as? TransferTransaction) + XCTAssertEqual(transaction.transactionId, "1234") + XCTAssertEqual(transaction.statusEnum, .pending) + XCTAssertEqual(transaction.chatRoom?.objectID, room.objectID) + } + + // AdmWalletService have different logic for just sending money and sending money with comments + + func test_sendJustMoney_isNotLoggedInThrowsError() async throws { + // GIVEN + accountServiceMock.given(.account(getter: makeAccount())) + + // WHEN + let result = await Result { + try await self.sut.sendMoney( + recipient: Constants.recipientAddress, + amount: Constants.sendAmount, + comments: "", + replyToMessageId: nil + ) + } + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .notLogged) + } + + func test_sendJustMoney_noKeyPairThrowsError() async throws { + // GIVEN + accountServiceMock.given(.account(getter: makeAccount())) + accountServiceMock.given(.keypair(getter: nil)) + + // WHEN + let result = await Result { + try await self.sut.sendMoney( + recipient: Constants.recipientAddress, + amount: Constants.sendAmount, + comments: "", + replyToMessageId: nil + ) + } + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .notLogged) + } + + func test_sendJustMoney_notEnoughMoneyThrowsError() async throws { + // GIVEN + setupAccountService() + + // WHEN + let result = await Result { + try await self.sut.sendMoney( + recipient: Constants.recipientAddress, + amount: 20, + comments: "", + replyToMessageId: nil + ) + } + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .notEnoughMoney) + } + + func test_sendJustMoney_invalidRecipientThrowsError() async throws { + // GIVEN + setupAccountService() + await MainActor.run { + accountsProviderMock.given(.getAccount(byAddress: .any, willThrow: AccountsProviderError.invalidAddress(address: ""))) + } + + // WHEN + let result = await Result { + try await self.sut.sendMoney( + recipient: Constants.recipientAddress, + amount: Constants.sendAmount, + comments: "", + replyToMessageId: nil + ) + } + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .accountNotFound) + } + + func test_sendJustMoney_invalidRecipientQueriesDummyAndThrowsErrorWhenFails() async throws { + // GIVEN + setupAccountService() + await MainActor.run { + accountsProviderMock.given(.getAccount(byAddress: .any, willThrow: AccountsProviderError.notFound(address: ""))) + accountsProviderMock.given(.getDummyAccount(for: .any, willThrow: AccountsProviderDummyAccountError.invalidAddress(address: ""))) + } + + // WHEN + let result = await Result { + try await self.sut.sendMoney( + recipient: Constants.recipientAddress, + amount: Constants.sendAmount, + comments: "", + replyToMessageId: nil + ) + } + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .accountNotFound) + await MainActor.run { + accountsProviderMock.verify(.getAccount(byAddress: .any), count: 1) + accountsProviderMock.verify(.getDummyAccount(for: .any), count: 1) + } + } + + func test_sendJustMoney_correctRecipientBadMessageEncodeThrowsError() async throws { + // GIVEN + setupAccountService() + let (room, account) = setupCoreDataEntities(accountPublicKey: Constants.recipientPublicKeyAddress) + await MainActor.run { + accountsProviderMock.given(.getAccount(byAddress: .any, willReturn: account)) + } + adamantCoreMock.given(.encodeMessage(.any, recipientPublicKey: .any, privateKey: .any, willReturn: nil)) + + // WHEN + let result = await Result { + try await self.sut.sendMoney( + recipient: Constants.recipientAddress, + amount: Constants.sendAmount, + comments: "", + replyToMessageId: nil + ) + } + + // THEN + adamantCoreMock.verify(.encodeMessage(.any, recipientPublicKey: .any, privateKey: .any), count: 0) + switch result.error as? WalletServiceError { + case .internalError: + break + default: + XCTFail("Expected '.internalError', but got \(String(describing: result.error))") + } + } + + func test_sendJustMoney_signTransactionFailureThrowsError() async throws { + // GIVEN + setupAccountService() + let (room, account) = setupCoreDataEntities(accountPublicKey: Constants.recipientPublicKeyAddress) + await MainActor.run { + accountsProviderMock.given(.getAccount(byAddress: .any, willReturn: account)) + } + adamantCoreMock.given(.sign(transaction: .any, senderId: .any, keypair: .any, willReturn: nil)) + adamantCoreMock.given(.encodeMessage(.any, recipientPublicKey: .any, privateKey: .any, willReturn: ("message", "nonce"))) + + // WHEN + let result = await Result { + try await self.sut.sendMoney( + recipient: Constants.recipientAddress, + amount: Constants.sendAmount, + comments: "", + replyToMessageId: nil + ) + } + + // THEN + let keyPair = try XCTUnwrap(accountServiceMock.keypair) + adamantCoreMock.verify( + .sign( + transaction: .matching { + $0.type == .send + && $0.amount == Constants.sendAmount + && $0.recipientId == Constants.recipientAddress + }, + senderId: .value(Constants.accountAddress), + keypair: .value(keyPair) + ), + count: 1 + ) + + switch result.error as? WalletServiceError { + case .internalError: + break + default: + XCTFail("Expected '.internalError', but got \(String(describing: result.error))") + } + } + + func test_sendJustMoney_sendTransactionFailureThrowsError() async throws { + // GIVEN + setupAccountService() + let (room, account) = setupCoreDataEntities(accountPublicKey: Constants.recipientPublicKeyAddress) + await MainActor.run { + accountsProviderMock.given(.getAccount(byAddress: .any, willReturn: account)) + } + adamantCoreMock.given(.sign(transaction: .any, senderId: .any, keypair: .any, willReturn: "signature")) + adamantCoreMock.given(.encodeMessage(.any, recipientPublicKey: .any, privateKey: .any, willReturn: ("message", "nonce"))) + admApiServiceMock.given(.sendMessageTransaction(transaction: .any, willReturn: .failure(.accountNotFound))) + + // WHEN + let result = await Result { + try await self.sut.sendMoney( + recipient: Constants.recipientAddress, + amount: Constants.sendAmount, + comments: Constants.comment, + replyToMessageId: nil + ) + } + + // THEN + admApiServiceMock.verify( + .sendMessageTransaction( + transaction: .matching { + $0.type == .chatMessage + && $0.senderPublicKey == "8eefafa8d2f6a51bde207bcdc9029f3725f5d6aaa8f9b8fe3cd6d65d1f315a54" + && $0.senderId == Constants.accountAddress + && $0.recipientId == Constants.recipientAddress + && $0.amount == Constants.sendAmount + && $0.signature == "signature" + } + ), + count: 1 + ) + + let transactions: [TransferTransaction] = try stack.container.viewContext.fetch(TransferTransaction.fetchRequest()) + XCTAssertEqual(transactions.count, 1) + XCTAssertEqual(transactions.first?.statusEnum, .failed) + + switch result.error as? WalletServiceError { + case .remoteServiceError: + break + default: + XCTFail("Expected '.remoteServiceError', but got \(String(describing: result.error))") + } + } + + func test_sendJustMoney_sendTransactionSuccess() async throws { + // GIVEN + setupAccountService() + let (room, account) = setupCoreDataEntities(accountPublicKey: Constants.recipientPublicKeyAddress) + await MainActor.run { + accountsProviderMock.given(.getAccount(byAddress: .any, willReturn: account)) + } + adamantCoreMock.given(.sign(transaction: .any, senderId: .any, keypair: .any, willReturn: "signature")) + adamantCoreMock.given(.encodeMessage(.any, recipientPublicKey: .any, privateKey: .any, willReturn: ("message", "nonce"))) + admApiServiceMock.given(.sendMessageTransaction(transaction: .any, willReturn: .success(1234))) + + // WHEN + let result = await Result { + try await self.sut.sendMoney( + recipient: Constants.recipientAddress, + amount: Constants.sendAmount, + comments: Constants.comment, + replyToMessageId: nil + ) + } + + // THEN + let transaction = try XCTUnwrap(result.value as? TransferTransaction) + XCTAssertEqual(transaction.transactionId, "1234") + XCTAssertEqual(transaction.statusEnum, .pending) + XCTAssertEqual(transaction.chatRoom?.objectID, room.objectID) + } +} + +extension AdmWalletServiceTests { + fileprivate func makeAccount() -> AdamantAccount { + return AdamantAccount( + address: Constants.accountAddress, + unconfirmedBalance: Constants.unconfirmedBalance, + balance: Constants.balance, + publicKey: nil, + unconfirmedSignature: 0, + secondSignature: 0, + secondPublicKey: nil, + multisignatures: nil, + uMultisignatures: nil, + isDummy: false + ) + } + + fileprivate func setupAccountService() { + accountServiceMock.given(.account(getter: makeAccount())) + accountServiceMock.given(.keypair(getter: makeKeypair(passphrase: Constants.passphrase))) + } + + fileprivate func setupCoreDataEntities(accountPublicKey: String? = nil) -> (Chatroom, CoreDataAccount) { + let account = createCoreDataAccount(publicKey: accountPublicKey) + let room = createChatroom() + account.chatroom = room + + return (room, account) + } + + fileprivate func createCoreDataAccount(publicKey: String? = nil) -> CoreDataAccount { + let account = CoreDataAccount(context: stack.container.viewContext) + + account.address = Constants.recipientAddress + account.publicKey = publicKey + + return account + } + + fileprivate func createChatroom() -> Chatroom { + let room = Chatroom(context: stack.container.viewContext) + + return room + } + + fileprivate func makeKeypair(passphrase: String) -> Keypair? { + NativeAdamantCore().createKeypairFor(passphrase: passphrase, password: "") + } +} + +private enum Constants { + static let passphrase = "village lunch say patrol glow first hurt shiver name method dolphin dead" + + static let accountAddress = "adamant address" + static let recipientAddress = "recipient address" + static let recipientPublicKeyAddress = "public key" + static let unconfirmedBalance: Decimal = 10 + static let balance: Decimal = 8 + static let sendAmount: Decimal = 5 + + static let comment = "comment" +} diff --git a/AdamantTests/Modules/Wallets/BtcWalletServiceIntegrationTests.swift b/AdamantTests/Modules/Wallets/BtcWalletServiceIntegrationTests.swift new file mode 100644 index 000000000..62880a259 --- /dev/null +++ b/AdamantTests/Modules/Wallets/BtcWalletServiceIntegrationTests.swift @@ -0,0 +1,162 @@ +// +// BtcWalletServiceIntegrationTests.swift +// Adamant +// +// Created by Christian Benua on 13.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import BitcoinKit +import CommonKit +import Swinject +import XCTest + +@testable import Adamant + +final class BtcWalletServiceIntegrationTests: XCTestCase { + + private var apiCoreMock: APICoreProtocolMock! + private var btcApiServiceProtocolMock: BtcApiServiceProtocolMock! + private var sut: BtcWalletService! + + override func setUp() { + super.setUp() + apiCoreMock = APICoreProtocolMock() + btcApiServiceProtocolMock = BtcApiServiceProtocolMock() + btcApiServiceProtocolMock.api = BtcApiCore(apiCore: apiCoreMock) + + sut = BtcWalletService() + sut.addressConverter = AddressConverterFactory().make(network: .mainnetBTC) + sut.btcApiService = btcApiServiceProtocolMock + sut.btcTransactionFactory = BitcoinKitTransactionFactory() + } + + override func tearDown() { + apiCoreMock = nil + btcApiServiceProtocolMock = nil + sut = nil + super.tearDown() + } + + func test_createAndSendTransaction_createsValidTxIdAndHash() async throws { + // GIVEN + sut.setWalletForTests(try makeWallet()) + let data = Constants.unspentTranscationsData + await apiCoreMock.isolated { mock in + mock.given( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + parameters: .any([String: String].self), + encoding: .any, + timeout: .any, + downloadProgress: .any, + willReturn: APIResponseModel( + result: .success(data), + data: data, + code: 200 + ) + ) + ) + } + + // WHEN 1 + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: "1K4hFg49PaEt5pHCym7yb5B446Vb3roSMp", + amount: 0.00009, + fee: 0.00002159542, + comment: nil + ) + }) + + // THEN 1 + let transaction = try XCTUnwrap(result.value) + XCTAssertEqual(transaction.serialized().hex, Constants.expectedTransactionHex) + XCTAssertEqual(transaction.txID, Constants.expectedTransactionID) + + // GIVEN 2 + let txData = try XCTUnwrap(transaction.txID.data(using: .utf8)) + await apiCoreMock.isolated { mock in + mock.given( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + parameters: .any([String: String].self), + encoding: .any, + timeout: .any, + downloadProgress: .any, + willReturn: APIResponseModel( + result: .success(txData), + data: txData, + code: 200 + ) + ) + ) + } + + // WHEN 2 + let result2 = await Result { + try await self.sut.sendTransaction(transaction) + } + // THEN 3 + XCTAssertNil(result2.error) + await apiCoreMock.isolated { mock in + mock.verify( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + parameters: .any([String: String].self), + encoding: .any, + timeout: .any, + downloadProgress: .any + ), + count: 2 + ) + } + } + + private func makeWallet() throws -> BtcWallet { + let privateKeyData = Constants.passphrase + .data(using: .utf8)! + .sha256() + let privateKey = PrivateKey( + data: privateKeyData, + network: .mainnetBTC, + isPublicKeyCompressed: true + ) + return try BtcWallet( + unicId: "BTCBTC", + privateKey: privateKey, + addressConverter: AddressConverterFactory().make(network: .mainnetBTC) + ) + } +} + +private enum Constants { + + static let passphrase = "village lunch say patrol glow first hurt shiver name method dolphin dead" + + static let expectedTransactionID = "e9e99b0d38e3b3fc362a3a9a2809807af179cbaaff59ae7f9ddb3ed30a4f9582" + + static let expectedTransactionHex = + "0100000001a0d73e3bd0aa2025d91eabd8512d5e19ad80752892f415480f75b97966b06f0e010000006a47304402200f8908e3a4b1c3ab181fa875c15dc8816ec29298a74e78122000d4e08bced3a2022016767a16bb9ea315a9ea9a8536d39bd3e6e8dce3594cf5b17c4576f7bfc39140012102cd3dcbdfc1b77e54b3a8f273310806ab56b0c2463c2f1677c7694a89a713e0d0ffffffff0228230000000000001976a914c6251d0e16c0e1946b745b69caa3a7c36014381088ac38560200000000001976a91457f6f900ac7a7e3ccab712326cd7b85638fc15a888ac00000000" + + static let unspentTranscationsData = unspentTranscationsRawJSON.data(using: .utf8)! + + static let unspentTranscationsRawJSON: String = """ + [{ + "txid":"0e6fb06679b9750f4815f492287580ad195e2d51d8ab1ed92520aad03b3ed7a0", + "vout":1, + "status":{ + "confirmed":true, + "block_height":879091, + "block_hash":"00000000000000000001e0da09b0792ff69dcd98af264b1750cbf9ef2deab73d", + "block_time":1736786953}, + "value":164303 + }] + """ +} diff --git a/AdamantTests/Modules/Wallets/BtcWalletServiceTests.swift b/AdamantTests/Modules/Wallets/BtcWalletServiceTests.swift new file mode 100644 index 000000000..a8a9c659a --- /dev/null +++ b/AdamantTests/Modules/Wallets/BtcWalletServiceTests.swift @@ -0,0 +1,422 @@ +// +// BtcWalletServiceTests.swift +// Adamant +// +// Created by Christian Benua on 09.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import BitcoinKit +import CommonKit +import SwiftyMocky +import Swinject +import XCTest + +@testable import Adamant + +final class BtcWalletServiceTests: XCTestCase { + + private var addressConverterMock: AddressConverterMock! + private var apiCoreMock: APICoreProtocolMock! + private var btcApiServiceProtocolMock: BtcApiServiceProtocolMock! + private var transactionFactoryMock: BitcoinKitTransactionFactoryProtocolMock! + private var sut: BtcWalletService! + + override func setUp() { + super.setUp() + addressConverterMock = AddressConverterMock() + apiCoreMock = APICoreProtocolMock() + btcApiServiceProtocolMock = BtcApiServiceProtocolMock() + btcApiServiceProtocolMock.api = BtcApiCore(apiCore: apiCoreMock) + + sut = BtcWalletService() + sut.addressConverter = addressConverterMock + sut.btcApiService = btcApiServiceProtocolMock + transactionFactoryMock = BitcoinKitTransactionFactoryProtocolMock() + Matcher.default.register((AddressProtocol & QRCodeConvertible).self) { + $0.lockingScript == $1.lockingScript + && $0.lockingScriptPayload == $1.lockingScriptPayload + && $0.scriptType == $1.scriptType + && $0.stringValue == $1.stringValue + && $0.qrcodeString == $1.qrcodeString + } + sut.btcTransactionFactory = transactionFactoryMock + } + + override func tearDown() { + addressConverterMock = nil + apiCoreMock = nil + btcApiServiceProtocolMock = nil + transactionFactoryMock = nil + sut = nil + super.tearDown() + } + + func test_createTransaction_noWalletThrowsError() async throws { + // GIVEN + sut.setWalletForTests(nil) + + // WHEN + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: "recipient", + amount: 10, + fee: 0.1, + comment: nil + ) + }) + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .notLogged) + } + + func test_createTransaction_accountNotFoundThrowsError() async throws { + // GIVEN + sut.setWalletForTests(try makeWallet()) + addressConverterMock.given(.convert(address: .any, willThrow: NSError())) + + // WHEN + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: "recipient", + amount: 10, + fee: 1, + comment: nil + ) + }) + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .accountNotFound) + } + + func test_createTransaction_notEnoughMoneyThrowsError() async throws { + // GIVEN + sut.setWalletForTests(try makeWallet()) + let data = Constants.unspentTranscationsData + await apiCoreMock.isolated { mock in + mock.given( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + parameters: .any([String: String].self), + encoding: .any, + timeout: .any, + downloadProgress: .any, + willReturn: APIResponseModel( + result: .success(data), + data: data, + code: 200 + ) + ) + ) + } + addressConverterMock.given(.convert(address: .any, willReturn: try makeDefaultAddress())) + + // WHEN + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: "recipient", + amount: 30 / BtcWalletService.multiplier, + fee: 1 / BtcWalletService.multiplier, + comment: nil + ) + }) + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .notEnoughMoney) + } + + func test_createTransaction_badUnspentTransactionResponseDataThrowsError() async throws { + // GIVEN + sut.setWalletForTests(try makeWallet()) + let data = Constants.unspentTranscationsCorruptedData + await apiCoreMock.isolated { mock in + mock.given( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + parameters: .any([String: String].self), + encoding: .any, + timeout: .any, + downloadProgress: .any, + willReturn: APIResponseModel( + result: .success(data), + data: data, + code: 200 + ) + ) + ) + } + addressConverterMock.given(.convert(address: .any, willReturn: try makeDefaultAddress())) + + // WHEN + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: "recipient", + amount: 30 / BtcWalletService.multiplier, + fee: 1 / BtcWalletService.multiplier, + comment: nil + ) + }) + + // THEN + switch result.error as? WalletServiceError { + case .internalError?: + break + default: + XCTFail("Expected `internalError`, but got \(String(describing: result.error))") + } + } + + func test_createTransaction_enoughMoneyReturnsRealTransaction() async throws { + // GIVEN + sut.setWalletForTests(try makeWallet(address: Constants.anotherBtcAddress)) + let data = Constants.unspentTranscationsData + await apiCoreMock.isolated { mock in + mock.given( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + parameters: .any([String: String].self), + encoding: .any, + timeout: .any, + downloadProgress: .any, + willReturn: APIResponseModel( + result: .success(data), + data: data, + code: 200 + ) + ) + ) + } + let expectedToAddress = try makeDefaultAddress() + let changeAddress = try XCTUnwrap(sut.btcWallet?.addressEntity) + let expectedTransaction = BitcoinKit.Transaction.createNewTransaction( + toAddress: expectedToAddress, + amount: 10, + fee: 1, + changeAddress: changeAddress, + utxos: Constants.expectedUnspentTransactions, + lockTime: 0, + keys: [] + ) + addressConverterMock.given(.convert(address: .any, willReturn: expectedToAddress)) + transactionFactoryMock.given( + .createTransaction( + toAddress: .any, + amount: .any, + fee: .any, + changeAddress: .any, + utxos: .any, + lockTime: .any, + keys: .any, + willReturn: expectedTransaction + ) + ) + + // WHEN + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: "recipient", + amount: 10 / BtcWalletService.multiplier, + fee: 1 / BtcWalletService.multiplier, + comment: nil + ) + }) + + // THEN + XCTAssertNil(result.error) + XCTAssertEqual(result.value, Constants.expectedTransaction) + + transactionFactoryMock.verify( + .createTransaction( + toAddress: .value(expectedToAddress), + amount: .value(10), + fee: .value(1), + changeAddress: .value(changeAddress), + utxos: .value(Constants.expectedUnspentTransactions), + lockTime: .any, + keys: .any + ) + ) + } + + func test_sendTransaction_failIfTxIdCorrupted() async throws { + // GIVEN + let txData = try XCTUnwrap(Constants.anotherTransactionId.data(using: .utf8)) + await apiCoreMock.isolated { mock in + mock.given( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + parameters: .any([String: String].self), + encoding: .any, + timeout: .any, + downloadProgress: .any, + willReturn: APIResponseModel( + result: .success(txData), + data: txData, + code: 200 + ) + ) + ) + } + + // WHEN + let result = await Result { + try await self.sut.sendTransaction(BitcoinKit.Transaction.deserialize(Data(hex: Constants.transactionHex)!)) + } + + // THEN + XCTAssertEqual( + result.error as? WalletServiceError, + WalletServiceError.remoteServiceError(message: Constants.anotherTransactionId) + ) + } + + func test_sendTransaction_successIfTxIdMatches() async throws { + // GIVEN + let txData = try XCTUnwrap(Constants.transactionId.data(using: .utf8)) + await apiCoreMock.isolated { mock in + mock.given( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + parameters: .any([String: String].self), + encoding: .any, + timeout: .any, + downloadProgress: .any, + willReturn: APIResponseModel( + result: .success(txData), + data: txData, + code: 200 + ) + ) + ) + } + + // WHEN + let result = await Result { + try await self.sut.sendTransaction(BitcoinKit.Transaction.deserialize(Data(hex: Constants.transactionHex)!)) + } + + // THEN + XCTAssertNil(result.error) + } + + private func makeWallet(address: String = Constants.btcAddress) throws -> BtcWallet { + let privateKeyData = "my long passphrase" + .data(using: .utf8)! + .sha256() + let privateKey = PrivateKey( + data: privateKeyData, + network: .testnet, + isPublicKeyCompressed: true + ) + + return try BtcWallet( + unicId: "unicId", + privateKey: privateKey, + address: makeDefaultAddress(address: address) + ) + } + + private func makeDefaultAddress(address: String = Constants.btcAddress) throws -> Address { + try AddressConverterFactory() + .make(network: .mainnetBTC) + .convert(address: address) + } + + private func assertAddressesEqual(_ lhs: Address, _ rhs: Address, file: StaticString = #file, line: UInt = #line) { + XCTAssertEqual(lhs.lockingScript, rhs.lockingScript, file: file, line: line) + XCTAssertEqual(lhs.stringValue, rhs.stringValue, file: file, line: line) + XCTAssertEqual(lhs.lockingScriptPayload, rhs.lockingScriptPayload, file: file, line: line) + XCTAssertEqual(lhs.scriptType, rhs.scriptType, file: file, line: line) + } +} + +private enum Constants { + + static let btcAddress = "1DU8Hi1sbHTpEP9vViBEkEw6noeUrgKkJH" + + static let anotherBtcAddress = "1JFHeK6mv8g7EmLj6g5MRgbQ58B9udtHC5" + + static let lockingScript = Data([118, 169, 20, 136, 194, 210, 250, 132, 98, 130, 200, 112, 167, 108, 173, 236, 190, 69, 196, 172, 215, 43, 182, 136, 172]) + + static let lockingScript2 = Data([118, 169, 20, 189, 45, 218, 220, 109, 190, 133, 34, 44, 61, 83, 31, 41, 204, 37, 209, 62, 168, 11, 45, 136, 172]) + + static let transactionId = "8b2654793f94539e5c66b87dee6d0908fb9728eb25c90396e25286c6d4b8a371" + + static let anotherTransactionId = String("8b2654793f94539e5c66b87dee6d0908fb9728eb25c90396e25286c6d4b8a371".reversed()) + + static let transactionHex = + "0100000001a0d73e3bd0aa2025d91eabd8512d5e19ad80752892f415480f75b97966b06f0e010000006a473044022072c8ecd3143e663520807c496dba3dc8010478f3cae09fcb65995be29737a55702206d23617cad2f88a3bd28757be956c731dbde06615fb9bb9fabf2d55e6a8f67ba0121037ec9f6126013088b3d1e8f844f3e755144756a4e9a7da6b0094c189f55031934ffffffff0228230000000000001976a914c6251d0e16c0e1946b745b69caa3a7c36014381088ac38560200000000001976a914931ef5cbdad28723ba9596de5da1145ae969a71888ac00000000" + + static let expectedTransaction = BitcoinKit.Transaction( + version: 1, + inputs: [ + TransactionInput(previousOutput: TransactionOutPoint(hash: Data(), index: 1), signatureScript: Data(), sequence: 4_294_967_295), + TransactionInput(previousOutput: TransactionOutPoint(hash: Data(), index: 2), signatureScript: Data(), sequence: 4_294_967_295) + ], + outputs: [ + TransactionOutput( + value: 10, + lockingScript: Constants.lockingScript + ), + TransactionOutput( + value: 19, + lockingScript: Constants.lockingScript2 + ) + ], + lockTime: 0 + ) + + static let expectedUnspentTransactions = [ + UnspentTransaction( + output: TransactionOutput(value: 10, lockingScript: Constants.lockingScript2), + outpoint: TransactionOutPoint(hash: Data(), index: 1) + ), + UnspentTransaction( + output: TransactionOutput(value: 20, lockingScript: Constants.lockingScript2), + outpoint: TransactionOutPoint(hash: Data(), index: 2) + ) + ] + + static let unspentTranscationsData = unspentTranscationsRawJSON.data(using: .utf8)! + + static let unspentTranscationsCorruptedData = Data(unspentTranscationsData.shuffled()) + + static let unspentTranscationsRawJSON: String = """ + [ + { + "txid": "1", + "vout": 1, + "value": 10, + "status": { + "confirmed": true + } + }, + { + "txid": "1", + "vout": 2, + "value": 20, + "status": { + "confirmed": true + } + }, + { + "txid": "1", + "vout": 3, + "value": 30, + "status": { + "confirmed": false + } + } + ] + """ +} diff --git a/AdamantTests/Modules/Wallets/DashWalletServiceIntegrationTests.swift b/AdamantTests/Modules/Wallets/DashWalletServiceIntegrationTests.swift new file mode 100644 index 000000000..821f240ad --- /dev/null +++ b/AdamantTests/Modules/Wallets/DashWalletServiceIntegrationTests.swift @@ -0,0 +1,179 @@ +// +// DashWalletServiceIntegrationTests.swift +// Adamant +// +// Created by Christian Benua on 24.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import BitcoinKit +import CommonKit +import Swinject +import XCTest + +@testable import Adamant + +final class DashWalletServiceIntegrationTests: XCTestCase { + + private var apiCoreMock: APICoreProtocolMock! + private var lastTransactionStorageMock: DashLastTransactionStorageProtocolMock! + private var dashApiServiceProtocolMock: DashApiServiceProtocolMock! + private var sut: DashWalletService! + + override func setUp() { + super.setUp() + apiCoreMock = APICoreProtocolMock() + dashApiServiceProtocolMock = DashApiServiceProtocolMock() + dashApiServiceProtocolMock.api = DashApiCore(apiCore: apiCoreMock) + lastTransactionStorageMock = DashLastTransactionStorageProtocolMock() + + sut = DashWalletService() + sut.lastTransactionStorage = lastTransactionStorageMock + sut.addressConverter = AddressConverterFactory().make(network: DashMainnet()) + sut.dashApiService = dashApiServiceProtocolMock + sut.transactionFactory = BitcoinKitTransactionFactory() + } + + override func tearDown() { + apiCoreMock = nil + dashApiServiceProtocolMock = nil + lastTransactionStorageMock = nil + sut = nil + super.tearDown() + } + + func test_createAndSendTransaction_createsValidTxIdAndHash() async throws { + // GIVEN + sut.setWalletForTests(try makeWallet()) + let data = Constants.unspentTranscationsData + await apiCoreMock.isolated { mock in + mock.given( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + parameters: .any(DashGetUnspentTransactionDTO.self), + encoding: .any, + timeout: .any, + downloadProgress: .any, + willReturn: APIResponseModel( + result: .success(data), + data: data, + code: 200 + ) + ) + ) + } + + // WHEN 1 + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: Constants.recipient, + amount: 0.01, + fee: 0.0000001, + comment: nil + ) + }) + + // THEN 1 + let transaction = try XCTUnwrap(result.value) + XCTAssertEqual(transaction.serialized().hex, Constants.expectedTransactionHex) + XCTAssertEqual(transaction.txID, Constants.expectedTransactionID) + + // GIVEN 2 + let txData = Constants.sendTransactionResponseData + await apiCoreMock.isolated { mock in + mock.given( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + parameters: .any(DashSendRawTransactionDTO.self), + encoding: .any, + timeout: .any, + downloadProgress: .any, + willReturn: APIResponseModel( + result: .success(txData), + data: txData, + code: 200 + ) + ) + ) + } + + // WHEN 2 + let result2 = await Result { + try await self.sut.sendTransaction(transaction) + } + // THEN 2 + XCTAssertNil(result2.error) + await apiCoreMock.isolated { mock in + mock.verify( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + parameters: .any(DashGetUnspentTransactionDTO.self), + encoding: .any, + timeout: .any, + downloadProgress: .any + ), + count: 1 + ) + mock.verify( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + parameters: .any(DashSendRawTransactionDTO.self), + encoding: .any, + timeout: .any, + downloadProgress: .any + ), + count: 1 + ) + } + } +} + +// MARK: Private + +extension DashWalletServiceIntegrationTests { + fileprivate func makeWallet() throws -> DashWallet { + let privateKeyData = Constants.passphrase + .data(using: .utf8)! + .sha256() + let privateKey = PrivateKey( + data: privateKeyData, + network: DashMainnet(), + isPublicKeyCompressed: true + ) + return try DashWallet( + unicId: "DASHDASH", + privateKey: privateKey, + addressConverter: AddressConverterFactory().make(network: DashMainnet()) + ) + } +} + +private enum Constants { + + static let passphrase = "village lunch say patrol glow first hurt shiver name method dolphin dead" + + static let expectedTransactionID = "d4cf3fde45d0e7ba855db9621bdc6da091856011d86f199bebd1937f7b63020a" + + static let recipient = "Xp6kFbogHMD4QRBDLQdqRp5zUgzmfj1KPn" + + static let expectedTransactionHex = + "0100000001721f0c2437124acb8a20fea5af60908057b086118b240119adcb39c890b4c3a2010000006a473044022002e19bc62748ca3f34a6e5f1aeab31bb3dc43997792d9551e9b8c51a094abbae02200e3c9bde7c60326ef6984045a81304a9915e8fbce96de01813fe89886ef26453012102cd3dcbdfc1b77e54b3a8f273310806ab56b0c2463c2f1677c7694a89a713e0d0ffffffff0240420f00000000001976a914931ef5cbdad28723ba9596de5da1145ae969a71888acb695a905000000001976a91457f6f900ac7a7e3ccab712326cd7b85638fc15a888ac00000000" + + static let unspentTranscationsData = Data.readResource( + name: "dash_unspent_transaction_intergration_test", + withExtension: "json" + )! + + static let sendTransactionResponseData = Data.readResource( + name: "dash_send_transation_response", + withExtension: "json" + )! +} diff --git a/AdamantTests/Modules/Wallets/DashWalletServiceTests.swift b/AdamantTests/Modules/Wallets/DashWalletServiceTests.swift new file mode 100644 index 000000000..cb33977c0 --- /dev/null +++ b/AdamantTests/Modules/Wallets/DashWalletServiceTests.swift @@ -0,0 +1,409 @@ +// +// DashWalletServiceTests.swift +// Adamant +// +// Created by Christian Benua on 22.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import BitcoinKit +import CommonKit +import XCTest + +@testable import Adamant + +final class DashWalletServiceTests: XCTestCase { + + private var sut: DashWalletService! + private var lastTransactionStorageMock: DashLastTransactionStorageProtocolMock! + private var apiServiceMock: DashApiServiceProtocolMock! + private var apiCoreMock: APICoreProtocolMock! + private var transactionFactoryMock: BitcoinKitTransactionFactoryProtocolMock! + + override func setUp() { + super.setUp() + + lastTransactionStorageMock = DashLastTransactionStorageProtocolMock() + transactionFactoryMock = BitcoinKitTransactionFactoryProtocolMock() + apiCoreMock = APICoreProtocolMock() + apiServiceMock = DashApiServiceProtocolMock() + apiServiceMock.api = DashApiCore(apiCore: apiCoreMock) + sut = DashWalletService() + + sut.dashApiService = apiServiceMock + sut.lastTransactionStorage = lastTransactionStorageMock + sut.addressConverter = makeAddressConverter() + sut.transactionFactory = transactionFactoryMock + } + + override func tearDown() { + sut = nil + apiServiceMock = nil + lastTransactionStorageMock = nil + apiCoreMock = nil + transactionFactoryMock = nil + + super.tearDown() + } + + func test_createTransaction_throwsErrorWhenHasLastTransactionIdAndNotEnoughConfirmations() async throws { + // GIVEN + lastTransactionStorageMock.given(.getLastTransactionId(willReturn: Constants.lastTransactionId)) + await apiCoreMock.isolated { mock in + mock.given( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + jsonParameters: .any, + timeout: .any, + willReturn: APIResponseModel( + result: .success(Constants.getTransactionZeroConfirmationsData), + data: Constants.getTransactionZeroConfirmationsData, + code: 200 + ) + ) + ) + } + + // WHEN + let result = await Result { + try await self.sut.create(recipient: Constants.invalidDashAddress, amount: 10) + } + + // THEN + switch result.error as? WalletServiceError { + case .remoteServiceError?: + break + default: + XCTFail("Expected .remoteServiceError, but got \(result.error) error") + } + } + + func test_createTransaction_throwsErrorWhenNoWallet() async throws { + // GIVEN + lastTransactionStorageMock.given(.getLastTransactionId(willReturn: nil)) + + // WHEN + let result = await Result { + try await self.sut.create(recipient: Constants.invalidDashAddress, amount: 10) + } + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .notLogged) + } + + func test_createTransaction_throwsErrorWhenInvalidRecipient() async throws { + // GIVEN + sut.setWalletForTests(try makeWallet()) + lastTransactionStorageMock.given(.getLastTransactionId(willReturn: nil)) + + // WHEN + let result = await Result { + try await self.sut.create(recipient: Constants.invalidDashAddress, amount: 10) + } + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .accountNotFound) + } + + func test_createTransaction_badUnspentTransactionResponseDataThrowsError() async throws { + // GIVEN + sut.setWalletForTests(try makeWallet()) + let data = Constants.unspentTransactionsCorruptedData + await apiCoreMock.isolated { mock in + mock.given( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + parameters: .any(DashGetUnspentTransactionDTO.self), + encoding: .any, + timeout: .any, + downloadProgress: .any, + willReturn: APIResponseModel( + result: .success(data), + data: data, + code: 200 + ) + ) + ) + } + + // WHEN + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: Constants.validDashAddress, + amount: 30 / DashWalletService.multiplier, + fee: 1 / DashWalletService.multiplier, + comment: nil + ) + }) + + // THEN + XCTAssertNotNil(result.error) + } + + func test_createTransaction_notEnoughMoneyThrowsError() async throws { + // GIVEN + sut.setWalletForTests(try makeWallet()) + let data = Constants.unspentTranscationsData + await apiCoreMock.isolated { mock in + mock.given( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + parameters: .any(DashGetUnspentTransactionDTO.self), + encoding: .any, + timeout: .any, + downloadProgress: .any, + willReturn: APIResponseModel( + result: .success(data), + data: data, + code: 200 + ) + ) + ) + } + + // WHEN + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: Constants.validDashAddress, + amount: 130 / DashWalletService.multiplier, + fee: 1 / DashWalletService.multiplier, + comment: nil + ) + }) + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .notEnoughMoney) + } + + func test_createTransaction_enoughMoneyReturnsRealTransaction() async throws { + // GIVEN + sut.setWalletForTests(try makeWallet()) + let data = Constants.unspentTranscationsData + await apiCoreMock.isolated { mock in + mock.given( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + parameters: .any(DashGetUnspentTransactionDTO.self), + encoding: .any, + timeout: .any, + downloadProgress: .any, + willReturn: APIResponseModel( + result: .success(data), + data: data, + code: 200 + ) + ) + ) + } + transactionFactoryMock.given( + .createTransaction( + toAddress: .any, + amount: .any, + fee: .any, + changeAddress: .any, + utxos: .any, + lockTime: .any, + keys: .any, + willReturn: Constants.expectedTransaction + ) + ) + + // WHEN + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: Constants.validDashAddress, + amount: 30 / DashWalletService.multiplier, + fee: 1 / DashWalletService.multiplier, + comment: nil + ) + }) + + // THEN + XCTAssertNil(result.error) + XCTAssertEqual(result.value?.version, Constants.expectedTransaction.version) + XCTAssertEqual(result.value?.outputs, Constants.expectedTransaction.outputs) + let changeAddress = try XCTUnwrap(try makeWallet().address) + transactionFactoryMock.verify( + .createTransaction( + toAddress: .matching { $0.stringValue == Constants.validDashAddress }, + amount: .value(30), + fee: .value(1), + changeAddress: .matching { $0.stringValue == changeAddress }, + utxos: .value(Constants.expectedUnspentTransactions), + lockTime: .any, + keys: .any + ) + ) + } + + func test_createAndSendTransaction_updatesLastTransactionId() async throws { + // GIVEN + sut.setWalletForTests(try makeWallet()) + let data = Constants.unspentTranscationsData + await apiCoreMock.isolated { mock in + mock.given( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + parameters: .any(DashGetUnspentTransactionDTO.self), + encoding: .any, + timeout: .any, + downloadProgress: .any, + willReturn: APIResponseModel( + result: .success(data), + data: data, + code: 200 + ) + ) + ) + } + + transactionFactoryMock.given( + .createTransaction( + toAddress: .any, + amount: .any, + fee: .any, + changeAddress: .any, + utxos: .any, + lockTime: .any, + keys: .any, + willReturn: Constants.expectedTransaction + ) + ) + + // WHEN 1 + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: Constants.validDashAddress, + amount: 30 / DashWalletService.multiplier, + fee: 1 / DashWalletService.multiplier, + comment: nil + ) + }) + + // THEN 1 + XCTAssertNil(result.error) + let transaction = try XCTUnwrap(result.value) + + // GIVEN 2 + await apiCoreMock.isolated { mock in + mock.given( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + parameters: .any(DashSendRawTransactionDTO.self), + encoding: .any, + timeout: .any, + downloadProgress: .any, + willReturn: APIResponseModel( + result: .success(Constants.sendTransactionResponseData), + data: Constants.sendTransactionResponseData, + code: 200 + ) + ) + ) + } + + // WHEN 2 + let result2 = await Result(catchingAsync: { + try await self.sut.sendTransaction(transaction) + }) + + // THEN 2 + XCTAssertNil(result2.error) + lastTransactionStorageMock.verify(.setLastTransactionId(.value(transaction.txID)), count: 1) + } +} + +// MARK: Private + +extension DashWalletServiceTests { + fileprivate func makeWallet() throws -> DashWallet { + let privateKeyData = Constants.passphrase.data(using: .utf8)!.sha256() + let key = PrivateKey(data: privateKeyData, network: DashMainnet(), isPublicKeyCompressed: true) + return try DashWallet( + unicId: Constants.tokenUnicId, + privateKey: key, + addressConverter: makeAddressConverter() + ) + } + + fileprivate func makeAddressConverter() -> AddressConverter { + AddressConverterFactory().make(network: DashMainnet()) + } +} + +private enum Constants { + + static let tokenUnicId = "DASHDASH" + + static let validDashAddress = "Xp6kFbogHMD4QRBDLQdqRp5zUgzmfj1KPn" + + static let invalidDashAddress = "recipient" + + static let passphrase = "village lunch say patrol glow first hurt shiver name method dolphin dead" + + static let lastTransactionId = "lastTransactionId" + + // RPCResponseModel with BTCRawTransaction inside data + static let getTransactionZeroConfirmationsData = Data.readResource( + name: "dash_unverified_unspent_transactions", + withExtension: "json" + )! + + static let unspentTranscationsData = Data.readResource( + name: "dash_unspent_transaction_unit_test", + withExtension: "json" + )! + + static let unspentTransactionsCorruptedData = Data() + + static let sendTransactionResponseData = Data.readResource( + name: "dash_send_transaction_unit_response", + withExtension: "json" + )! + + static let expectedUnspentTransactions = [ + UnspentTransaction( + output: TransactionOutput(value: 30, lockingScript: Constants.lockingScript2), + outpoint: TransactionOutPoint(hash: Data(), index: 1) + ), + UnspentTransaction( + output: TransactionOutput(value: 20, lockingScript: Constants.lockingScript2), + outpoint: TransactionOutPoint(hash: Data(), index: 2) + ) + ] + + static let expectedTransaction = BitcoinKit.Transaction( + version: 1, + inputs: [ + TransactionInput(previousOutput: TransactionOutPoint(hash: Data(), index: 1), signatureScript: Data(), sequence: 4_294_967_295), + TransactionInput(previousOutput: TransactionOutPoint(hash: Data(), index: 2), signatureScript: Data(), sequence: 4_294_967_295) + ], + outputs: [ + TransactionOutput( + value: 30, + lockingScript: Constants.lockingScript + ), + TransactionOutput( + value: 19, + lockingScript: Constants.lockingScript2 + ) + ], + lockTime: 0 + ) + + static let lockingScript = Data([118, 169, 20, 147, 30, 245, 203, 218, 210, 135, 35, 186, 149, 150, 222, 93, 161, 20, 90, 233, 105, 167, 24, 136, 172]) + + static let lockingScript2 = Data([118, 169, 20, 87, 246, 249, 0, 172, 122, 126, 60, 202, 183, 18, 50, 108, 215, 184, 86, 56, 252, 21, 168, 136, 172]) +} diff --git a/AdamantTests/Modules/Wallets/DogeWalletServiceIntegrationTests.swift b/AdamantTests/Modules/Wallets/DogeWalletServiceIntegrationTests.swift new file mode 100644 index 000000000..5225107be --- /dev/null +++ b/AdamantTests/Modules/Wallets/DogeWalletServiceIntegrationTests.swift @@ -0,0 +1,178 @@ +// +// DogeWalletServiceIntegrationTests.swift +// Adamant +// +// Created by Christian Benua on 20.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import BitcoinKit +import CommonKit +import Swinject +import XCTest + +@testable import Adamant + +final class DogeWalletServiceIntegrationTests: XCTestCase { + + private var apiCoreMock: APICoreProtocolMock! + private var dogeApiServiceProtocolMock: DogeApiServiceProtocolMock! + private var sut: DogeWalletService! + + override func setUp() { + super.setUp() + apiCoreMock = APICoreProtocolMock() + dogeApiServiceProtocolMock = DogeApiServiceProtocolMock() + dogeApiServiceProtocolMock._api = DogeApiCore(apiCore: apiCoreMock) + + sut = DogeWalletService() + sut.addressConverter = AddressConverterFactory().make(network: DogeMainnet()) + sut.dogeApiService = dogeApiServiceProtocolMock + sut.btcTransactionFactory = BitcoinKitTransactionFactory() + } + + override func tearDown() { + apiCoreMock = nil + dogeApiServiceProtocolMock = nil + sut = nil + super.tearDown() + } + + func test_createAndSendTransaction_createsValidTxIdAndHash() async throws { + // GIVEN + sut.setWalletForTests(try makeWallet()) + let data = Constants.unspentTranscationsData + await apiCoreMock.isolated { mock in + mock.given( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + parameters: .any([String: String].self), + encoding: .any, + timeout: .any, + downloadProgress: .any, + willReturn: APIResponseModel( + result: .success(data), + data: data, + code: 200 + ) + ) + ) + } + + // WHEN 1 + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: Constants.recipient, + amount: 9, + fee: 1, + comment: nil + ) + }) + + // THEN 1 + let transaction = try XCTUnwrap(result.value) + XCTAssertEqual(transaction.serialized().hex, Constants.expectedTransactionHex) + XCTAssertEqual(transaction.txID, Constants.expectedTransactionID) + + // GIVEN 2 + let txData = try XCTUnwrap(transaction.txID.data(using: .utf8)) + await apiCoreMock.isolated { mock in + mock.given( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + parameters: .any([String: String].self), + encoding: .any, + timeout: .any, + downloadProgress: .any, + willReturn: APIResponseModel( + result: .success(txData), + data: txData, + code: 200 + ) + ) + ) + } + + // WHEN 2 + let result2 = await Result { + try await self.sut.sendTransaction(transaction) + } + // THEN 3 + XCTAssertNil(result2.error) + await apiCoreMock.isolated { mock in + mock.verify( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + parameters: .any([String: String].self), + encoding: .any, + timeout: .any, + downloadProgress: .any + ), + count: 2 + ) + } + } +} + +extension DogeWalletServiceIntegrationTests { + fileprivate func makeWallet() throws -> DogeWallet { + let privateKeyData = Constants.passphrase + .data(using: .utf8)! + .sha256() + let privateKey = PrivateKey( + data: privateKeyData, + network: DogeMainnet(), + isPublicKeyCompressed: true + ) + return try DogeWallet( + unicId: Constants.tokenId, + privateKey: privateKey, + addressConverter: AddressConverterFactory().make(network: DogeMainnet()) + ) + } +} + +private enum Constants { + + static let passphrase = "village lunch say patrol glow first hurt shiver name method dolphin dead" + + static let recipient = "DPCnnvzngz9AcpToiM7Y8qLewEDtP7jN8T" + + static let tokenId = "DOGEDOGE" + + static let expectedTransactionID = "4f9700bca38cce8f442ba0ebf6b2c1b95d235854cabb797fb1178499a5403c7a" + + static let expectedTransactionHex = + "0100000001010000006b483045022100c54ae687dfaa6e910eaf2d40ec755cc11eb1263de38cbe4a5b48b1a13c6d113c022043cce15981221cef35fbcfe0a35ad9a9a218257acb854d1dc0f6e0ebbe892c2d012102cd3dcbdfc1b77e54b3a8f273310806ab56b0c2463c2f1677c7694a89a713e0d0ffffffff0200e9a435000000001976a914c6251d0e16c0e1946b745b69caa3a7c36014381088ac00362634f28623001976a91457f6f900ac7a7e3ccab712326cd7b85638fc15a888ac00000000" + + static let unspentTranscationsData = unspentTranscationsRawJSON.data(using: .utf8)! + + static let unspentTranscationsRawJSON: String = """ + [ + { + "txid": "1", + "vout": 1, + "amount": 100000000, + "confirmations": 1 + }, + { + "txid": "1", + "vout": 2, + "amount": 100000000, + "confirmations": 1 + }, + { + "txid": "1", + "vout": 3, + "amount": 200000000, + "confirmations": 0 + } + ] + """ +} diff --git a/AdamantTests/Modules/Wallets/DogeWalletServiceTests.swift b/AdamantTests/Modules/Wallets/DogeWalletServiceTests.swift new file mode 100644 index 000000000..b677effa0 --- /dev/null +++ b/AdamantTests/Modules/Wallets/DogeWalletServiceTests.swift @@ -0,0 +1,373 @@ +// +// DogeWalletServiceTests.swift +// Adamant +// +// Created by Christian Benua on 17.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import BitcoinKit +import CommonKit +import SwiftyMocky +import XCTest + +@testable import Adamant + +final class DogeWalletServiceTests: XCTestCase { + private var addressConverterMock: AddressConverterMock! + private var apiCoreMock: APICoreProtocolMock! + private var dogeApiServiceProtocolMock: DogeApiServiceProtocolMock! + private var transactionFactoryMock: BitcoinKitTransactionFactoryProtocolMock! + private var sut: DogeWalletService! + + override func setUp() { + super.setUp() + addressConverterMock = AddressConverterMock() + apiCoreMock = APICoreProtocolMock() + dogeApiServiceProtocolMock = DogeApiServiceProtocolMock() + dogeApiServiceProtocolMock._api = DogeApiCore(apiCore: apiCoreMock) + + sut = DogeWalletService() + sut.addressConverter = addressConverterMock + sut.dogeApiService = dogeApiServiceProtocolMock + transactionFactoryMock = BitcoinKitTransactionFactoryProtocolMock() + sut.btcTransactionFactory = transactionFactoryMock + Matcher.default.register((AddressProtocol & QRCodeConvertible).self) { + $0.lockingScript == $1.lockingScript + && $0.lockingScriptPayload == $1.lockingScriptPayload + && $0.scriptType == $1.scriptType + && $0.stringValue == $1.stringValue + && $0.qrcodeString == $1.qrcodeString + } + } + + override func tearDown() { + addressConverterMock = nil + apiCoreMock = nil + dogeApiServiceProtocolMock = nil + transactionFactoryMock = nil + sut = nil + super.tearDown() + } + + func test_createTransaction_noWalletThrowsError() async throws { + // GIVEN + sut.setWalletForTests(nil) + + // WHEN + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: "recipient", + amount: 10, + fee: 0.1, + comment: nil + ) + }) + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .notLogged) + } + + func test_createTransaction_accountNotFoundThrowsError() async throws { + // GIVEN + sut.setWalletForTests(try makeWallet()) + addressConverterMock.given(.convert(address: .any, willThrow: NSError())) + + // WHEN + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: "recipient", + amount: 10, + fee: 1, + comment: nil + ) + }) + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .accountNotFound) + } + + func test_createTransaction_notEnoughMoneyThrowsError() async throws { + // GIVEN + sut.setWalletForTests(try makeWallet()) + let data = Constants.unspentTranscationsData + await apiCoreMock.isolated { mock in + mock.given( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + parameters: .any([String: String].self), + encoding: .any, + timeout: .any, + downloadProgress: .any, + willReturn: APIResponseModel( + result: .success(data), + data: data, + code: 200 + ) + ) + ) + } + addressConverterMock.given(.convert(address: .any, willReturn: try makeDefaultAddress())) + + // WHEN + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: "recipient", + amount: 30, + fee: 1, + comment: nil + ) + }) + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .notEnoughMoney) + } + + func test_createTransaction_badUnspentTransactionResponseDataThrowsError() async throws { + // GIVEN + sut.setWalletForTests(try makeWallet()) + let data = Constants.unspentTranscationsCorruptedData + await apiCoreMock.isolated { mock in + mock.given( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + parameters: .any([String: String].self), + encoding: .any, + timeout: .any, + downloadProgress: .any, + willReturn: APIResponseModel( + result: .success(data), + data: data, + code: 200 + ) + ) + ) + } + addressConverterMock.given(.convert(address: .any, willReturn: try makeDefaultAddress())) + + // WHEN + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: "recipient", + amount: 30 / DogeWalletService.multiplier, + fee: 1 / DogeWalletService.multiplier, + comment: nil + ) + }) + + // THEN + switch result.error as? WalletServiceError { + case .remoteServiceError?: + break + default: + XCTFail("Expected `remoteServiceError`, but got \(String(describing: result.error))") + } + } + + func test_createTransaction_enoughMoneyReturnsRealTransaction() async throws { + // GIVEN + sut.setWalletForTests(try makeWallet(address: Constants.anotherDogeAddress)) + let data = Constants.unspentTranscationsData + await apiCoreMock.isolated { mock in + mock.given( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + parameters: .any([String: String].self), + encoding: .any, + timeout: .any, + downloadProgress: .any, + willReturn: APIResponseModel( + result: .success(data), + data: data, + code: 200 + ) + ) + ) + } + let expectedToAddress = try makeDefaultAddress() + addressConverterMock.given(.convert(address: .any, willReturn: expectedToAddress)) + transactionFactoryMock.given( + .createTransaction( + toAddress: .any, + amount: .any, + fee: .any, + changeAddress: .any, + utxos: .any, + lockTime: .any, + keys: .any, + willReturn: Constants.expectedTransaction + ) + ) + + // WHEN + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: "recipient", + amount: 10 / DogeWalletService.multiplier, + fee: 1 / DogeWalletService.multiplier, + comment: nil + ) + }) + + // THEN + XCTAssertNil(result.error) + XCTAssertEqual(result.value, Constants.expectedTransaction) + + let changeAddress = try XCTUnwrap(sut.dogeWallet?.addressEntity) + transactionFactoryMock.verify( + .createTransaction( + toAddress: .value(expectedToAddress), + amount: .value(10), + fee: .value(1), + changeAddress: .value(changeAddress), + utxos: .value(Constants.expectedUnspentTransactions), + lockTime: .any, + keys: .any + ) + ) + } + + func test_sendTransaction_successIfTxIdMatches() async throws { + // GIVEN + let txData = try XCTUnwrap(Constants.transactionId.data(using: .utf8)) + await apiCoreMock.isolated { mock in + mock.given( + .sendRequestBasic( + origin: .any, + path: .any, + method: .any, + parameters: .any([String: String].self), + encoding: .any, + timeout: .any, + downloadProgress: .any, + willReturn: APIResponseModel( + result: .success(txData), + data: txData, + code: 200 + ) + ) + ) + } + + // WHEN + let result = await Result { + try await self.sut.sendTransaction(BitcoinKit.Transaction.deserialize(Data(hex: Constants.transactionHex)!)) + } + + // THEN + XCTAssertNil(result.error) + } +} + +// MARK: Private + +extension DogeWalletServiceTests { + fileprivate func makeWallet(address: String = Constants.dogeAddress) throws -> DogeWallet { + let privateKeyData = "my long passphrase" + .data(using: .utf8)! + .sha256() + let privateKey = PrivateKey( + data: privateKeyData, + network: .testnet, + isPublicKeyCompressed: true + ) + + return try DogeWallet( + unicId: "unicId", + privateKey: privateKey, + addressEntity: makeDefaultAddress(address: address) + ) + } + + fileprivate func makeDefaultAddress(address: String = Constants.dogeAddress) throws -> Address { + try AddressConverterFactory() + .make(network: DogeMainnet()) + .convert(address: address) + } + + fileprivate func assertAddressesEqual(_ lhs: Address, _ rhs: Address, file: StaticString = #file, line: UInt = #line) { + XCTAssertEqual(lhs.lockingScript, rhs.lockingScript, file: file, line: line) + XCTAssertEqual(lhs.stringValue, rhs.stringValue, file: file, line: line) + XCTAssertEqual(lhs.lockingScriptPayload, rhs.lockingScriptPayload, file: file, line: line) + XCTAssertEqual(lhs.scriptType, rhs.scriptType, file: file, line: line) + } +} + +private enum Constants { + static let dogeAddress = "DJYzxc6Rd3tknUmED7KB83ZoXV9NzhZxss" + + static let anotherDogeAddress = "DPCnnvzngz9AcpToiM7Y8qLewEDtP7jN8T" + + static let transactionHex = + "0100000001a0d73e3bd0aa2025d91eabd8512d5e19ad80752892f415480f75b97966b06f0e010000006a473044022072c8ecd3143e663520807c496dba3dc8010478f3cae09fcb65995be29737a55702206d23617cad2f88a3bd28757be956c731dbde06615fb9bb9fabf2d55e6a8f67ba0121037ec9f6126013088b3d1e8f844f3e755144756a4e9a7da6b0094c189f55031934ffffffff0228230000000000001976a914c6251d0e16c0e1946b745b69caa3a7c36014381088ac38560200000000001976a914931ef5cbdad28723ba9596de5da1145ae969a71888ac00000000" + + static let transactionId = "8b2654793f94539e5c66b87dee6d0908fb9728eb25c90396e25286c6d4b8a371" + + static let anotherTransactionId = String("8b2654793f94539e5c66b87dee6d0908fb9728eb25c90396e25286c6d4b8a371".reversed()) + + static let lockingScript = Data([118, 169, 20, 147, 30, 245, 203, 218, 210, 135, 35, 186, 149, 150, 222, 93, 161, 20, 90, 233, 105, 167, 24, 136, 172]) + + static let lockingScript2 = Data([118, 169, 20, 198, 37, 29, 14, 22, 192, 225, 148, 107, 116, 91, 105, 202, 163, 167, 195, 96, 20, 56, 16, 136, 172]) + + static let expectedTransaction = BitcoinKit.Transaction( + version: 1, + inputs: [ + TransactionInput(previousOutput: TransactionOutPoint(hash: Data(), index: 1), signatureScript: Data(), sequence: 4_294_967_295) + ], + outputs: [ + TransactionOutput( + value: 10, + lockingScript: Constants.lockingScript + ), + TransactionOutput( + value: 999_999_989, + lockingScript: Constants.lockingScript2 + ) + ], + lockTime: 0 + ) + + static let expectedUnspentTransactions = [ + UnspentTransaction( + output: TransactionOutput(value: ((10 * DogeWalletService.multiplier) as NSDecimalNumber).uint64Value, lockingScript: Constants.lockingScript2), + outpoint: TransactionOutPoint(hash: Data(), index: 1) + ), + UnspentTransaction( + output: TransactionOutput(value: ((20 * DogeWalletService.multiplier) as NSDecimalNumber).uint64Value, lockingScript: Constants.lockingScript2), + outpoint: TransactionOutPoint(hash: Data(), index: 2) + ) + ] + + static let unspentTranscationsData = unspentTranscationsRawJSON.data(using: .utf8)! + + static let unspentTranscationsCorruptedData = Data(unspentTranscationsData.shuffled()) + + static let unspentTranscationsRawJSON: String = """ + [ + { + "txid": "1", + "vout": 1, + "amount": 10, + "confirmations": 1 + }, + { + "txid": "1", + "vout": 2, + "amount": 20, + "confirmations": 1 + }, + { + "txid": "1", + "vout": 3, + "amount": 30, + "confirmations": 0 + } + ] + """ +} diff --git a/AdamantTests/Modules/Wallets/ERC20WalletServiceTests.swift b/AdamantTests/Modules/Wallets/ERC20WalletServiceTests.swift new file mode 100644 index 000000000..41538a8f0 --- /dev/null +++ b/AdamantTests/Modules/Wallets/ERC20WalletServiceTests.swift @@ -0,0 +1,267 @@ +// +// ERC20WalletServiceTests.swift +// Adamant +// +// Created by Christian Benua on 25.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import BigInt +import Web3Core +import XCTest + +@testable import Adamant +@testable import CommonKit +@testable import web3swift + +final class ERC20WalletServiceTests: XCTestCase { + private var sut: ERC20WalletService! + private var erc20ApiMock: ERC20ApiServiceProtocolMock! + private var apiCoreProtocolMock: APICoreProtocolMock! + private var web3ProviderMock: Web3ProviderMock! + private var increaseFeeServiceMock: IncreaseFeeServiceMock! + private var ethMock: IEthMock! + private var web3: Web3! + private var session: URLSession! + + override func setUp() async throws { + try await super.setUp() + + let keystore = try XCTUnwrap( + try BIP32Keystore( + mnemonics: Constants.passphrase, + password: EthWalletService.walletPassword, + mnemonicsPassword: "", + language: .english, + prefixPath: EthWalletService.walletPath + ) + ) + let ethAddress = try XCTUnwrap(keystore.addresses?.first) + + let eWallet = EthWallet( + unicId: Constants.tokenUniqueID, + address: ethAddress.address, + ethAddress: ethAddress, + keystore: keystore + ) + web3ProviderMock = Web3ProviderMock() + web3 = Web3(provider: web3ProviderMock) + ethMock = IEthMock() + web3.ethInstance = ethMock + ethMock._provider = web3ProviderMock + apiCoreProtocolMock = APICoreProtocolMock() + let ethApi = EthApiCore(apiCore: apiCoreProtocolMock) + erc20ApiMock = ERC20ApiServiceProtocolMock() + erc20ApiMock.api = ethApi + erc20ApiMock.keystoreManager = .init([keystore]) + erc20ApiMock.contractAddress = try XCTUnwrap(EthereumAddress(from: Constants.token.contractAddress)) + erc20ApiMock.web3 = web3 + session = makeSession() + web3ProviderMock.session = session + increaseFeeServiceMock = IncreaseFeeServiceMock() + + increaseFeeServiceMock.given(.isIncreaseFeeEnabled(for: .any, willReturn: false)) + sut = ERC20WalletService(token: Constants.token) + sut.setWalletForTests(eWallet) + sut.increaseFeeService = increaseFeeServiceMock + sut.erc20ApiService = erc20ApiMock + } + + override func tearDown() async throws { + sut = nil + erc20ApiMock = nil + apiCoreProtocolMock = nil + web3ProviderMock = nil + ethMock = nil + web3 = nil + session = nil + increaseFeeServiceMock = nil + try await super.tearDown() + } + + func test_createTransaction_noWalletThrowsError() async throws { + // GIVEN + sut.setWalletForTests(nil) + + // WHEN + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: "recipient", + amount: 10, + fee: 0.1, + comment: nil + ) + }) + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .notLogged) + } + + func test_createTransaction_invalidRecipientAddressThrowsError() async throws { + // WHEN + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: Constants.invalidEthAddress, + amount: 10, + fee: 0.1, + comment: nil + ) + }) + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .accountNotFound) + } + + func test_createTransaction_noKeystoreThrowsError() async throws { + // GIVEN + erc20ApiMock.keystoreManager = nil + + // WHEN + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: Constants.toEthAddress, + amount: 10, + fee: 0.1, + comment: nil + ) + }) + + // THEN + switch result.error as? WalletServiceError { + case .internalError: + break + default: + XCTFail("Expected '.internalError', but got \(String(describing: result.error))") + } + } + + func test_createTransaction_correctFields() async throws { + // GIVEN + makeTransactionsCountMock() + var calledMakeDecimals = false + makeDecimalsMock { + calledMakeDecimals = true + } + + // WHEN + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: Constants.toEthAddress, + amount: 10, + fee: 0.1, + comment: nil + ) + }) + + // THEN + XCTAssertTrue(calledMakeDecimals) + XCTAssertNil(result.error) + let transaction = try XCTUnwrap(result.value) + XCTAssertEqual(transaction.from?.address, Constants.validEthAddress) + XCTAssertEqual(transaction.to.address.lowercased(), Constants.token.contractAddress.lowercased()) + XCTAssertEqual(transaction.nonce, BigUInt(exactly: Constants.nonce)) + XCTAssertEqual(transaction.gasPrice, BigUInt(clamping: 11).toWei()) + XCTAssertEqual(transaction.hashForSignature(), Constants.expectedHashForSignature) + } +} + +// MARK: Private + +extension ERC20WalletServiceTests { + fileprivate func makeDecimalsMock(_ onCall: @escaping () -> Void) { + let prevHandler = MockURLProtocol.requestHandler + MockURLProtocol.requestHandler = MockURLProtocol.combineHandlers( + prevHandler + ) { request in + guard let stream = request.httpBodyStream else { return nil } + + let ethBody = try JSONDecoder().decode(EthRequestBody.self, from: Data(reading: stream)) + guard ethBody.method == Constants.ethCallMethod else { return nil } + onCall() + XCTAssertEqual((ethBody.params[0] as? [String: Any])?["to"] as? String, Constants.token.contractAddress.lowercased()) + return try self.makeResponseAndMockData( + url: request.url!, + ethResponse: EthAPIResponse(result: Constants.decimalsMethodResponse) + ) + } + } + + fileprivate func makeTransactionsCountMock() { + let prevHandler = MockURLProtocol.requestHandler + MockURLProtocol.requestHandler = MockURLProtocol.combineHandlers( + prevHandler + ) { request in + guard let stream = request.httpBodyStream else { return nil } + + let ethBody = try JSONDecoder().decode(EthRequestBody.self, from: Data(reading: stream)) + guard ethBody.method == Constants.transactionCountResponseMethod else { return nil } + + return try self.makeResponseAndMockData( + url: request.url!, + ethResponse: EthAPIResponse(result: "\(Constants.nonce)") + ) + } + } + + fileprivate func makeResponseAndMockData( + url: URL, + ethResponse: EthAPIResponse + ) throws -> (HTTPURLResponse, Data) { + let response = HTTPURLResponse( + url: url, + statusCode: 200, + httpVersion: nil, + headerFields: nil + )! + + let mockData = try JSONEncoder().encode(ethResponse) + return (response, mockData) + } + + fileprivate func makeSession() -> URLSession { + let config = URLSessionConfiguration.ephemeral + config.protocolClasses = [MockURLProtocol.self] + return URLSession(configuration: config) + } +} + +private enum Constants { + + static let passphrase = "village lunch say patrol glow first hurt shiver name method dolphin sample" + + static let tokenUniqueID = "ERC20\(token.symbol)\(token.contractAddress)" + + static let token = ERC20Token( + symbol: "BNB", + name: "Binance Coin", + contractAddress: "0xB8c77482e45F1F44dE1745F52C74426C631bDD52", + decimals: 18, + naturalUnits: 18, + defaultVisibility: false, + defaultOrdinalLevel: nil, + reliabilityGasPricePercent: 10, + reliabilityGasLimitPercent: 10, + increasedGasPricePercent: 0, + defaultGasPriceGwei: 10, + defaultGasLimit: 58000, + warningGasPriceGwei: 25, + transferDecimals: 6 + ) + + static let toEthAddress = "0xabfDF505fFd5587D9E7707dFB47F45AF1f03E275" + static let invalidEthAddress = "0xBA5CE20aE344CDBd6eAA01ffdDF1976d35Be14" + static let validEthAddress = "0xBA5CE20aE344CDBd6eAA01ffdDF1976d35Be142d" + + static let transactionCountResponseMethod = "eth_getTransactionCount" + static let decimalsMethod = "decimals" + static let ethCallMethod = "eth_call" + static let nonce = 2 + static let decimalsMethodResponse = "0x0000000000000000000000000000000000000000000000000000000000000006" + + static let expectedHashForSignature = Data([ + 19, 32, 58, 56, 131, 178, 156, 49, 149, + 112, 90, 80, 243, 4, 152, 101, 158, 186, + 191, 16, 58, 253, 186, 73, 77, 9, 179, + 26, 213, 185, 77, 201 + ]) +} diff --git a/AdamantTests/Modules/Wallets/EthWalletServiceTests.swift b/AdamantTests/Modules/Wallets/EthWalletServiceTests.swift new file mode 100644 index 000000000..53977d802 --- /dev/null +++ b/AdamantTests/Modules/Wallets/EthWalletServiceTests.swift @@ -0,0 +1,290 @@ +// +// EthWalletServiceTests.swift +// Adamant +// +// Created by Christian Benua on 15.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import BigInt +import CommonKit +import Web3Core +import XCTest + +@testable import Adamant +@testable import web3swift + +final class EthWalletServiceTests: XCTestCase { + private var sut: EthWalletService! + + private var apiCoreMock: APICoreProtocolMock! + private var ethApiMock: EthApiServiceProtocolMock! + private var walletStorage: EthWalletStorage! + private var web3ProviderMock: Web3ProviderMock! + private var increaseFeeServiceMock: IncreaseFeeServiceMock! + private var keystoreManager: KeystoreManager! + private var ethMock: IEthMock! + private var web3: Web3! + private var session: URLSession! + + override func setUp() async throws { + try await super.setUp() + apiCoreMock = APICoreProtocolMock() + ethApiMock = EthApiServiceProtocolMock() + web3ProviderMock = Web3ProviderMock() + web3 = Web3(provider: web3ProviderMock) + ethMock = IEthMock() + web3.ethInstance = ethMock + ethMock._provider = web3ProviderMock + ethApiMock.api = EthApiCore(apiCore: apiCoreMock) + ethApiMock.web3 = web3 + session = makeSession() + web3ProviderMock.session = session + let store = try XCTUnwrap(try makeKeystore()) + + walletStorage = .init(keystore: store, unicId: "ERC20ETH") + keystoreManager = .init([store]) + increaseFeeServiceMock = IncreaseFeeServiceMock() + + sut = EthWalletService() + sut.increaseFeeService = increaseFeeServiceMock + sut.setWalletForTests(walletStorage.getWallet()) + sut.ethApiService = ethApiMock + increaseFeeServiceMock.given(.isIncreaseFeeEnabled(for: .any, willReturn: false)) + } + + override func tearDown() { + sut = nil + apiCoreMock = nil + ethApiMock = nil + walletStorage = nil + web3ProviderMock = nil + increaseFeeServiceMock = nil + web3 = nil + keystoreManager = nil + MockURLProtocol.requestHandler = nil + super.tearDown() + } + + func test_createTransaction_noWalletThrowsError() async throws { + // GIVEN + sut.setWalletForTests(nil) + + // WHEN + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: "recipient", + amount: 10, + fee: 0.1, + comment: nil + ) + }) + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .notLogged) + } + + func test_createTransaction_invalidAddressThrowsError() async throws { + // WHEN + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: Constants.invalidEthAddress, + amount: 10, + fee: 0.1, + comment: nil + ) + }) + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .accountNotFound) + } + + func test_createTransaction_invalidAmountThrowsError() async throws { + // WHEN + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: Constants.toEthAddress, + amount: Decimal.nan, + fee: 0.1, + comment: nil + ) + }) + + // THEN + XCTAssertEqual(result.error as? WalletServiceError, .invalidAmount(.nan)) + } + + func test_createTransaction_noKeystoreManagerThrowsError() async throws { + // WHEN + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: Constants.toEthAddress, + amount: 10, + fee: 0.1, + comment: nil + ) + }) + + // THEN + switch result.error as? WalletServiceError { + case .internalError?: + break + default: + XCTFail("Expected `.internalError`, got :\(String(describing: result.error))") + } + } + + func test_createTransaction_correctFields() async throws { + // GIVEN + await setupKeyStoreManager() + makeTransactionsCountMock() + + // WHEN + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: Constants.toEthAddress, + amount: 10, + fee: 0.1, + comment: nil + ) + }) + + // THEN + XCTAssertNil(result.error) + let transaction = try XCTUnwrap(result.value) + XCTAssertEqual(transaction.from?.address, Constants.ethAddress) + XCTAssertEqual(transaction.to.address, Constants.toEthAddress) + XCTAssertEqual(transaction.nonce, BigUInt(exactly: Constants.nonce)) + XCTAssertEqual(transaction.gasPrice, BigUInt(clamping: 11).toWei()) + XCTAssertEqual(transaction.encode(), Constants.expectedTxData) + XCTAssertEqual(transaction.hashForSignature(), Constants.extectedSignatureHash) + } + + func test_createAndSendTransaction_sendsCorrectData() async throws { + // GIVEN + var didCallSendMock = false + await setupKeyStoreManager() + makeTransactionsCountMock() + + // WHEN + let result = await Result(catchingAsync: { + try await self.sut.createTransaction( + recipient: Constants.toEthAddress, + amount: 10, + fee: 0.1, + comment: nil + ) + }) + + let transaction = try XCTUnwrap(result.value) + let data = try XCTUnwrap(transaction.encode()) + let hash = data.toHexString().addHexPrefix() + makeEthSendMock(expectedHash: hash) { didCallSendMock = true } + + let sendResult = await Result { + try await self.sut.sendTransaction(transaction) + } + + // THEN + XCTAssertNil(sendResult.error) + XCTAssertTrue(ethMock.invokedSendRaw) + XCTAssertTrue(didCallSendMock) + } + + private func setupKeyStoreManager() async { + await ethApiMock.setKeystoreManager(keystoreManager) + web3ProviderMock.attachedKeystoreManager = keystoreManager + } + + private func makeSession() -> URLSession { + let config = URLSessionConfiguration.ephemeral + config.protocolClasses = [MockURLProtocol.self] + return URLSession(configuration: config) + } + + private func makeTransactionsCountMock() { + let prevHandler = MockURLProtocol.requestHandler + MockURLProtocol.requestHandler = MockURLProtocol.combineHandlers( + prevHandler + ) { request in + guard let stream = request.httpBodyStream else { return nil } + + let ethBody = try JSONDecoder().decode(EthRequestBody.self, from: Data(reading: stream)) + guard ethBody.method == Constants.transactionCountResponseMethod else { return nil } + + return try self.makeResponseAndMockData( + url: request.url!, + ethResponse: EthAPIResponse(result: "\(Constants.nonce)") + ) + } + } + + private func makeEthSendMock(expectedHash: String, _ onCall: @escaping () -> Void) { + let prevHandler = MockURLProtocol.requestHandler + MockURLProtocol.requestHandler = MockURLProtocol.combineHandlers( + prevHandler + ) { request in + guard let stream = request.httpBodyStream else { return nil } + + let ethBody = try JSONDecoder().decode(EthRequestBody.self, from: Data(reading: stream)) + guard ethBody.method == Constants.sendTransactionResponseMethod else { return nil } + + XCTAssertEqual(ethBody.params[0] as? String, expectedHash) + onCall() + return try self.makeResponseAndMockData( + url: request.url!, + ethResponse: EthAPIResponse(result: expectedHash) + ) + } + } + + private func makeResponseAndMockData( + url: URL, + ethResponse: EthAPIResponse + ) throws -> (HTTPURLResponse, Data) { + let response = HTTPURLResponse( + url: url, + statusCode: 200, + httpVersion: nil, + headerFields: nil + )! + + let mockData = try JSONEncoder().encode(ethResponse) + return (response, mockData) + } + + private func makeKeystore() throws -> BIP32Keystore? { + try BIP32Keystore( + mnemonics: "village lunch say patrol glow first hurt shiver name method dolphin sample", + password: EthWalletService.walletPassword, + mnemonicsPassword: "", + language: .english, + prefixPath: EthWalletService.walletPath + ) + } +} + +private enum Constants { + static let transactionCountResponseMethod = "eth_getTransactionCount" + static let sendTransactionResponseMethod = "eth_sendRawTransaction" + + static let extectedSignatureHash = Data([ + 96, 116, 68, 167, 52, 14, 211, 94, 127, 63, 95, 104, 12, 204, 124, + 91, 13, 9, 179, 80, 252, 71, 102, 108, 115, 61, 254, 105, 0, 115, 81, 242 + ]) + static let expectedTxData = Data([ + 248, 108, 2, 133, 2, 143, 166, 174, 0, 130, 94, 136, 148, 171, 253, 245, + 5, 255, 213, 88, 125, 158, 119, 7, 223, 180, 127, 69, 175, 31, 3, 226, 117, + 136, 138, 199, 35, 4, 137, 232, 0, 0, 128, 36, 160, 142, 188, 94, 138, 169, + 58, 131, 110, 28, 87, 29, 119, 40, 126, 98, 187, 13, 210, 2, 58, 244, 151, 234, + 149, 49, 14, 49, 195, 53, 129, 29, 40, 160, 79, 100, 92, 44, 207, 73, 57, 12, 184, + 108, 82, 24, 242, 197, 97, 151, 32, 107, 254, 152, 1, 73, 147, 254, 141, 113, 51, + 45, 211, 225, 3, 2 + ]) + + static let nonce = 2 + + static let ethAddress = "0xBA5CE20aE344CDBd6eAA01ffdDF1976d35Be142d" + static let toEthAddress = "0xabfDF505fFd5587D9E7707dFB47F45AF1f03E275" + static let invalidEthAddress = "0xBA5CE20aE344CDBd6eAA01ffdDF1976d35Be14" +} diff --git a/AdamantTests/Modules/Wallets/NativeAdamantCoreTests.swift b/AdamantTests/Modules/Wallets/NativeAdamantCoreTests.swift new file mode 100644 index 000000000..4694b248d --- /dev/null +++ b/AdamantTests/Modules/Wallets/NativeAdamantCoreTests.swift @@ -0,0 +1,69 @@ +// +// NativeAdamantCoreTests.swift +// Adamant +// +// Created by Christian Benua on 30.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import CommonKit +import XCTest + +final class NativeAdamantCoreTests: XCTestCase { + func test_encodeMessage() throws { + let (encryptedMessage, nonce) = try XCTUnwrap( + NativeAdamantCore().encodeMessage( + Constants.message, + recipientPublicKey: Constants.publicKey, + privateKey: Constants.privateKey + ) + ) + + let message = NativeAdamantCore().decodeMessage( + rawMessage: encryptedMessage, + rawNonce: nonce, + senderPublicKey: Constants.publicKey, + privateKey: Constants.privateKey + ) + + XCTAssertEqual(message, Constants.message) + } + + func test_sign() { + let signature = NativeAdamantCore().sign( + transaction: Constants.transaction, + senderId: Constants.senderPublicKey, + keypair: Keypair(publicKey: Constants.senderPublicKey, privateKey: Constants.privateKey) + ) + + XCTAssertEqual(signature, Constants.expectedSignature) + } +} + +private enum Constants { + static let privateKey = "c2b4df1562b93e5e37bef8551d430a21736da9b021cad8e3eec54cfca05b8db2fdfa0ad06afc6445c8d2c63078cba7f6d079ee0367e764cf286f42ab955a4d67" + static let publicKey = "1ed651ec1c686c23249dadb2cb656edd5f8e7d35076815d8a81c395c3eed1a85" + static let message = "Sample encode message for testing" + + static let expectedSignature = + "328870db4ae22f0b74c829a32ae16ed3191d6dbfb13077f2ec0b35ee005270d13ff0f615de7d9a5486c4f7a66d41c99e814a7e7d2a8611d140476573266e2503" + + static let transaction = NormalizedTransaction( + type: .chatMessage, + amount: 0, + senderPublicKey: Constants.senderPublicKey, + requesterPublicKey: nil, + date: Date(timeIntervalSince1970: 1_738_267_672), + recipientId: "U3716604363012166999", + asset: TransactionAsset( + chat: ChatAsset( + message: "1507aaf7fdf4bdea3cf4e4df3d2476962be70102e2cac0961c25c1642713e2e14a0c8bf02e7f", + ownMessage: "c488a53dff457feb4e46d83ae26c3821b9aa10d06a727eb5", + type: .message + ) + ) + ) + + static let senderId = "U12686887375123482464" + static let senderPublicKey = "fdfa0ad06afc6445c8d2c63078cba7f6d079ee0367e764cf286f42ab955a4d67" +} diff --git a/AdamantTests/Stubs/BtcApiServiceProtocolMock.swift b/AdamantTests/Stubs/BtcApiServiceProtocolMock.swift new file mode 100644 index 000000000..80cddd38e --- /dev/null +++ b/AdamantTests/Stubs/BtcApiServiceProtocolMock.swift @@ -0,0 +1,42 @@ +// +// BtcApiServiceProtocol.swift +// Adamant +// +// Created by Christian Benua on 09.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import CommonKit +import Foundation + +@testable import Adamant + +final class BtcApiServiceProtocolMock: BtcApiServiceProtocol { + + var api: BtcApiCore! + + func request( + waitsForConnectivity: Bool, + _ request: @Sendable @escaping (APICoreProtocol, NodeOrigin) async -> ApiServiceResult + ) async -> WalletServiceResult { + await api.request(origin: .mock) { core, origin in + await request(core, origin) + } + } + + func getStatusInfo() async -> WalletServiceResult { + return .failure(.networkError) + } + + var nodesInfo: CommonKit.NodesListInfo { + fatalError("\(#file).\(#function) is not implemented") + } + + var nodesInfoPublisher: CommonKit.AnyObservable { + fatalError("\(#file).\(#function) is not implemented") + } + + func healthCheck() { + fatalError("\(#file).\(#function) is not implemented") + } +} diff --git a/AdamantTests/Stubs/DashApiServiceProtocolMock.swift b/AdamantTests/Stubs/DashApiServiceProtocolMock.swift new file mode 100644 index 000000000..be443bcd6 --- /dev/null +++ b/AdamantTests/Stubs/DashApiServiceProtocolMock.swift @@ -0,0 +1,41 @@ +// +// DashApiServiceProtocolMock.swift +// Adamant +// +// Created by Christian Benua on 23.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import CommonKit +import Foundation + +@testable import Adamant + +final class DashApiServiceProtocolMock: DashApiServiceProtocol { + var api: DashApiCore! + + func request( + waitsForConnectivity: Bool, + _ request: @Sendable @escaping (APICoreProtocol, NodeOrigin) async -> ApiServiceResult + ) async -> WalletServiceResult { + await api.request(origin: .mock) { core, origin in + await request(core, origin) + } + } + + func getStatusInfo() async -> WalletServiceResult { + return .failure(.networkError) + } + + var nodesInfo: CommonKit.NodesListInfo { + fatalError("\(#file).\(#function) is not implemented") + } + + var nodesInfoPublisher: CommonKit.AnyObservable { + fatalError("\(#file).\(#function) is not implemented") + } + + func healthCheck() { + fatalError("\(#file).\(#function) is not implemented") + } +} diff --git a/AdamantTests/Stubs/DogeApiServiceProtocolMock.swift b/AdamantTests/Stubs/DogeApiServiceProtocolMock.swift new file mode 100644 index 000000000..2ef83643e --- /dev/null +++ b/AdamantTests/Stubs/DogeApiServiceProtocolMock.swift @@ -0,0 +1,53 @@ +// +// DogeApiServiceProtocolMock.swift +// Adamant +// +// Created by Christian Benua on 17.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import CommonKit +import Foundation + +@testable import Adamant + +final class DogeApiServiceProtocolMock: DogeApiServiceProtocol, DogeInternalApiProtocol { + + var api: DogeInternalApiProtocol { + self + } + + var _api: DogeApiCore! + + func request( + waitsForConnectivity: Bool, + _ request: @Sendable @escaping (APICoreProtocol, NodeOrigin) async -> ApiServiceResult + ) async -> WalletServiceResult { + await _api.request(origin: NodeOrigin(url: URL(string: "http://samplenodeorigin.com")!)) { core, origin in + await request(core, origin) + } + } + + func request( + waitsForConnectivity: Bool, + _ requestAction: @Sendable (DogeApiCore, NodeOrigin) async -> WalletServiceResult + ) async -> WalletServiceResult { + await requestAction(_api, NodeOrigin(url: URL(string: "http://samplenodeorigin.com")!)) + } + + func getStatusInfo() async -> WalletServiceResult { + return .failure(.networkError) + } + + var nodesInfo: NodesListInfo { + fatalError("\(#file).\(#function) is not implemented") + } + + var nodesInfoPublisher: AnyObservable { + fatalError("\(#file).\(#function) is not implemented") + } + + func healthCheck() { + fatalError("\(#file).\(#function) is not implemented") + } +} diff --git a/AdamantTests/Stubs/ERC20ApiServiceProtocolMock.swift b/AdamantTests/Stubs/ERC20ApiServiceProtocolMock.swift new file mode 100644 index 000000000..998901eb9 --- /dev/null +++ b/AdamantTests/Stubs/ERC20ApiServiceProtocolMock.swift @@ -0,0 +1,71 @@ +// +// ERC20ApiServiceProtocolMock.swift +// Adamant +// +// Created by Christian Benua on 25.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import CommonKit +import Foundation +@preconcurrency import Web3Core +import web3swift + +@testable import Adamant + +final class ERC20ApiServiceProtocolMock: ERC20ApiServiceProtocol { + + var keystoreManager: KeystoreManager? + var api: EthApiCore! + var web3: Web3! + var contractAddress: EthereumAddress! + + func requestERC20( + token: ERC20Token, + _ body: @escaping @Sendable (ERC20) async throws -> Output + ) async -> WalletServiceResult { + await api.performRequest( + origin: .mock + ) { _ in + let erc20 = ERC20(web3: self.web3, provider: self.web3.provider, address: self.contractAddress) + return try await body(erc20) + } + } + + func requestWeb3( + waitsForConnectivity: Bool, + _ request: @escaping @Sendable (Web3) async throws -> Output + ) async -> WalletServiceResult { + await api.performRequest( + origin: .mock + ) { _ in + try await request(self.web3) + } + } + + func requestApiCore( + waitsForConnectivity: Bool, + _ request: @escaping @Sendable (APICoreProtocol, NodeOrigin) async -> ApiServiceResult + ) async -> WalletServiceResult { + await request( + api.apiCore, + .mock + ).mapError { $0.asWalletServiceError() } + } + + func setKeystoreManager(_ keystoreManager: KeystoreManager) async { + await api.setKeystoreManager(keystoreManager) + } + + var nodesInfo: CommonKit.NodesListInfo { + fatalError("\(#file).\(#function) is not implemented") + } + + var nodesInfoPublisher: CommonKit.AnyObservable { + fatalError("\(#file).\(#function) is not implemented") + } + + func healthCheck() { + fatalError("\(#file).\(#function) is not implemented") + } +} diff --git a/AdamantTests/Stubs/EthApiServiceProtocolMock.swift b/AdamantTests/Stubs/EthApiServiceProtocolMock.swift new file mode 100644 index 000000000..e967f58d8 --- /dev/null +++ b/AdamantTests/Stubs/EthApiServiceProtocolMock.swift @@ -0,0 +1,57 @@ +// +// EthApiServiceProtocolMock.swift +// Adamant +// +// Created by Christian Benua on 15.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import CommonKit +import Foundation +@preconcurrency import Web3Core +import web3swift + +@testable import Adamant + +final class EthApiServiceProtocolMock: EthApiServiceProtocol { + + var api: EthApiCore! + var web3: Web3! + + func requestWeb3( + waitsForConnectivity: Bool, + _ request: @escaping @Sendable (Web3) async throws -> Output + ) async -> WalletServiceResult { + await api.performRequest( + origin: .mock + ) { _ in + try await request(self.web3) + } + } + + func requestApiCore( + waitsForConnectivity: Bool, + _ request: @escaping @Sendable (APICoreProtocol, NodeOrigin) async -> ApiServiceResult + ) async -> WalletServiceResult { + await request( + api.apiCore, + .mock + ).mapError { $0.asWalletServiceError() } + } + + func setKeystoreManager(_ keystoreManager: KeystoreManager) async { + await api.setKeystoreManager(keystoreManager) + } + + var nodesInfo: CommonKit.NodesListInfo { + fatalError("\(#file).\(#function) is not implemented") + } + + var nodesInfoPublisher: CommonKit.AnyObservable { + fatalError("\(#file).\(#function) is not implemented") + } + + func healthCheck() { + fatalError("\(#file).\(#function) is not implemented") + } +} diff --git a/AdamantTests/Stubs/EthRequestBody.swift b/AdamantTests/Stubs/EthRequestBody.swift new file mode 100644 index 000000000..63dbce89e --- /dev/null +++ b/AdamantTests/Stubs/EthRequestBody.swift @@ -0,0 +1,25 @@ +// +// EthRequestBody.swift +// Adamant +// +// Created by Christian Benua on 16.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import Web3Core + +struct EthRequestBody: Decodable { + var method: String + var params: [Any] + + enum CodingKeys: String, CodingKey { + case method + case params + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.method = try container.decode(String.self, forKey: .method) + self.params = try container.decode([Any].self, forKey: .params) + } +} diff --git a/AdamantTests/Stubs/EthResponseBody.swift b/AdamantTests/Stubs/EthResponseBody.swift new file mode 100644 index 000000000..a1770ab26 --- /dev/null +++ b/AdamantTests/Stubs/EthResponseBody.swift @@ -0,0 +1,19 @@ +// +// EthResponseBody.swift +// Adamant +// +// Created by Christian Benua on 16.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +struct EthAPIResponse: Codable { + var id: Int = 1 + var jsonrpc = "2.0" + var result: Result + + init(id: Int = 1, jsonrpc: String = "2.0", result: Result) { + self.id = id + self.jsonrpc = jsonrpc + self.result = result + } +} diff --git a/AdamantTests/Stubs/IEthMock.swift b/AdamantTests/Stubs/IEthMock.swift new file mode 100644 index 000000000..14d460b41 --- /dev/null +++ b/AdamantTests/Stubs/IEthMock.swift @@ -0,0 +1,119 @@ +// +// IEthMock.swift +// Adamant +// +// Created by Christian Benua on 15.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import BigInt +import Foundation +import Web3Core +import web3swift + +@testable import Adamant + +final class IEthMock: IEth { + var provider: Web3Provider { + _provider + } + + var _provider: Web3Provider! { + didSet { + ethDefault._provider = _provider + } + } + + var ethDefault: EthDefault = EthDefault() + + var invokedCallTransaction: Bool = false + var invokedCallTransactionParameters: CodableTransaction? + + func callTransaction(_ transaction: CodableTransaction) async throws -> Data { + invokedCallTransaction = true + invokedCallTransactionParameters = transaction + + return try await ethDefault.callTransaction(transaction) + } + + func send(_ transaction: CodableTransaction) async throws -> TransactionSendingResult { + fatalError("\(#file).\(#function) is not implemented") + } + + var invokedSendRaw: Bool = false + var invokedSendRawParameters: Data? + + func send(raw data: Data) async throws -> TransactionSendingResult { + invokedSendRaw = true + invokedSendRawParameters = data + return try await ethDefault.send(raw: data) + } + + var stubbedEstimateGas: BigUInt = BigUInt(clamping: 22000) + + func estimateGas(for transaction: CodableTransaction, onBlock: BlockNumber) async throws -> BigUInt { + return stubbedEstimateGas + } + + func feeHistory(blockCount: BigUInt, block: BlockNumber, percentiles: [Double]) async throws -> Oracle.FeeHistory { + fatalError("\(#file).\(#function) is not implemented") + } + + func ownedAccounts() async throws -> [EthereumAddress] { + fatalError("\(#file).\(#function) is not implemented") + } + + func getBalance(for address: EthereumAddress, onBlock: BlockNumber) async throws -> BigUInt { + fatalError("\(#file).\(#function) is not implemented") + } + + func block(by hash: Data, fullTransactions: Bool) async throws -> Block { + fatalError("\(#file).\(#function) is not implemented") + } + + func block(by number: BlockNumber, fullTransactions: Bool) async throws -> Block { + fatalError("\(#file).\(#function) is not implemented") + } + + func block(by hash: Hash, fullTransactions: Bool) async throws -> Block { + fatalError("\(#file).\(#function) is not implemented") + } + + func blockNumber() async throws -> BigUInt { + fatalError("\(#file).\(#function) is not implemented") + } + + func code(for address: EthereumAddress, onBlock: BlockNumber) async throws -> Hash { + fatalError("\(#file).\(#function) is not implemented") + } + + func getLogs(eventFilter: EventFilterParameters) async throws -> [EventLog] { + fatalError("\(#file).\(#function) is not implemented") + } + + var stubbedGasPrice: BigUInt = BigUInt(clamping: 10).toWei() + + func gasPrice() async throws -> BigUInt { + return stubbedGasPrice + } + + func getTransactionCount(for address: EthereumAddress, onBlock: BlockNumber) async throws -> BigUInt { + fatalError("\(#file).\(#function) is not implemented") + } + + func transactionDetails(_ txHash: Data) async throws -> Web3Core.TransactionDetails { + fatalError("\(#file).\(#function) is not implemented") + } + + func transactionReceipt(_ txHash: Data) async throws -> TransactionReceipt { + fatalError("\(#file).\(#function) is not implemented") + } +} + +final class EthDefault: IEth { + var provider: Web3Provider { + _provider + } + + var _provider: Web3Provider! +} diff --git a/AdamantTests/Stubs/KlyTransactionSubmitModel.swift b/AdamantTests/Stubs/KlyTransactionSubmitModel.swift new file mode 100644 index 000000000..ad6c33bae --- /dev/null +++ b/AdamantTests/Stubs/KlyTransactionSubmitModel.swift @@ -0,0 +1,11 @@ +// +// KlyTransactionSubmitModel.swift +// Adamant +// +// Created by Christian Benua on 22.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +struct KlyTransactionSubmitModel: Codable { + let transactionId: String +} diff --git a/AdamantTests/Stubs/RequestStubs/dash_send_transaction_unit_response.json b/AdamantTests/Stubs/RequestStubs/dash_send_transaction_unit_response.json new file mode 100644 index 000000000..131f40999 --- /dev/null +++ b/AdamantTests/Stubs/RequestStubs/dash_send_transaction_unit_response.json @@ -0,0 +1,3 @@ +{ + "result": "txid" +} diff --git a/AdamantTests/Stubs/RequestStubs/dash_send_transation_response.json b/AdamantTests/Stubs/RequestStubs/dash_send_transation_response.json new file mode 100644 index 000000000..2b5d24d7a --- /dev/null +++ b/AdamantTests/Stubs/RequestStubs/dash_send_transation_response.json @@ -0,0 +1,3 @@ +{ + "result": "d4cf3fde45d0e7ba855db9621bdc6da091856011d86f199bebd1937f7b63020a" +} diff --git a/AdamantTests/Stubs/RequestStubs/dash_unspent_transaction_intergration_test.json b/AdamantTests/Stubs/RequestStubs/dash_unspent_transaction_intergration_test.json new file mode 100644 index 000000000..73953b6c8 --- /dev/null +++ b/AdamantTests/Stubs/RequestStubs/dash_unspent_transaction_intergration_test.json @@ -0,0 +1,10 @@ +{ + "result": [{ + "txid":"a2c3b490c839cbad1901248b1186b057809060afa5fe208acb4a1237240c1f72", + "address": "Xp6kFbogHMD4QRBDLQdqRp5zUgzmfj1KPn", + "outputIndex": 1, + "script": "76a914931ef5cbdad28723ba9596de5da1145ae969a71888ac", + "satoshis": 96000000, + "height": 2209770 + }] +} diff --git a/AdamantTests/Stubs/RequestStubs/dash_unspent_transaction_unit_test.json b/AdamantTests/Stubs/RequestStubs/dash_unspent_transaction_unit_test.json new file mode 100644 index 000000000..e5a50bb12 --- /dev/null +++ b/AdamantTests/Stubs/RequestStubs/dash_unspent_transaction_unit_test.json @@ -0,0 +1,26 @@ +{ + "result": [ + { + "txid": "1", + "script": "some script", + "address": "some address", + "outputIndex": 1, + "satoshis": 30, + "height": 1, + "status": { + "confirmed": true + } + }, + { + "txid": "1", + "script": "some script", + "address": "some address", + "outputIndex": 2, + "satoshis": 20, + "height": 1, + "status": { + "confirmed": true + } + } + ] +} diff --git a/AdamantTests/Stubs/RequestStubs/dash_unverified_unspent_transactions.json b/AdamantTests/Stubs/RequestStubs/dash_unverified_unspent_transactions.json new file mode 100644 index 000000000..7d6bffc7d --- /dev/null +++ b/AdamantTests/Stubs/RequestStubs/dash_unverified_unspent_transactions.json @@ -0,0 +1,12 @@ +{ + "id": "some id", + "result": { + "txid": "some txid", + "confirmations": 0, + "hash": "some hash", + "valueIn": 1, + "valueOut": 0.95, + "vin": [], + "vout": [] + } +} diff --git a/AdamantTests/Stubs/RpsRequestBody.swift b/AdamantTests/Stubs/RpsRequestBody.swift new file mode 100644 index 000000000..2ed20eb87 --- /dev/null +++ b/AdamantTests/Stubs/RpsRequestBody.swift @@ -0,0 +1,23 @@ +// +// RpsRequestBody.swift +// Adamant +// +// Created by Christian Benua on 22.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +struct RpcRequestBody: Decodable { + var method: String + var params: [String: Any] + + enum CodingKeys: String, CodingKey { + case method + case params + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.method = try container.decode(String.self, forKey: .method) + self.params = try container.decode([String: Any].self, forKey: .params) + } +} diff --git a/AdamantTests/Stubs/Web3ProviderMock.swift b/AdamantTests/Stubs/Web3ProviderMock.swift new file mode 100644 index 000000000..830f43cda --- /dev/null +++ b/AdamantTests/Stubs/Web3ProviderMock.swift @@ -0,0 +1,36 @@ +// +// Web3ProviderMock.swift +// Adamant +// +// Created by Christian Benua on 15.01.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import Foundation +import Web3Core + +final class Web3ProviderMock: Web3Provider { + var network: Networks? + var attachedKeystoreManager: KeystoreManager? + var policies: Policies + var url: URL + var session: URLSession + + init( + network: Networks? = nil, + attachedKeystoreManager: KeystoreManager? = nil, + policies: Policies = .auto, + url: URL = Web3ProviderMock.defaultURL, + session: URLSession = .shared + ) { + self.network = network + self.attachedKeystoreManager = attachedKeystoreManager + self.policies = policies + self.url = url + self.session = session + } +} + +extension Web3ProviderMock { + private static let defaultURL = URL(string: "http://google.com")! +} diff --git a/AdamantWalletsKit/.gitignore b/AdamantWalletsKit/.gitignore new file mode 100644 index 000000000..0023a5340 --- /dev/null +++ b/AdamantWalletsKit/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/AdamantWalletsKit/Package.swift b/AdamantWalletsKit/Package.swift new file mode 100644 index 000000000..cfa7eac55 --- /dev/null +++ b/AdamantWalletsKit/Package.swift @@ -0,0 +1,27 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "AdamantWalletsKit", + platforms: [ + .iOS(.v15), .macOS(.v10_15) + ], + products: [ + .library( + name: "AdamantWalletsKit", + targets: ["AdamantWalletsKit"] + ) + ], + targets: [ + .target( + name: "AdamantWalletsKit", + resources: [ + .copy("JsonStore/general"), + .copy("JsonStore/blockchains"), + .process("Wallets.xcassets") + ] + ) + ] +) diff --git a/AdamantWalletsKit/Scripts/CopyScript.sh b/AdamantWalletsKit/Scripts/CopyScript.sh new file mode 100755 index 000000000..65fbb5c3f --- /dev/null +++ b/AdamantWalletsKit/Scripts/CopyScript.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +# Define the base project directory (the folder where this script is located) +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +PROJECT_ROOT=$(cd "$SCRIPT_DIR/../.." && pwd) # Project root directory + +# Package details +PACKAGE_NAME="adamant-wallets" +GITHUB_REPO="https://github.com/Adamant-im/adamant-wallets.git" +TARGET_DIR="$PROJECT_ROOT/$PACKAGE_NAME" + +# Remove the old version and clone the new one +if [ -d "$TARGET_DIR" ]; then + echo "Removing old version of $PACKAGE_NAME..." + rm -rf "$TARGET_DIR" +fi + +echo "Cloning $PACKAGE_NAME from GitHub..." +git clone --depth 1 --branch master "$GITHUB_REPO" "$TARGET_DIR" + +# Check if cloning was successful +if [ ! -d "$TARGET_DIR" ]; then + echo "Error: Failed to download $PACKAGE_NAME from GitHub." + exit 1 +fi + +# Define paths for required directories +ASSETS_PATH="$TARGET_DIR/assets" +ADAMANTWALLETSKIT_PATH="$PROJECT_ROOT/AdamantWalletsKit" +JSON_STORE_PATH="$ADAMANTWALLETSKIT_PATH/Sources/AdamantWalletsKit/JsonStore" +TEMP_ASSETS_PATH="$ADAMANTWALLETSKIT_PATH/Sources/AdamantWalletsKit/TemporaryAssets" + +# Clear and create necessary directories +rm -rf "$JSON_STORE_PATH" "$TEMP_ASSETS_PATH" +mkdir -p "$JSON_STORE_PATH" "$TEMP_ASSETS_PATH" + +# Copy blockchain assets +cp -R "$ASSETS_PATH/blockchains" "$JSON_STORE_PATH" + +# Process the "general" folder +GENERAL_PATH="$ASSETS_PATH/general" +if [ -d "$GENERAL_PATH" ]; then + echo "Processing 'general' folder..." + + for TOKEN_FOLDER in "$GENERAL_PATH"/*; do + if [ -d "$TOKEN_FOLDER" ]; then + TOKEN_NAME=$(basename "$TOKEN_FOLDER") + + # Create directories + mkdir -p "$JSON_STORE_PATH/general/$TOKEN_NAME" + mkdir -p "$TEMP_ASSETS_PATH/general/$TOKEN_NAME" + + # Copy JSON files + find "$TOKEN_FOLDER" -maxdepth 1 -type f -name "*.json" -exec cp {} "$JSON_STORE_PATH/general/$TOKEN_NAME" \; + + # Copy images folder + if [ -d "$TOKEN_FOLDER/images" ]; then + cp -R "$TOKEN_FOLDER/images" "$TEMP_ASSETS_PATH/general/$TOKEN_NAME" + fi + fi + done + + echo "Processing of 'general' folder completed." +else + echo "Error: 'general' folder not found at path $GENERAL_PATH." + exit 1 +fi + +# Remove the downloaded package after processing +echo "Cleaning up: Removing downloaded package $PACKAGE_NAME..." +rm -rf "$TARGET_DIR" + +echo "Script execution completed successfully." diff --git a/AdamantWalletsKit/Scripts/DeleteTempraryAssets.sh b/AdamantWalletsKit/Scripts/DeleteTempraryAssets.sh new file mode 100755 index 000000000..6088f15c9 --- /dev/null +++ b/AdamantWalletsKit/Scripts/DeleteTempraryAssets.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Automatically determine the root of the project +ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) + +# Path to the TemporaryAssets folder +TEMP_ASSETS_PATH="$ROOT/AdamantWalletsKit/Sources/AdamantWalletsKit/TemporaryAssets" + +echo "ROOT: $ROOT" +echo "TEMP_ASSETS_PATH: $TEMP_ASSETS_PATH" + +# Check if the TemporaryAssets directory exists +if [ -d "$TEMP_ASSETS_PATH" ]; then + echo "Removing TemporaryAssets folder: $TEMP_ASSETS_PATH..." + rm -rf "$TEMP_ASSETS_PATH" + echo "TemporaryAssets folder has been successfully removed." +else + echo "TemporaryAssets folder does not exist: $TEMP_ASSETS_PATH" +fi diff --git a/AdamantWalletsKit/Scripts/GenerateAssetsScript.sh b/AdamantWalletsKit/Scripts/GenerateAssetsScript.sh new file mode 100755 index 000000000..6e8efc9be --- /dev/null +++ b/AdamantWalletsKit/Scripts/GenerateAssetsScript.sh @@ -0,0 +1,137 @@ +#!/bin/bash + +# Automatically determine the root of the project +ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd) + +# Paths +TEMP_ASSETS_PATH="$ROOT/AdamantWalletsKit/Sources/AdamantWalletsKit/TemporaryAssets/General" +WALLETS_ASSETS_PATH="$ROOT/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets" +NOTIFICATION_IMAGES_PATH="$ROOT/NotificationServiceExtension/WalletImages" + +echo "ROOT: $ROOT" +echo "TEMP_ASSETS_PATH: $TEMP_ASSETS_PATH" +echo "WALLETS_ASSETS_PATH: $WALLETS_ASSETS_PATH" +echo "NOTIFICATION_IMAGES_PATH: $NOTIFICATION_IMAGES_PATH" + +# Remove old asset folders +rm -rf "$NOTIFICATION_IMAGES_PATH" "$WALLETS_ASSETS_PATH" +mkdir -p "$NOTIFICATION_IMAGES_PATH" "$WALLETS_ASSETS_PATH" + +echo "Created new WalletImages and Wallets.xcassets folders." + +# Function to create Contents.json +function create_contents { + TARGET=$1 + IMAGE_NAME=$2 + WITH_DARK=$3 + + echo "Generating Contents.json for $TARGET..." + + if [ "$WITH_DARK" = true ]; then + cat > "${TARGET}/Contents.json" << __EOF__ +{ + "images" : [ + { "filename" : "${IMAGE_NAME}.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "${IMAGE_NAME}@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "${IMAGE_NAME}@3x.png", "idiom" : "universal", "scale" : "3x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "${IMAGE_NAME}_dark.png", "idiom": "universal", "scale": "1x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "${IMAGE_NAME}_dark@2x.png", "idiom": "universal", "scale": "2x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "${IMAGE_NAME}_dark@3x.png", "idiom": "universal", "scale": "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} +__EOF__ + else + cat > "${TARGET}/Contents.json" << __EOF__ +{ + "images" : [ + { "filename" : "${IMAGE_NAME}.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "${IMAGE_NAME}@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "${IMAGE_NAME}@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} +__EOF__ + fi +} + +# Function to copy images with fallback to wallet icons +function copy_images_with_fallback { + SOURCE_DIR=$1 + IMAGE_NAME=$2 + DEST_DIR=$3 + FALLBACK_IMAGE_NAME=$4 + + mkdir -p "$DEST_DIR" + + # Copy regular images with fallback + cp "$SOURCE_DIR/${IMAGE_NAME}.png" "$DEST_DIR/${IMAGE_NAME}.png" 2>/dev/null || cp "$SOURCE_DIR/${FALLBACK_IMAGE_NAME}.png" "$DEST_DIR/${IMAGE_NAME}.png" 2>/dev/null + cp "$SOURCE_DIR/${IMAGE_NAME}@2x.png" "$DEST_DIR/${IMAGE_NAME}@2x.png" 2>/dev/null || cp "$SOURCE_DIR/${FALLBACK_IMAGE_NAME}@2x.png" "$DEST_DIR/${IMAGE_NAME}@2x.png" 2>/dev/null + cp "$SOURCE_DIR/${IMAGE_NAME}@3x.png" "$DEST_DIR/${IMAGE_NAME}@3x.png" 2>/dev/null || cp "$SOURCE_DIR/${FALLBACK_IMAGE_NAME}@3x.png" "$DEST_DIR/${IMAGE_NAME}@3x.png" 2>/dev/null + + # Copy dark mode images with fallback + cp "$SOURCE_DIR/${IMAGE_NAME}_dark.png" "$DEST_DIR/${IMAGE_NAME}_dark.png" 2>/dev/null || cp "$SOURCE_DIR/${FALLBACK_IMAGE_NAME}_dark.png" "$DEST_DIR/${IMAGE_NAME}_dark.png" 2>/dev/null + cp "$SOURCE_DIR/${IMAGE_NAME}_dark@2x.png" "$DEST_DIR/${IMAGE_NAME}_dark@2x.png" 2>/dev/null || cp "$SOURCE_DIR/${FALLBACK_IMAGE_NAME}_dark@2x.png" "$DEST_DIR/${IMAGE_NAME}_dark@2x.png" 2>/dev/null + cp "$SOURCE_DIR/${IMAGE_NAME}_dark@3x.png" "$DEST_DIR/${IMAGE_NAME}_dark@3x.png" 2>/dev/null || cp "$SOURCE_DIR/${FALLBACK_IMAGE_NAME}_dark@3x.png" "$DEST_DIR/${IMAGE_NAME}_dark@3x.png" 2>/dev/null +} + +# Process each token in TemporaryAssets/General +function process_tokens { + echo "Processing tokens in $TEMP_ASSETS_PATH..." + + for TOKEN_DIR in "$TEMP_ASSETS_PATH"/*; do + if [ -d "$TOKEN_DIR" ]; then + TOKEN_NAME=$(basename "$TOKEN_DIR") + IMAGES_DIR="$TOKEN_DIR/images" + + echo "Processing token: $TOKEN_NAME" + echo "IMAGES_DIR: $IMAGES_DIR" + + # Skip if no images directory exists + if [ ! -d "$IMAGES_DIR" ]; then + echo "Skipping $TOKEN_NAME: no images directory found." + continue + fi + + # Function to process an image set + function process_image_set { + TYPE=$1 + FALLBACK_TYPE=$2 + TARGET_PATH="$WALLETS_ASSETS_PATH/${TOKEN_NAME}_${TYPE}.imageset" + IMAGE_BASE_NAME="${TOKEN_NAME}_${TYPE}" + + mkdir -p "$TARGET_PATH" + echo "Creating $TYPE imageset: $TARGET_PATH" + + # Copy images, using wallet images as fallback + copy_images_with_fallback "$IMAGES_DIR" "$IMAGE_BASE_NAME" "$TARGET_PATH" "${TOKEN_NAME}_${FALLBACK_TYPE}" + + # Check for dark mode images + WITH_DARK=false + if [ -e "$TARGET_PATH/${IMAGE_BASE_NAME}_dark.png" ]; then + WITH_DARK=true + fi + + # Generate Contents.json + create_contents "$TARGET_PATH" "$IMAGE_BASE_NAME" "$WITH_DARK" + } + + # Process wallet, wallet_row (fallback to wallet), and notification (fallback to wallet) + process_image_set "wallet" "wallet" + process_image_set "wallet_row" "wallet" + process_image_set "notification" "wallet" + + # Copy notification content image (only @3x) + if [ -e "$IMAGES_DIR/${TOKEN_NAME}_wallet@3x.png" ]; then + echo "Copying notification content image for $TOKEN_NAME" + cp "$IMAGES_DIR/${TOKEN_NAME}_wallet@3x.png" "$NOTIFICATION_IMAGES_PATH/${TOKEN_NAME}_notificationContent.png" + fi + else + echo "Skipping $TOKEN_DIR: Not a directory" + fi + done +} + +# Main script execution +process_tokens +echo "Asset generation completed!" diff --git a/AdamantWalletsKit/Scripts/RunAllScripts.sh b/AdamantWalletsKit/Scripts/RunAllScripts.sh new file mode 100755 index 000000000..dc91edbb2 --- /dev/null +++ b/AdamantWalletsKit/Scripts/RunAllScripts.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# Define the script names +SCRIPTS=("CopyScript.sh" "GenerateAssetsScript.sh" "DeleteTempraryAssets.sh") + +# Get the directory where the script is located +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +# Iterate over each script and execute it +for script in "${SCRIPTS[@]}"; do + SCRIPT_PATH="$SCRIPT_DIR/$script" + + if [ -f "$SCRIPT_PATH" ]; then + echo "Executing $script..." + bash "$SCRIPT_PATH" + + # Check if the script exited with an error + if [ $? -ne 0 ]; then + echo "Error: $script failed to execute. Stopping execution." + exit 1 + fi + else + echo "Error: $script not found in $SCRIPT_DIR." + exit 1 + fi +done + +echo "All scripts executed successfully." diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Decoders/CoinInfoDTO.swift b/AdamantWalletsKit/Sources/AdamantWalletsKit/Decoders/CoinInfoDTO.swift new file mode 100644 index 000000000..25ece598c --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Decoders/CoinInfoDTO.swift @@ -0,0 +1,106 @@ +import Foundation + +public struct CoinInfoDTO: Codable { + public let name: String + public let nameShort: String? + public let website: String? + public let description: String? + public let explorer: String? + public let explorerTx: String? + public let explorerAddress: String? + public let regexAddress: String? + public let symbol: String + public let type: String + public let decimals: Int + public let cryptoTransferDecimals: Int + public let minBalance: Decimal? + public let minTransferAmount: Double? + public let fixedFee: Decimal? + public let defaultFee: Decimal? + public let qqPrefix: String? + public let status: String + public let createCoin: Bool + public let defaultVisibility: Bool? + public let defaultOrdinalLevel: Int? + public let consensus: String? + public let blockTimeFixed: Int? + public let reliabilityGasPricePercent: Int? + public let reliabilityGasLimitPercent: Int? + public let increasedGasPricePercent: Int? + public let defaultGasPriceGwei: Int? + public let defaultGasLimit: Int? + public let warningGasPriceGwei: Int? + public let blockTimeAvg: Int? + public let nodes: Nodes? + public let services: Services? + public let links: [Link]? + public let tor: Tor? + public let txFetchInfo: TxFetchInfo? + public let timeout: Timeout? + public let contractId: String? + public let txConsistencyMaxTime: Int? + + public struct Node: Codable { + public let url: String + public let altIP: String? + } + + public struct NodeHealthCheck: Codable { + public let normalUpdateInterval: Int + public let crucialUpdateInterval: Int + public let onScreenUpdateInterval: Int + public let threshold: Int? + } + + public struct Service: Codable { + let description: Description + public let list: [Node] + public let healthCheck: NodeHealthCheck? + let minVersion: String? + } + + public struct Description: Codable { + let software: String + let github: String + let docs: String? + } + + public struct Services: Codable { + public let infoService: Service? + public let klyService: Service? + public let ipfsNode: Service? + } + + public struct Tor: Codable { + let website: String? + let explorer: String? + let explorerTx: String? + let explorerAddress: String? + let nodes: Nodes? + let services: Services? + } + + public struct Nodes: Codable { + public let list: [Node] + public let healthCheck: NodeHealthCheck + public let minVersion: String? + } + + public struct TxFetchInfo: Codable { + public let newPendingInterval: Int + public let oldPendingInterval: Int + public let registeredInterval: Int + public let newPendingAttempts: Int? + public let oldPendingAttempts: Int? + } + + public struct Timeout: Codable { + let message: Int + let attachment: Int + } + + public struct Link: Codable { + let name: String + let url: String + } +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Decoders/CoinsMapper.swift b/AdamantWalletsKit/Sources/AdamantWalletsKit/Decoders/CoinsMapper.swift new file mode 100644 index 000000000..9051141d2 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Decoders/CoinsMapper.swift @@ -0,0 +1,265 @@ +// +// CoinsMapper.swift +// AdamantWalletsKit +// +// Created by Sergei Veretennikov on 25.03.2025. +// + +// Everything here goes synchronousely here's no data races or race conditions + +import Foundation + +public enum AnyBlockchain: Hashable { + public enum DeclaredBlockchain: String { + case ethereum + } + + case declared(DeclaredBlockchain) + case undeclared(String) + + public var rawValue: String { + switch self { + case let .declared(chain): + chain.rawValue + case let .undeclared(undeclared): + undeclared + } + } + + init(from raw: String) { + if let declared = DeclaredBlockchain(rawValue: raw) { + self = .declared(declared) + } else { + self = .undeclared(raw) + } + } +} + +public final class BlockchainTokensStorage { + private let chain: AnyBlockchain + var tokens: [String: CoinInfoDTO] = [:] + + init(chain: AnyBlockchain) { + self.chain = chain + } + + func addToken(coin: CoinInfoDTO) { + tokens[coin.symbol] = coin + } + + func getToken(token symbol: String) -> CoinInfoDTO? { + tokens[symbol] + } +} + +public protocol AnyTokensStorage: AnyObject { + subscript(symbol: String) -> CoinInfoDTO? { get } + subscript(symbol: String, chain: AnyBlockchain?) -> CoinInfoDTO? { get } + subscript(chain: AnyBlockchain) -> [String: CoinInfoDTO]? { get } + + func loadTokens() + func getCoin(_ coin: String) -> CoinInfoDTO? + + func getCoinsAndChains() -> (coins: [String: CoinInfoDTO], chains: [AnyBlockchain: BlockchainTokensStorage]) +} + +public final class TokensStorage: AnyTokensStorage { + private var blockchains: [AnyBlockchain] = [] + private var blockchaisTokensStorage: [AnyBlockchain: BlockchainTokensStorage] = [:] + private var coinsStorage: [String: CoinInfoDTO] = [:] + + public init() {} + + /// Coin from storage + public subscript(symbol: String) -> CoinInfoDTO? { + if let coin = getCoin(symbol) { + return coin + } + return nil + } + + /// Get token from storage for specific chain, or try to get coin if there is no chain in storage + public subscript(symbol: String, chain: AnyBlockchain? = nil) -> CoinInfoDTO? { + if let chain { + return blockchaisTokensStorage[chain]?.getToken(token: symbol) + } + if let coin = getCoin(symbol) { + return coin + } + return nil + } + + /// Get list of tokens for chain + public subscript(chain: AnyBlockchain) -> [String: CoinInfoDTO]? { + blockchainTokens(for: chain)?.tokens + } + + public func getCoinsAndChains() -> ( + coins: [String: CoinInfoDTO], + chains: [AnyBlockchain: BlockchainTokensStorage] + ) { + (self.coinsStorage, self.blockchaisTokensStorage) + } + + public func getCoin(_ coin: String) -> CoinInfoDTO? { + coinsStorage[coin] + } + + public func loadTokens() { + loadBlockchains() + loadMainTokensForBlockchains() + loadBaseOfSpecificTokens() + loadCoins() + } + + private func loadCoins() { + guard let counsResource = Bundle.module.url(forResource: "general", withExtension: nil), + let enumerator = FileManager.default.enumerator(at: counsResource, includingPropertiesForKeys: nil) + else { + return + } + + let allGeneral: [String] = enumerator.compactMap { + guard let url = $0 as? URL, url.lastPathComponent == "info.json" else { return nil } + return url.deletingLastPathComponent().lastPathComponent + } + + allGeneral.forEach { + addCoinFromGeneralIfNeeded(folderName: $0) + } + } + + private func addCoinFromGeneralIfNeeded(folderName: String) { + guard let infoJson = Bundle.module.url(forResource: "general/\(folderName)/info", withExtension: "json"), + let data = try? Data(contentsOf: infoJson), + let coinInfo = try? JSONDecoder().decode(CoinInfoDTO.self, from: data), + coinInfo._type == .coin, coinInfo.isActive, coinsStorage[coinInfo.symbol] == nil + else { + return + } + coinsStorage[coinInfo.symbol] = coinInfo + } + + private func loadBlockchains() { + guard let blockchainsResourceURL = Bundle.module.url(forResource: "blockchains", withExtension: nil), + let blockhainsList = FileManager.default.enumerator(at: blockchainsResourceURL, includingPropertiesForKeys: nil) + else { + return + } + + guard let firstFolder = (blockhainsList.nextObject() as? URL) else { return } + blockchains.append(.init(from: firstFolder.lastPathComponent)) + let depth = firstFolder.pathComponents.count + while let element = blockhainsList.nextObject() as? URL { + if depth == element.pathComponents.count { + blockchains.append(.init(from: element.lastPathComponent)) + } + } + } + + private func loadMainTokensForBlockchains() { + blockchains.forEach { + loadMainTokenFor(chain: $0) + } + } + + private func loadBaseOfSpecificTokens() { + blockchains.forEach { chain in + let specificTokenList = tokenListForChain(chain: chain) + for tokenSymbol in specificTokenList { + guard let baseCoin = loadMainTokenFor(chain: chain), + let generalMerge = mergeTokenFrom(path: "general/\(tokenSymbol)", base: baseCoin), + let generalMainCoin = mergeTokenFrom(path: "blockchains/\(chain.rawValue)", base: generalMerge), + let blockchainMerge = mergeTokenFrom(path: "blockchains/\(chain.rawValue)/\(tokenSymbol)", base: generalMainCoin), + blockchainMerge.isActive, blockchainMerge.symbol != baseCoin.symbol + else { + continue + } + + blockchaisTokensStorage[chain]?.addToken(coin: blockchainMerge) + } + } + } + + private func tokenListForChain(chain: AnyBlockchain) -> [String] { + guard let specificBlockchainsResourceURL = Bundle.module.url(forResource: "blockchains/\(chain.rawValue)", withExtension: nil), + let tokensList = FileManager.default.enumerator(at: specificBlockchainsResourceURL, includingPropertiesForKeys: nil) + else { + return [] + } + + let blockchainTokenList: [String] = tokensList.compactMap { token in + guard let token = token as? URL, + token.lastPathComponent == "info.json" + else { return nil } + return token.deletingLastPathComponent().lastPathComponent + } + + return blockchainTokenList + } + + private func mergeTokenFrom(path: String, base: CoinInfoDTO) -> CoinInfoDTO? { + guard let tokenURLGeneral = Bundle.module.url(forResource: "\(path)/info", withExtension: "json"), + let tokenData = try? Data(contentsOf: tokenURLGeneral), + let tokenDict = try? JSONSerialization.jsonObject(with: tokenData) as? [String: Any], + let baseData = try? JSONEncoder().encode(base), + var baseDict = try? JSONSerialization.jsonObject(with: baseData) as? [String: Any] + else { + return nil + } + + tokenDict.forEach { key, value in + baseDict[key] = value + } + + guard let finalData = try? JSONSerialization.data(withJSONObject: baseDict), + let mergedCoin = try? JSONDecoder().decode(CoinInfoDTO.self, from: finalData) + else { + return nil + } + return mergedCoin + } + + @discardableResult + private func loadMainTokenFor(chain: AnyBlockchain) -> CoinInfoDTO? { + guard let mainCoin = Bundle.module.url(forResource: "general/\(chain.rawValue)/info", withExtension: "json"), + let data = try? Data(contentsOf: mainCoin, options: .alwaysMapped) + else { + return nil + } + + guard let baseCoin = try? JSONDecoder().decode(CoinInfoDTO.self, from: data), + let coinInfo = mergeTokenFrom(path: "blockchains/\(chain.rawValue)", base: baseCoin), + coinInfo.isActive + else { return nil } + + if coinsStorage[coinInfo.symbol] == nil { + blockchaisTokensStorage[chain] = .init(chain: chain) + coinsStorage[coinInfo.symbol] = coinInfo + } + return coinInfo + } + + private func blockchainTokens(for blockchain: AnyBlockchain) -> BlockchainTokensStorage? { + blockchaisTokensStorage[blockchain] + } +} + +extension CoinInfoDTO { + fileprivate enum CoinType { + case coin + case token + } + + fileprivate var isActive: Bool { + status == "active" + } + + fileprivate var _type: CoinType? { + switch type { + case "coin": .coin + case "token": .token + default: nil + } + } +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/bnb/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/bnb/info.json new file mode 100644 index 000000000..fae315270 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/bnb/info.json @@ -0,0 +1,7 @@ +{ + "name": "Binance Coin", + "symbol": "BNB", + "status": "active", + "contractId": "0xB8c77482e45F1F44dE1745F52C74426C631bDD52", + "decimals": 18 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/busd/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/busd/info.json new file mode 100644 index 000000000..f9a07b33a --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/busd/info.json @@ -0,0 +1,7 @@ +{ + "name": "Binance USD", + "symbol": "BUSD", + "status": "active", + "contractId": "0x4fabb145d64652a948d72533023f6e7a623c7c53", + "decimals": 18 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/bzz/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/bzz/info.json new file mode 100644 index 000000000..59b9bceda --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/bzz/info.json @@ -0,0 +1,9 @@ +{ + "name": "Swarm", + "symbol": "BZZ", + "status": "active", + "defaultVisibility": false, + "defaultOrdinalLevel": 95, + "contractId": "0x19062190B1925b5b6689D7073fDfC8c2976EF8Cb", + "decimals": 16 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/dai/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/dai/info.json new file mode 100644 index 000000000..f9d0fa6b6 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/dai/info.json @@ -0,0 +1,9 @@ +{ + "name": "Dai", + "symbol": "DAI", + "status": "active", + "defaultVisibility": false, + "defaultOrdinalLevel": 80, + "contractId": "0x6b175474e89094c44da98b954eedeac495271d0f", + "decimals": 18 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/ens/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/ens/info.json new file mode 100644 index 000000000..83f9a2e70 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/ens/info.json @@ -0,0 +1,7 @@ +{ + "name": "Ethereum Name Service", + "symbol": "ENS", + "status": "active", + "contractId": "0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72", + "decimals": 18 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/floki/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/floki/info.json new file mode 100644 index 000000000..3f872e942 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/floki/info.json @@ -0,0 +1,9 @@ +{ + "name": "Floki", + "symbol": "FLOKI", + "status": "active", + "defaultVisibility": false, + "defaultOrdinalLevel": 100, + "contractId": "0xcf0c122c6b73ff809c693db761e7baebe62b6a2e", + "decimals": 9 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/flux/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/flux/info.json new file mode 100644 index 000000000..de24ab143 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/flux/info.json @@ -0,0 +1,9 @@ +{ + "name": "Flux", + "symbol": "FLUX", + "status": "active", + "defaultVisibility": false, + "defaultOrdinalLevel": 90, + "contractId": "0x720CD16b011b987Da3518fbf38c3071d4F0D1495", + "decimals": 8 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/gt/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/gt/info.json new file mode 100644 index 000000000..534d4c2c8 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/gt/info.json @@ -0,0 +1,9 @@ +{ + "name": "Gate", + "symbol": "GT", + "status": "active", + "defaultVisibility": false, + "defaultOrdinalLevel": 115, + "contractId": "0xe66747a101bff2dba3697199dcce5b743b454759", + "decimals": 18 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/hot/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/hot/info.json new file mode 100644 index 000000000..97f9dbd72 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/hot/info.json @@ -0,0 +1,7 @@ +{ + "name": "Holo", + "symbol": "HOT", + "status": "active", + "contractId": "0x6c6ee5e31d828de241282b9606c8e98ea48526e2", + "decimals": 18 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/info.json new file mode 100644 index 000000000..fb8b36d83 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/info.json @@ -0,0 +1,7 @@ +{ + "blockchain": "Ethereum", + "type": "ERC20", + "mainCoin": "ethereum", + "fees": "ethereum", + "defaultGasLimit": 58000 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/inj/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/inj/info.json new file mode 100644 index 000000000..56bf36ec3 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/inj/info.json @@ -0,0 +1,7 @@ +{ + "name": "Injective", + "symbol": "INJ", + "status": "active", + "contractId": "0xe28b3b32b6c345a34ff64674606124dd5aceca30", + "decimals": 18 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/link/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/link/info.json new file mode 100644 index 000000000..7ed4067eb --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/link/info.json @@ -0,0 +1,7 @@ +{ + "name": "Chainlink", + "symbol": "LINK", + "status": "active", + "contractId": "0x514910771af9ca656af840dff83e8264ecf986ca", + "decimals": 18 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/mana/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/mana/info.json new file mode 100644 index 000000000..9b32006e5 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/mana/info.json @@ -0,0 +1,7 @@ +{ + "name": "Decentraland", + "symbol": "MANA", + "status": "active", + "contractId": "0x0f5d2fb29fb7d3cfee444a200298f468908cc942", + "decimals": 18 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/matic/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/matic/info.json new file mode 100644 index 000000000..4e0184e7b --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/matic/info.json @@ -0,0 +1,7 @@ +{ + "name": "Polygon", + "symbol": "MATIC", + "status": "active", + "contractId": "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0", + "decimals": 18 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/paxg/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/paxg/info.json new file mode 100644 index 000000000..5a8eaf7b5 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/paxg/info.json @@ -0,0 +1,7 @@ +{ + "name": "PAX Gold", + "symbol": "PAXG", + "status": "active", + "contractId": "0x45804880de22913dafe09f4980848ece6ecbaf78", + "decimals": 18 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/qnt/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/qnt/info.json new file mode 100644 index 000000000..f52ea1446 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/qnt/info.json @@ -0,0 +1,7 @@ +{ + "name": "Quant", + "symbol": "QNT", + "status": "active", + "contractId": "0x4a220E6096B25EADb88358cb44068A3248254675", + "decimals": 18 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/ren/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/ren/info.json new file mode 100644 index 000000000..c6f4df6b2 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/ren/info.json @@ -0,0 +1,7 @@ +{ + "name": "Ren", + "symbol": "REN", + "status": "active", + "contractId": "0x408e41876cccdc0f92210600ef50372656052a38", + "decimals": 18 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/skl/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/skl/info.json new file mode 100644 index 000000000..a744bc3b9 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/skl/info.json @@ -0,0 +1,9 @@ +{ + "name": "SKALE", + "symbol": "SKL", + "status": "active", + "defaultVisibility": false, + "defaultOrdinalLevel": 85, + "contractId": "0x00c83aecc790e8a4453e5dd3b0b4b3680501a7a7", + "decimals": 18 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/snt/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/snt/info.json new file mode 100644 index 000000000..4ab30013e --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/snt/info.json @@ -0,0 +1,7 @@ +{ + "name": "Status", + "symbol": "SNT", + "status": "active", + "contractId": "0x744d70fdbe2ba4cf95131626614a1763df805b9e", + "decimals": 18 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/snx/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/snx/info.json new file mode 100644 index 000000000..403c7c505 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/snx/info.json @@ -0,0 +1,7 @@ +{ + "name": "Synthetix Network", + "symbol": "SNX", + "status": "active", + "contractId": "0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f", + "decimals": 18 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/storj/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/storj/info.json new file mode 100644 index 000000000..c29762412 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/storj/info.json @@ -0,0 +1,9 @@ +{ + "name": "Storj", + "symbol": "STORJ", + "status": "active", + "defaultVisibility": false, + "defaultOrdinalLevel": 105, + "contractId": "0xb64ef51c888972c908cfacf59b47c1afbc0ab8ac", + "decimals": 8 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/tusd/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/tusd/info.json new file mode 100644 index 000000000..8f4ed9341 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/tusd/info.json @@ -0,0 +1,7 @@ +{ + "name": "TrueUSD", + "symbol": "TUSD", + "status": "active", + "contractId": "0x0000000000085d4780b73119b644ae5ecd22b376", + "decimals": 18 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/uni/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/uni/info.json new file mode 100644 index 000000000..824067012 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/uni/info.json @@ -0,0 +1,7 @@ +{ + "name": "Uniswap", + "symbol": "UNI", + "status": "active", + "contractId": "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + "decimals": 18 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/usdc/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/usdc/info.json new file mode 100644 index 000000000..58d7f3615 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/usdc/info.json @@ -0,0 +1,9 @@ +{ + "name": "USD Coin", + "symbol": "USDC", + "status": "active", + "defaultVisibility": true, + "defaultOrdinalLevel": 40, + "contractId": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "decimals": 6 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/usdp/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/usdp/info.json new file mode 100644 index 000000000..b3e3eb048 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/usdp/info.json @@ -0,0 +1,7 @@ +{ + "name": "PAX Dollar", + "symbol": "USDP", + "status": "active", + "contractId": "0x8e870d67f660d95d5be530380d0ec0bd388289e1", + "decimals": 18 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/usds/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/usds/info.json new file mode 100644 index 000000000..30a906e27 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/usds/info.json @@ -0,0 +1,7 @@ +{ + "name": "Stably USD", + "symbol": "USDS", + "status": "active", + "contractId": "0xa4bdb11dc0a2bec88d24a3aa1e6bb17201112ebe", + "decimals": 6 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/usdt/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/usdt/info.json new file mode 100644 index 000000000..0e76b5a04 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/usdt/info.json @@ -0,0 +1,9 @@ +{ + "name": "Tether", + "symbol": "USDT", + "status": "active", + "defaultVisibility": true, + "defaultOrdinalLevel": 30, + "contractId": "0xdac17f958d2ee523a2206206994597c13d831ec7", + "decimals": 6 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/verse/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/verse/info.json new file mode 100644 index 000000000..4211e9018 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/verse/info.json @@ -0,0 +1,9 @@ +{ + "name": "Verse", + "symbol": "VERSE", + "status": "active", + "defaultVisibility": false, + "defaultOrdinalLevel": 95, + "contractId": "0x249cA82617eC3DfB2589c4c17ab7EC9765350a18", + "decimals": 18 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/woo/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/woo/info.json new file mode 100644 index 000000000..9716a0f95 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/woo/info.json @@ -0,0 +1,7 @@ +{ + "name": "WOO Network", + "symbol": "WOO", + "status": "active", + "contractId": "0x4691937a7508860f876c9c0a2a617e7d9e945d4b", + "decimals": 18 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/xcn/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/xcn/info.json new file mode 100644 index 000000000..badbd5042 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/blockchains/ethereum/xcn/info.json @@ -0,0 +1,9 @@ +{ + "name": "Onyxcoin", + "symbol": "XCN", + "status": "active", + "defaultVisibility": false, + "defaultOrdinalLevel": 110, + "contractId": "0xa2cd3d43c775978a96bdbf12d733d5a1ed94fb18", + "decimals": 18 +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/adamant/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/adamant/info.json new file mode 100644 index 000000000..a0f65df3a --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/adamant/info.json @@ -0,0 +1,244 @@ +{ + "name": "ADAMANT Messenger", + "nameShort": "ADAMANT", + "website": "https://adamant.im", + "description": "Decentralized Messenger", + "explorer": "https://explorer.adamant.im", + "explorerTx": "https://explorer.adamant.im/tx/${ID}", + "explorerAddress": "https://explorer.adamant.im/address/${ID}", + "regexAddress": "^U([0-9]{6,21})$", + "symbol": "ADM", + "type": "coin", + "decimals": 8, + "cryptoTransferDecimals": 8, + "fixedFee": 0.5, + "qqPrefix": "adm", + "status": "active", + "createCoin": true, + "defaultVisibility": true, + "defaultOrdinalLevel": 0, + "consensus": "dPoS", + "blockTimeFixed": 5000, + "txFetchInfo": { + "newPendingInterval": 4000, + "oldPendingInterval": 4000, + "registeredInterval": 4000 + }, + "timeout": { + "message": 300000, + "attachment": 300000 + }, + "nodes": { + "displayName": "adm-node", + "list": [ + { "url": "https://clown.adamant.im" }, + { "url": "https://lake.adamant.im" }, + { + "url": "https://endless.adamant.im", + "alt_ip": "http://149.102.157.15:36666" + }, + { "url": "https://bid.adamant.im" }, + { "url": "https://unusual.adamant.im" }, + { + "url": "https://debate.adamant.im", + "alt_ip": "http://95.216.161.113:36666" + }, + { "url": "http://78.47.205.206:36666" }, + { "url": "http://5.161.53.74:36666" }, + { "url": "http://184.94.215.92:45555" }, + { + "url": "https://node1.adamant.business", + "alt_ip": "http://194.233.75.29:45555" + }, + { "url": "https://node2.blockchain2fa.io" }, + { + "url": "https://phecda.adm.im", + "alt_ip": "http://46.250.234.248:36666" + }, + { + "url": "https://tegmine.adm.im" + }, + { + "url": "https://tauri.adm.im", + "alt_ip": "http://154.26.159.245:36666" + }, + { + "url": "https://dschubba.adm.im" + }, + { + "url": "https://tauri.bbry.org", + "alt_ip": "http://54.197.36.175:36666" + }, + { + "url": "https://endless.bbry.org", + "alt_ip": "http://54.197.36.175:46666" + }, + { + "url": "https://debate.bbry.org", + "alt_ip": "http://54.197.36.175:56666" + } + ], + "healthCheck": { + "normalUpdateInterval": 300000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000, + "threshold": 10 + }, + "minVersion": "0.8.0", + "nodeTimeCorrection": 500 + }, + "services": { + "infoService": { + "displayName": "rates-info", + "description": { + "software": "adamant-currencyinfo-services", + "github": "https://github.com/Adamant-im/adamant-currencyinfo-services", + "docs": "https://github.com/Adamant-im/adamant-currencyinfo-services/wiki/InfoServices-API-documentation" + }, + "list": [ + { + "url": "https://info.adamant.im", + "alt_ip": "http://88.198.156.44:44099" + }, + { + "url": "https://info2.adm.im", + "alt_ip": "http://207.180.210.95:33088" + } + ], + "healthCheck": { + "normalUpdateInterval": 300000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000, + "threshold": 6000000 + } + }, + "ipfsNode": { + "displayName": "ipfs-node", + "description": { + "software": "ipfs-node", + "github": "https://github.com/Adamant-im/ipfs-node", + "docs": "https://github.com/Adamant-im/ipfs-node/blob/master/README.md" + }, + "list": [ + { + "url": "https://ipfs4.adm.im", + "alt_ip": "http://95.216.45.88:44099" + }, + { + "url": "https://ipfs5.adamant.im", + "alt_ip": "http://62.72.43.99:44099" + }, + { + "url": "https://ipfs6.adamant.business", + "alt_ip": "http://75.119.138.235:44099" + } + ], + "healthCheck": { + "normalUpdateInterval": 300000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000, + "threshold": 6000000 + } + } + }, + "links": [ + { + "name": "github", + "url": "https://github.com/adamant-im" + }, + { + "name": "twitter", + "url": "https://twitter.com/adamant_im" + }, + { + "name": "whitepaper", + "url": "https://adamant.im/whitepaper/adamant-whitepaper-en.pdf" + } + ], + "tor": { + "website": "http://adamantim24okpwfr4wxjgsh6vtw4odoiabhsfaqaktnfqzrjrspjuid.onion", + "explorer": "http://srovpmanmrbmbqe63vp5nycsa3j3g6be3bz46ksmo35u5pw7jjtjamid.onion", + "explorerTx": "http://srovpmanmrbmbqe63vp5nycsa3j3g6be3bz46ksmo35u5pw7jjtjamid.onion/tx/${ID}", + "explorerAddress": "http://srovpmanmrbmbqe63vp5nycsa3j3g6be3bz46ksmo35u5pw7jjtjamid.onion/address/${ID}", + "nodes": { + "displayName": "adm-node", + "list": [ + { + "url": "http://37g5to2z6bdoeegun4hms2hvkfbdxh4rcon4e3p267wtfkh4ji2ns6id.onion" + }, + { + "url": "http://fqjcvsibnbwhlwxndzqzziapiwoidgn24avfiauu4wo2fc3yyiy2qcid.onion" + }, + { + "url": "http://omhrhpc45njpgzp4fggtndazaodwuwvb5in6hsn3k7tvaqennmfaa2qd.onion" + }, + { + "url": "http://uxu6vnnrrnl7tmo7i657yvs4jbgdz734xvufryoi326az5dzufuwboad.onion" + }, + { + "url": "http://rukehjd2yalzgny7mdjqtzown2mazsaeh4zr7zjdg4l5afkgc3z2kgqd.onion" + }, + { + "url": "http://osd7qoluu6akud6b2brxxi3bhidrbx54yqm5swr3rjwjz2cvposigiqd.onion" + }, + { + "url": "http://nkacjpsif6wpc6x7qvavyr2vnvqolwnxxl5qciolhqdkzmgy5mgoajyd.onion" + } + ], + "healthCheck": { + "normalUpdateInterval": 300000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000, + "threshold": 10 + }, + "minVersion": "0.8.0", + "nodeTimeCorrection": 500 + }, + "services": { + "infoService": { + "displayName": "rates-info", + "description": { + "software": "adamant-currencyinfo-services", + "github": "https://github.com/Adamant-im/adamant-currencyinfo-services", + "docs": "https://github.com/Adamant-im/adamant-currencyinfo-services/wiki/InfoServices-API-documentation" + }, + "list": [ + { + "url": "http://czjsawp2crjmnkliw2h2kpk7wwd3a36zvvnvqgvzmi4t4vc2yzm7j2qd.onion" + } + ], + "healthCheck": { + "normalUpdateInterval": 300000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000, + "threshold": 6000000 + } + }, + "ipfsNode": { + "displayName": "ipfs-node", + "description": { + "software": "ipfs-node", + "github": "https://github.com/Adamant-im/ipfs-node", + "docs": "https://github.com/Adamant-im/ipfs-node/blob/master/README.md" + }, + "list": [ + { + "url": "http://z455rax4mwcseyc7efog7czrbwdvphwocatl5sjcc6htcoj2k2vz7dad.onion" + }, + { + "url": "http://cds45bjd7ynxkffxpzfifnm55r6vmhgocvpvonjmry3mrskuhda6z7qd.onion" + }, + { + "url": "http://3ytwoe62bqw264v4rkaqpn5iovdg3oxly5tx2uc5qijkrdqixm6tmdyd.onion" + } + ], + "healthCheck": { + "normalUpdateInterval": 300000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000, + "threshold": 6000000 + } + } + } + } +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/bitcoin/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/bitcoin/info.json new file mode 100644 index 000000000..771f3cb90 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/bitcoin/info.json @@ -0,0 +1,135 @@ +{ + "name": "Bitcoin", + "website": "https://bitcoin.org", + "description": "Bitcoin is a cryptocurrency and worldwide payment system. It is the first decentralized digital currency, as the system works without a central bank or single administrator.", + "explorer": "https://bitcoinexplorer.org", + "explorerTx": "https://bitcoinexplorer.org/tx/${ID}", + "explorerAddress": "https://bitcoinexplorer.org/address/${ID}", + "regexAddress": "^bc1[a-zA-Z0-9]{39,59}$|^[13][1-9A-HJ-NP-Za-km-z]{25,34}$", + "symbol": "BTC", + "type": "coin", + "decimals": 8, + "minBalance": 0.00001, + "cryptoTransferDecimals": 8, + "minTransferAmount": 0.00000546, + "defaultFee": 0.00003153, + "qqPrefix": "bitcoin", + "status": "active", + "createCoin": true, + "defaultVisibility": true, + "defaultOrdinalLevel": 10, + "consensus": "PoW", + "blockTimeAvg": 600000, + "txFetchInfo": { + "newPendingInterval": 10000, + "oldPendingInterval": 3000, + "registeredInterval": 40000, + "newPendingAttempts": 20, + "oldPendingAttempts": 4 + }, + "txConsistencyMaxTime": 10800000, + "nodes": { + "displayName": "btc-node", + "list": [ + { + "url": "https://btcnode1.adamant.im/bitcoind", + "alt_ip": "http://176.9.38.204:44099/bitcoind" + }, + { + "url": "https://btcnode3.adamant.im/bitcoind", + "alt_ip": "http://195.201.242.108:44099/bitcoind" + } + ], + "healthCheck": { + "normalUpdateInterval": 360000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000, + "threshold": 2 + } + }, + "services": { + "btcIndexer": { + "displayName": "btc-indexer", + "description": { + "software": "Esplora/Electrs", + "github": "https://github.com/blockstream/electrs", + "docs": "https://github.com/blockstream/esplora/blob/master/API.md" + }, + "list": [ + { + "url": "https://btcnode1.adamant.im", + "alt_ip": "http://176.9.38.204:44099" + }, + { + "url": "https://btcnode3.adamant.im", + "alt_ip": "http://195.201.242.108:44099" + } + ], + "healthCheck": { + "normalUpdateInterval": 330000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000 + } + } + }, + "links": [ + { + "name": "github", + "url": "https://github.com/bitcoin" + }, + { + "name": "twitter", + "url": "https://twitter.com/Bitcoin" + }, + { + "name": "reddit", + "url": "https://reddit.com/r/Bitcoin" + }, + { + "name": "whitepaper", + "url": "https://bitcoin.org/bitcoin.pdf" + } + ], + "tor": { + "nodes": { + "displayName": "btc-node", + "list": [ + { + "url": "http://cc6ibzkfeseuwnmtjc6hlsd44bzg2sr3shbv7n35nj2rk2vm6dmtlnqd.onion/bitcoind" + }, + { + "url": "http://grnpvgtlrfws3424l726td5lctsod3hq2at4lhiasmedpxygbo5u2bqd.onion/bitcoind" + } + ], + "healthCheck": { + "normalUpdateInterval": 360000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000, + "threshold": 2 + } + }, + "services": { + "btcIndexer": { + "displayName": "btc-indexer", + "description": { + "software": "Esplora/Electrs", + "github": "https://github.com/blockstream/electrs", + "docs": "https://github.com/blockstream/esplora/blob/master/API.md" + }, + "list": [ + { + "url": "http://cc6ibzkfeseuwnmtjc6hlsd44bzg2sr3shbv7n35nj2rk2vm6dmtlnqd.onion" + }, + { + "url": "http://grnpvgtlrfws3424l726td5lctsod3hq2at4lhiasmedpxygbo5u2bqd.onion" + } + ], + "healthCheck": { + "normalUpdateInterval": 330000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000 + } + } + } + } +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/bnb/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/bnb/info.json new file mode 100644 index 000000000..4a00a8176 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/bnb/info.json @@ -0,0 +1,30 @@ +{ + "name": "Binance Coin", + "website": "https://bnbchain.org", + "description": "Cryptocurrency that powers the BNB Chain ecosystem", + "explorer": "https://explorer.bnbchain.org", + "symbol": "BNB", + "type": "coin", + "decimals": 8, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "github", + "url": "https://github.com/bnb-chain" + }, + { + "name": "twitter", + "url": "https://twitter.com/BNBChain" + }, + { + "name": "reddit", + "url": "http://reddit.com/r/bnbchainofficial" + }, + { + "name": "whitepaper", + "url": "https://github.com/bnb-chain/whitepaper" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/busd/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/busd/info.json new file mode 100644 index 000000000..5813c859e --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/busd/info.json @@ -0,0 +1,21 @@ +{ + "name": "Binance USD", + "website": "https://www.binance.com/en/busd", + "description": "A regulated, fiat-backed stablecoin pegged to the US dollar", + "symbol": "BUSD", + "type": "token", + "decimals": 18, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/PaxosGlobal" + }, + { + "name": "github", + "url": "https://github.com/paxosglobal/busd-contract" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/bzz/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/bzz/info.json new file mode 100644 index 000000000..3b13a574b --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/bzz/info.json @@ -0,0 +1,29 @@ +{ + "name": "Swarm", + "website": "https://www.ethswarm.org", + "description": "BZZ is the Swarm Network’s native token. Users of the network’s services (i.e. bandwidth and storage) use it to compensate the providers, or node operators, for those services.", + "symbol": "BZZ", + "type": "token", + "decimals": 16, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/ethswarm" + }, + { + "name": "github", + "url": "https://github.com/ethersphere" + }, + { + "name": "reddit", + "url": "https://www.reddit.com/r/ethswarm/" + }, + { + "name": "whitepaper", + "url": "https://www.ethswarm.org/swarm-whitepaper.pdf" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/dai/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/dai/info.json new file mode 100644 index 000000000..4b308c0d7 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/dai/info.json @@ -0,0 +1,29 @@ +{ + "name": "Dai", + "website": "https://makerdao.com", + "description": "A decentralized, unbiased, collateral-backed cryptocurrency soft-pegged to the US Dollar", + "symbol": "DAI", + "type": "token", + "decimals": 18, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/MakerDAO" + }, + { + "name": "reddit", + "url": "https://www.reddit.com/r/MakerDAO" + }, + { + "name": "github", + "url": "https://etherscan.io/token/github.com/makerdao" + }, + { + "name": "whitepaper", + "url": "https://makerdao.com/en/whitepaper" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/dash/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/dash/info.json new file mode 100644 index 000000000..ab6dc70ea --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/dash/info.json @@ -0,0 +1,88 @@ +{ + "name": "Dash", + "website": "https://dash.org", + "description": "Dash (DASH) is digital cash designed to offer financial freedom to everyone. Payments are instant, easy and secure, with near-zero fees.", + "explorer": "https://dashblockexplorer.com", + "explorerTx": "https://dashblockexplorer.com/tx/${ID}", + "explorerAddress": "https://dashblockexplorer.com/address/${ID}", + "regexAddress": "^[7X][1-9A-HJ-NP-Za-km-z]{33,}$", + "research": "https://research.binance.com/en/projects/dash", + "symbol": "DASH", + "type": "coin", + "decimals": 8, + "minBalance": 0.0001, + "cryptoTransferDecimals": 8, + "minTransferAmount": 0.00002, + "fixedFee": 0.0001, + "qqPrefix": "dash", + "status": "active", + "createCoin": true, + "defaultVisibility": true, + "defaultOrdinalLevel": 70, + "consensus": "PoW", + "blockTimeAvg": 160000, + "txFetchInfo": { + "newPendingInterval": 5000, + "oldPendingInterval": 3000, + "registeredInterval": 30000, + "newPendingAttempts": 20, + "oldPendingAttempts": 4 + }, + "txConsistencyMaxTime": 800000, + "nodes": { + "displayName": "dash-node", + "list": [ + { + "url": "https://dashnode1.adamant.im", + "alt_ip": "http://45.85.147.224:44099" + }, + { + "url": "https://dashnode2.adamant.im", + "alt_ip": "http://207.180.210.95:44099" + } + ], + "healthCheck": { + "normalUpdateInterval": 210000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000, + "threshold": 3 + } + }, + "links": [ + { + "name": "github", + "url": "https://github.com/dashpay/dash" + }, + { + "name": "twitter", + "url": "https://twitter.com/Dashpay" + }, + { + "name": "reddit", + "url": "https://reddit.com/r/dashpay" + }, + { + "name": "whitepaper", + "url": "https://github.com/dashpay/dash/wiki/Whitepaper" + } + ], + "tor": { + "nodes": { + "displayName": "dash-node", + "list": [ + { + "url": "http://ldod53womnlwjmd4noq5yuq26xx3mmbrr4uogkfymuakhofjcuqeygad.onion" + }, + { + "url": "http://eq2blda45h4kakbuq3g4stcgatiwqaefgmxzfsfuiahpdbt2pvbbepad.onion" + } + ], + "healthCheck": { + "normalUpdateInterval": 210000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000, + "threshold": 3 + } + } + } +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/doge/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/doge/info.json new file mode 100644 index 000000000..157efba2f --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/doge/info.json @@ -0,0 +1,151 @@ +{ + "name": "Dogecoin", + "website": "https://dogecoin.com", + "description": "Dogecoin is an open source peer-to-peer digital currency, favored by Shiba Inus worldwide. Introduced as a joke currency on 6 December 2013, Dogecoin quickly developed its own online community.", + "explorer": "https://dogechain.info", + "explorerTx": "https://dogechain.info/tx/${ID}", + "explorerAddress": "https://dogechain.info/address/${ID}", + "regexAddress": "^[A|D|9][A-Z0-9]([0-9a-zA-Z]{9,})$", + "research": "https://research.binance.com/en/projects/dogecoin", + "symbol": "DOGE", + "type": "coin", + "decimals": 8, + "cryptoTransferDecimals": 8, + "minTransferAmount": 1, + "fixedFee": 1, + "qqPrefix": "doge", + "status": "active", + "createCoin": true, + "defaultVisibility": true, + "defaultOrdinalLevel": 70, + "consensus": "PoW", + "blockTimeAvg": 60000, + "txFetchInfo": { + "newPendingInterval": 5000, + "oldPendingInterval": 3000, + "registeredInterval": 20000, + "newPendingAttempts": 20, + "oldPendingAttempts": 4 + }, + "txConsistencyMaxTime": 900000, + "nodes": { + "displayName": "doge-node", + "list": [ + { + "url": "https://dogenode1.adamant.im", + "alt_ip": "http://5.9.99.62:44099" + }, + { + "url": "https://dogenode2.adamant.im", + "alt_ip": "http://176.9.32.126:44098" + }, + { + "url": "https://dogenode3.adm.im", + "alt_ip": "http://95.216.45.88:44098" + } + ], + "healthCheck": { + "normalUpdateInterval": 390000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000, + "threshold": 3 + } + }, + "services": { + "dogeIndexer": { + "displayName": "doge-indexer", + "description": { + "software": "Esplora/Electrs", + "github": "https://github.com/blockstream/electrs", + "docs": "https://github.com/blockstream/esplora/blob/master/API.md" + }, + "list": [ + { + "url": "https://dogenode1.adamant.im/api", + "alt_ip": "http://5.9.99.62:44099/api" + }, + { + "url": "https://dogenode2.adamant.im/api", + "alt_ip": "http://176.9.32.126:44098/api" + }, + { + "url": "https://dogenode3.adm.im/api", + "alt_ip": "http://95.216.45.88:44098/api" + } + ], + "healthCheck": { + "normalUpdateInterval": 390000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000, + "threshold": 3 + } + } + }, + "links": [ + { + "name": "github", + "url": "https://github.com/dogecoin/dogecoin" + }, + { + "name": "twitter", + "url": "https://twitter.com/dogecoin" + }, + { + "name": "reddit", + "url": "https://reddit.com/r/dogecoin" + }, + { + "name": "whitepaper", + "url": "https://github.com/dogecoin/dogecoin/blob/master/README.md" + } + ], + "tor": { + "nodes": { + "displayName": "doge-node", + "list": [ + { + "url": "http://mtg4mq43p67cbj6qwcqgppjv7uzm7ximjlzapsbnbh5ls6zqkjsq26ad.onion" + }, + { + "url": "http://bfu3iiofsagyhi22zijfilbkzlzbalpylhhfcluqmezx2avdwcxut7yd.onion" + }, + { + "url": "http://tdl25bmpwystxnm6hxzqdrkaxxdicknbigs5umob2nlgcbbqgidd64qd.onion" + } + ], + "healthCheck": { + "normalUpdateInterval": 390000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000, + "threshold": 3 + } + }, + "services": { + "dogeIndexer": { + "displayName": "doge-indexer", + "description": { + "software": "Esplora/Electrs", + "github": "https://github.com/blockstream/electrs", + "docs": "https://github.com/blockstream/esplora/blob/master/API.md" + }, + "list": [ + { + "url": "http://mtg4mq43p67cbj6qwcqgppjv7uzm7ximjlzapsbnbh5ls6zqkjsq26ad.onion/api" + }, + { + "url": "http://bfu3iiofsagyhi22zijfilbkzlzbalpylhhfcluqmezx2avdwcxut7yd.onion/api" + }, + { + "url": "http://tdl25bmpwystxnm6hxzqdrkaxxdicknbigs5umob2nlgcbbqgidd64qd.onion/api" + } + ], + "healthCheck": { + "normalUpdateInterval": 390000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000, + "threshold": 3 + } + } + } + } +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/ens/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/ens/info.json new file mode 100644 index 000000000..2644ddbdc --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/ens/info.json @@ -0,0 +1,21 @@ +{ + "name": "Ethereum Name Service", + "website": "https://ens.domains", + "description": "Ethereum Name Service is a distributed, open, and extensible naming system based on the Ethereum blockchain", + "symbol": "ENS", + "type": "token", + "decimals": 18, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/ensdomains" + }, + { + "name": "github", + "url": "https://github.com/ensdomains" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/ethereum/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/ethereum/info.json new file mode 100644 index 000000000..5054d0b90 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/ethereum/info.json @@ -0,0 +1,140 @@ +{ + "name": "Ethereum", + "website": "https://ethereum.org", + "description": "Open source platform to write and distribute decentralized applications", + "explorer": "https://etherscan.io", + "explorerTx": "https://etherscan.io/tx/${ID}", + "explorerAddress": "https://etherscan.io/address/${ID}", + "explorerContract": "https://etherscan.io/token/${ID}", + "regexAddress": "^0x[0-9a-fA-F]{40}$", + "symbol": "ETH", + "type": "coin", + "decimals": 18, + "cryptoTransferDecimals": 6, + "status": "active", + "qqPrefix": "ethereum", + "createCoin": true, + "defaultVisibility": true, + "defaultOrdinalLevel": 20, + "consensus": "PoS", + "blockTimeFixed": 12000, + "txFetchInfo": { + "newPendingInterval": 4000, + "oldPendingInterval": 3000, + "registeredInterval": 5000, + "newPendingAttempts": 20, + "oldPendingAttempts": 4 + }, + "txConsistencyMaxTime": 1200000, + "reliabilityGasPricePercent": 10, + "reliabilityGasLimitPercent": 10, + "increasedGasPricePercent": 30, + "defaultGasPriceGwei": 10, + "defaultGasLimit": 22000, + "warningGasPriceGwei": 25, + "nodes": { + "displayName": "eth-node", + "list": [ + { + "url": "https://ethnode2.adamant.im", + "alt_ip": "http://95.216.114.252:44099" + }, + { + "url": "https://ethnode3.adamant.im", + "alt_ip": "http://46.4.37.157:44099" + } + ], + "healthCheck": { + "normalUpdateInterval": 300000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000, + "threshold": 5 + } + }, + "services": { + "ethIndexer": { + "displayName": "eth-indexer", + "description": { + "software": "ETH-transactions-storage", + "github": "https://github.com/Adamant-im/ETH-transactions-storage", + "docs": "https://github.com/Adamant-im/ETH-transactions-storage?tab=readme-ov-file#api-request-examples" + }, + "list": [ + { + "url": "https://ethnode2.adamant.im", + "alt_ip": "http://95.216.114.252:44099" + }, + { + "url": "https://ethnode3.adamant.im", + "alt_ip": "http://46.4.37.157:44099" + } + ], + "healthCheck": { + "normalUpdateInterval": 330000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000 + } + } + }, + "links": [ + { + "name": "github", + "url": "https://github.com/ethereum" + }, + { + "name": "twitter", + "url": "https://twitter.com/ethereum" + }, + { + "name": "reddit", + "url": "https://reddit.com/r/ethereum" + }, + { + "name": "whitepaper", + "url": "https://github.com/ethereum/wiki/wiki/White-Paper" + } + ], + "tor": { + "nodes": { + "displayName": "eth-node", + "list": [ + { + "url": "http://jpbrp6xapsyfnvyosrpu5wmoi62fqotazkicjeiob32yz77rt7axobqd.onion", + "hasIndex": true + }, + { + "url": "http://rekynxikhumzsme7phumocz3mquy7y3onkw33skmvk2akjkin2iopqqd.onion" + } + ], + "healthCheck": { + "normalUpdateInterval": 300000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000, + "threshold": 5 + } + }, + "services": { + "ethIndexer": { + "displayName": "eth-indexer", + "description": { + "software": "ETH-transactions-storage", + "github": "https://github.com/Adamant-im/ETH-transactions-storage", + "docs": "https://github.com/Adamant-im/ETH-transactions-storage?tab=readme-ov-file#api-request-examples" + }, + "list": [ + { + "url": "http://jpbrp6xapsyfnvyosrpu5wmoi62fqotazkicjeiob32yz77rt7axobqd.onion" + }, + { + "url": "http://rekynxikhumzsme7phumocz3mquy7y3onkw33skmvk2akjkin2iopqqd.onion" + } + ], + "healthCheck": { + "normalUpdateInterval": 330000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000 + } + } + } + } +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/floki/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/floki/info.json new file mode 100644 index 000000000..61b968e27 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/floki/info.json @@ -0,0 +1,37 @@ +{ + "name": "Floki", + "website": "https://floki.com", + "description": "FLOKI is the utility token of the Floki ecosystem", + "symbol": "FLOKI", + "type": "token", + "decimals": 9, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/RealFlokiInu" + }, + { + "name": "github", + "url": "https://github.com/floki-inu" + }, + { + "name": "whitepaper", + "url": "https://docs.floki.com/floki-whitepaper" + }, + { + "name": "reddit", + "url": "https://www.reddit.com/r/Floki" + }, + { + "name": "discord", + "url": "https://discord.gg/floki" + }, + { + "name": "telegram", + "url": "https://t.me/FlokiInuToken" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/flux/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/flux/info.json new file mode 100644 index 000000000..f66fbebd3 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/flux/info.json @@ -0,0 +1,29 @@ +{ + "name": "Flux", + "website": "https://runonflux.io", + "description": "Flux is the cryptocurrency that powers the Flux ecosystem.", + "symbol": "FLUX", + "type": "token", + "decimals": 8, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/RunOnFlux" + }, + { + "name": "github", + "url": "https://github.com/RunOnFlux" + }, + { + "name": "reddit", + "url": "https://www.reddit.com/r/Flux_Official" + }, + { + "name": "whitepaper", + "url": "https://whitepaper.app.runonflux.io" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/gt/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/gt/info.json new file mode 100644 index 000000000..da1e3c289 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/gt/info.json @@ -0,0 +1,25 @@ +{ + "name": "GateToken", + "website": "https://gatechain.io", + "description": "Public blockchain which facilitates digital asset transfers", + "symbol": "GT", + "type": "token", + "decimals": 18, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/gatechain_io" + }, + { + "name": "github", + "url": "https://github.com/gatechain" + }, + { + "name": "discord", + "url": "https://discord.gg/TADecrzfcP" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/hot/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/hot/info.json new file mode 100644 index 000000000..dfb1f9562 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/hot/info.json @@ -0,0 +1,29 @@ +{ + "name": "Holo", + "website": "https://holo.host", + "description": "Holo is a peer-to-peer platform for distributed applications based on a cloud storage infrastructure", + "symbol": "HOT", + "type": "token", + "decimals": 18, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/H_O_L_O_" + }, + { + "name": "reddit", + "url": "https://www.reddit.com/r/holochain" + }, + { + "name": "github", + "url": "https://github.com/Holo-Host" + }, + { + "name": "whitepaper", + "url": "https://holo.host/whitepapers" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/inj/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/inj/info.json new file mode 100644 index 000000000..30ddd69f7 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/inj/info.json @@ -0,0 +1,32 @@ +{ + "name": "Injective", + "website": "https://injective.com", + "description": "An open, interoperable smart contract platform optimized for DeFi applications", + "explorer": "https://explorer.injective.network", + "explorerTx": "https://explorer.injective.network/transaction/${ID}", + "explorerAddress": "https://explorer.injective.network/account/${ID}", + "explorerContract": "https://explorer.injective.network/contract/${ID}", + "regexAddress": "^(inj)[a-zA-HJ-NP-Z0-9]{25,39}$", + "symbol": "INJ", + "type": "coin", + "decimals": 18, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "consensus": "PoS", + "blockTimeFixed": 1100, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/Injective_" + }, + { + "name": "reddit", + "url": "https://www.reddit.com/r/injective" + }, + { + "name": "github", + "url": "https://github.com/InjectiveLabs" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/klayr/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/klayr/info.json new file mode 100644 index 000000000..9f3757c55 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/klayr/info.json @@ -0,0 +1,130 @@ +{ + "name": "Klayr", + "website": "https://klayr.xyz", + "description": "Blockchain application platform", + "explorer": "https://explorer.klayr.xyz", + "explorerTx": "https://explorer.klayr.xyz/transaction/${ID}", + "explorerAddress": "https://explorer.klayr.xyz/account/${ID}", + "regexAddress": "^kly[a-z2-9]{38}$", + "symbol": "KLY", + "type": "coin", + "decimals": 8, + "minBalance": 0.05, + "cryptoTransferDecimals": 8, + "defaultFee": 0.00164, + "qqPrefix": "klayr", + "status": "active", + "createCoin": true, + "defaultVisibility": true, + "defaultOrdinalLevel": 50, + "consensus": "dPoS", + "blockTimeFixed": 10000, + "txFetchInfo": { + "newPendingInterval": 3000, + "oldPendingInterval": 3000, + "registeredInterval": 5000, + "newPendingAttempts": 15, + "oldPendingAttempts": 4 + }, + "txConsistencyMaxTime": 60000, + "nodes": { + "displayName": "kly-node", + "list": [ + { + "url": "https://klynode2.adamant.im", + "alt_ip": "http://109.176.199.130:44099" + }, + { + "url": "https://klynode3.adm.im", + "alt_ip": "http://37.27.205.78:44099" + } + ], + "healthCheck": { + "normalUpdateInterval": 270000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000, + "threshold": 5 + } + }, + "services": { + "klyService": { + "displayName": "kly-indexer", + "description": { + "software": "klayr-service", + "github": "https://github.com/KlayrHQ/klayr-service", + "docs": "https://klayr.xyz/documentation/klayr-service" + }, + "list": [ + { + "url": "https://klyservice2.adamant.im", + "alt_ip": "http://109.176.199.130:44098" + }, + { + "url": "https://klyservice3.adm.im", + "alt_ip": "http://37.27.205.78:44098" + } + ], + "healthCheck": { + "normalUpdateInterval": 330000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000 + } + } + }, + "links": [ + { + "name": "github", + "url": "https://github.com/klayrHQ" + }, + { + "name": "twitter", + "url": "https://x.com/KlayrHQ" + }, + { + "name": "reddit", + "url": "https://reddit.com/r/klayr" + } + ], + "tor": { + "nodes": { + "displayName": "kly-node", + "list": [ + { + "url": "http://5fr7uybpxecid5gikrm65hstsfhve2772keyqdvxirsiywlyo6zap6yd.onion" + }, + { + "url": "http://5rmyjfvazkg5gcyo3gwvdinykvycsleeebumdm6zlj6dhf6gshpelfid.onion" + } + ], + "healthCheck": { + "normalUpdateInterval": 270000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000, + "threshold": 5 + } + }, + "services": { + "klyService": { + "displayName": "kly-indexer", + "description": { + "software": "klayr-service", + "github": "https://github.com/KlayrHQ/klayr-service", + "docs": "https://klayr.xyz/documentation/klayr-service" + }, + "list": [ + { + "url": "http://3om4mobnbppxuexwufprp4cle4fivstooqqap6yoll5qk3kikesmqgad.onion" + }, + { + "url": "http://xif2b7cchtn27aq2qypjc6p3phamrt4fdc3gbl5e6kk2fw3ypovre5id.onion" + } + ], + "healthCheck": { + "normalUpdateInterval": 330000, + "crucialUpdateInterval": 30000, + "onScreenUpdateInterval": 10000 + } + } + } + } +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/link/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/link/info.json new file mode 100644 index 000000000..bcfe0fc8b --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/link/info.json @@ -0,0 +1,29 @@ +{ + "name": "Chainlink", + "website": "https://chain.link", + "description": "Chainlink is a decentralized blockchain oracle network built on Ethereum", + "symbol": "LINK", + "type": "token", + "decimals": 18, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/chainlink" + }, + { + "name": "reddit", + "url": "https://www.reddit.com/r/Chainlink" + }, + { + "name": "github", + "url": "https://github.com/smartcontractkit" + }, + { + "name": "whitepaper", + "url": "https://research.chain.link/whitepaper-v2.pdf" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/mana/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/mana/info.json new file mode 100644 index 000000000..f4c29a7fe --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/mana/info.json @@ -0,0 +1,29 @@ +{ + "name": "Decentraland", + "website": "https://decentraland.org", + "description": "Decentraland is a decentralized virtual reality world that is powered by the Ethereum blockchain", + "symbol": "MANA", + "type": "token", + "decimals": 18, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/decentraland" + }, + { + "name": "reddit", + "url": "https://www.reddit.com/r/decentraland" + }, + { + "name": "github", + "url": "https://github.com/decentraland" + }, + { + "name": "whitepaper", + "url": "https://decentraland.org/whitepaper.pdf" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/matic/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/matic/info.json new file mode 100644 index 000000000..049362b49 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/matic/info.json @@ -0,0 +1,36 @@ +{ + "name": "Polygon", + "website": "https://polygon.technology", + "description": "Tokens used to govern and secure the Polygon network and pay transaction fees", + "explorer": "https://polygonscan.com", + "explorerTx": "https://polygonscan.com/tx/${ID}", + "explorerAddress": "https://polygonscan.com/address/${ID}", + "explorerContract": "https://polygonscan.com/token/${ID}", + "regexAddress": "^0x[0-9a-fA-F]{40}$", + "symbol": "MATIC", + "type": "coin", + "decimals": 18, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "consensus": "PoS", + "blockTimeAvg": 2250, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/0xPolygon" + }, + { + "name": "reddit", + "url": "https://www.reddit.com/r/0xPolygon" + }, + { + "name": "github", + "url": "https://github.com/maticnetwork" + }, + { + "name": "whitepaper", + "url": "https://github.com/maticnetwork/whitepaper" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/paxg/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/paxg/info.json new file mode 100644 index 000000000..775081fa2 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/paxg/info.json @@ -0,0 +1,25 @@ +{ + "name": "PAX Gold", + "website": "https://paxos.com/paxgold", + "description": "PAXG is an ERC-20 stablecoin backed by physical gold reserves, held in custody by the Paxos Trust Company", + "symbol": "PAXG", + "type": "token", + "decimals": 18, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/PaxosGlobal" + }, + { + "name": "github", + "url": "https://github.com/paxosglobal/paxos-gold-contract" + }, + { + "name": "whitepaper", + "url": "https://paxos.com/wp-content/uploads/2019/09/PAX-Gold-Whitepaper.pdf" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/qnt/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/qnt/info.json new file mode 100644 index 000000000..99365f3d6 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/qnt/info.json @@ -0,0 +1,29 @@ +{ + "name": "Quant", + "website": "https://quant.network", + "description": "An operating system distributed ledger technology— and Overledger Network — for connecting different blockchain networks.", + "symbol": "QNT", + "type": "token", + "decimals": 18, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/quant_network" + }, + { + "name": "reddit", + "url": "https://www.reddit.com/r/QuantNetwork/" + }, + { + "name": "github", + "url": "https://github.com/quantnetwork" + }, + { + "name": "whitepaper", + "url": "http://files.quant.network/files.quant.network/Quant_Overledger_Whitepaper_v0.1.pdf" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/ren/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/ren/info.json new file mode 100644 index 000000000..309e360df --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/ren/info.json @@ -0,0 +1,25 @@ +{ + "name": "Ren", + "website": "https://renproject.io", + "description": "Ren is an open and community-driven protocol that enables the movement of value between blockchains", + "symbol": "REN", + "type": "token", + "decimals": 18, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/renprotocol" + }, + { + "name": "reddit", + "url": "https://www.reddit.com/r/RenProject" + }, + { + "name": "github", + "url": "https://github.com/renproject" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/skl/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/skl/info.json new file mode 100644 index 000000000..566fe7f0a --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/skl/info.json @@ -0,0 +1,29 @@ +{ + "name": "SKALE", + "website": "https://skale.space", + "description": "SKALE is a Layer-2 Ethereum sidechain network that creates a high-throughput, low-latency, low-cost environment for decentralized app development", + "symbol": "SKL", + "type": "token", + "decimals": 18, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/SkaleNetwork" + }, + { + "name": "reddit", + "url": "https://www.reddit.com/r/SKALEnetwork" + }, + { + "name": "github", + "url": "https://github.com/skalenetwork" + }, + { + "name": "whitepaper", + "url": "https://skale.space/whitepaper" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/snt/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/snt/info.json new file mode 100644 index 000000000..01e571522 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/snt/info.json @@ -0,0 +1,29 @@ +{ + "name": "Status", + "website": "https://status.im", + "description": "Status is categorized as a mobile and desktop operating system and decentralized browser that incorporates a messaging system", + "symbol": "SNT", + "type": "token", + "decimals": 18, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/ethstatus" + }, + { + "name": "reddit", + "url": "https://www.reddit.com/r/statusim" + }, + { + "name": "github", + "url": "https://github.com/status-im" + }, + { + "name": "whitepaper", + "url": "https://status.im/whitepaper.pdf" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/snx/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/snx/info.json new file mode 100644 index 000000000..3f821f1c4 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/snx/info.json @@ -0,0 +1,25 @@ +{ + "name": "Synthetix Network", + "website": "https://www.synthetix.io", + "description": "Synthetix is an Ethereum-based protocol that tokenizes underlying assets ranging from commodities to fiat money", + "symbol": "SNX", + "type": "token", + "decimals": 18, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/synthetix_io" + }, + { + "name": "reddit", + "url": "https://www.reddit.com/r/synthetix_io" + }, + { + "name": "github", + "url": "https://github.com/Synthetixio/synthetix" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/storj/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/storj/info.json new file mode 100644 index 000000000..7efe4d530 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/storj/info.json @@ -0,0 +1,29 @@ +{ + "name": "Storj", + "website": "https://www.storj.io", + "description": "Storj is an open-source cloud storage platform", + "symbol": "STORJ", + "type": "token", + "decimals": 8, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/storj" + }, + { + "name": "github", + "url": "https://github.com/storj" + }, + { + "name": "whitepaper", + "url": "https://www.storj.io/storj.pdf" + }, + { + "name": "reddit", + "url": "https://www.reddit.com/r/storj" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/tusd/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/tusd/info.json new file mode 100644 index 000000000..a0a529b49 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/tusd/info.json @@ -0,0 +1,25 @@ +{ + "name": "TrueUSD", + "website": "https://trueusd.com", + "description": "TUSD is the first independently verified digital asset redeemable 1-for-1 in US dollars", + "symbol": "TUSD", + "type": "token", + "decimals": 18, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/tusd_official" + }, + { + "name": "github", + "url": "https://github.com/trusttoken/contracts-pre22" + }, + { + "name": "whitepaper", + "url": "https://trueusd.com/pdf/TUSD_WhitePaper.pdf" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/uni/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/uni/info.json new file mode 100644 index 000000000..c7c4ea4cd --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/uni/info.json @@ -0,0 +1,29 @@ +{ + "name": "Uniswap", + "website": "https://uniswap.org", + "description": "Uniswap is a decentralized cryptocurrency exchange that uses a set of smart contracts to execute trades on its exchange", + "symbol": "UNI", + "type": "token", + "decimals": 18, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/uniswap" + }, + { + "name": "reddit", + "url": "https://www.reddit.com/r/UniSwap" + }, + { + "name": "github", + "url": "https://github.com/Uniswap" + }, + { + "name": "whitepaper", + "url": "https://uniswap.org/whitepaper-v3.pdf" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/usdc/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/usdc/info.json new file mode 100644 index 000000000..983e9e4ae --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/usdc/info.json @@ -0,0 +1,29 @@ +{ + "name": "USD Coin", + "website": "https://www.centre.io/usdc", + "description": "An open source, smart contract-based stablecoin", + "symbol": "USDC", + "type": "token", + "decimals": 6, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/centre_io" + }, + { + "name": "reddit", + "url": "https://www.reddit.com/r/USDC/" + }, + { + "name": "github", + "url": "https://github.com/centrehq" + }, + { + "name": "whitepaper", + "url": "https://f.hubspotusercontent30.net/hubfs/9304636/PDF/centre-whitepaper.pdf" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/usdp/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/usdp/info.json new file mode 100644 index 000000000..91377cc46 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/usdp/info.json @@ -0,0 +1,21 @@ +{ + "name": "PAX Dollar", + "website": "https://www.paxos.com/usdp", + "description": "PAX Dollar is a regulated stablecoin collateralized 1:1 by United States Dollars held in US bank", + "symbol": "USDP", + "type": "token", + "decimals": 18, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/paxosglobal" + }, + { + "name": "github", + "url": "https://github.com/paxosglobal/usdp-contracts" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/usds/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/usds/info.json new file mode 100644 index 000000000..3badd23de --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/usds/info.json @@ -0,0 +1,17 @@ +{ + "name": "Stably USD", + "website": "https://stably.io/usds", + "description": "One-to-one U.S. Dollar backed, redeemable stablecoin", + "symbol": "USDS", + "type": "token", + "decimals": 6, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/stably_official" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/usdt/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/usdt/info.json new file mode 100644 index 000000000..4e6aebea0 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/usdt/info.json @@ -0,0 +1,25 @@ +{ + "name": "Tether", + "website": "https://tether.to", + "description": "Stablecoin pegged to the US Dollar", + "symbol": "USDT", + "type": "token", + "decimals": 6, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/Tether_to" + }, + { + "name": "reddit", + "url": "https://www.reddit.com/r/Tether" + }, + { + "name": "whitepaper", + "url": "https://assets.ctfassets.net/vyse88cgwfbl/5UWgHMvz071t2Cq5yTw5vi/c9798ea8db99311bf90ebe0810938b01/TetherWhitePaper.pdf" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/verse/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/verse/info.json new file mode 100644 index 000000000..32a6e0754 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/verse/info.json @@ -0,0 +1,29 @@ +{ + "name": "Verse", + "website": "https://www.getverse.com", + "description": "VERSE is the reward and utility token for the Bitcoin.com ecosystem", + "symbol": "VERSE", + "type": "token", + "decimals": 18, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/BitcoinCom" + }, + { + "name": "medium", + "url": "https://blog.bitcoin.com" + }, + { + "name": "telegram", + "url": "https://t.me/GetVerse" + }, + { + "name": "whitepaper", + "url": "https://www.getverse.com/verse-whitepaper.pdf" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/woo/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/woo/info.json new file mode 100644 index 000000000..36cd62b6a --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/woo/info.json @@ -0,0 +1,25 @@ +{ + "name": "WOO Network", + "website": "https://woo.network", + "description": "WOO Network is a system of centralized finance and decentralized finance services designed to enable deeper liquidity for various cryptocurrency market participants", + "symbol": "WOO", + "type": "token", + "decimals": 18, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/WOOnetwork" + }, + { + "name": "reddit", + "url": "https://www.reddit.com/r/WOO_X" + }, + { + "name": "whitepaper", + "url": "https://woo.org/Litepaper.pdf" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/xcn/info.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/xcn/info.json new file mode 100644 index 000000000..c150ebd56 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/JsonStore/general/xcn/info.json @@ -0,0 +1,33 @@ +{ + "name": "Onyxcoin", + "website": "https://onyx.org", + "description": "Onyx DeFi Liquidity Protocol supported by the Onyx DAO", + "symbol": "XCN", + "type": "token", + "decimals": 18, + "cryptoTransferDecimals": 6, + "status": "active", + "createCoin": false, + "links": [ + { + "name": "twitter", + "url": "https://twitter.com/OnyxProtocol" + }, + { + "name": "github", + "url": "https://github.com/Onyx-Protocol" + }, + { + "name": "whitepaper", + "url": "https://onyx.org/Whitepaper.pdf" + }, + { + "name": "telegram_chat", + "url": "https://t.me/OnyxOrg" + }, + { + "name": "blog", + "url": "https://medium.com/OnyxProtocol" + } + ] +} diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_notification.imageset/Contents.json new file mode 100644 index 000000000..27a0fe4b3 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "adamant_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "adamant_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "adamant_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_notification.imageset/adamant_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_notification.imageset/adamant_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_notification.imageset/adamant_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_notification.imageset/adamant_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_notification.imageset/adamant_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_notification.imageset/adamant_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_notification.imageset/adamant_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_notification.imageset/adamant_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_notification.imageset/adamant_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_notification.imageset/adamant_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_notification.imageset/adamant_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_notification.imageset/adamant_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_wallet.imageset/Contents.json new file mode 100644 index 000000000..0fb51a2fd --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "adamant_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "adamant_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "adamant_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_wallet.imageset/adamant_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_wallet.imageset/adamant_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_wallet.imageset/adamant_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_wallet.imageset/adamant_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_wallet.imageset/adamant_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_wallet.imageset/adamant_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_wallet.imageset/adamant_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_wallet.imageset/adamant_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_wallet.imageset/adamant_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_wallet.imageset/adamant_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_wallet.imageset/adamant_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_wallet.imageset/adamant_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..f40e60e7f --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "adamant_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "adamant_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "adamant_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_wallet_row.imageset/adamant_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_wallet_row.imageset/adamant_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_wallet_row.imageset/adamant_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_wallet_row.imageset/adamant_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_wallet_row.imageset/adamant_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_wallet_row.imageset/adamant_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_wallet_row.imageset/adamant_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_wallet_row.imageset/adamant_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_wallet_row.imageset/adamant_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_wallet_row.imageset/adamant_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_wallet_row.imageset/adamant_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/adamant_wallet_row.imageset/adamant_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_notification.imageset/Contents.json new file mode 100644 index 000000000..889d9c397 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "bitcoin_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "bitcoin_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "bitcoin_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_notification.imageset/bitcoin_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_notification.imageset/bitcoin_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_notification.imageset/bitcoin_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_notification.imageset/bitcoin_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_notification.imageset/bitcoin_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_notification.imageset/bitcoin_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_notification.imageset/bitcoin_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_notification.imageset/bitcoin_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_notification.imageset/bitcoin_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_notification.imageset/bitcoin_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_notification.imageset/bitcoin_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_notification.imageset/bitcoin_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_wallet.imageset/Contents.json new file mode 100644 index 000000000..b0d315f7a --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "bitcoin_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "bitcoin_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "bitcoin_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_wallet.imageset/bitcoin_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_wallet.imageset/bitcoin_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_wallet.imageset/bitcoin_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_wallet.imageset/bitcoin_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_wallet.imageset/bitcoin_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_wallet.imageset/bitcoin_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_wallet.imageset/bitcoin_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_wallet.imageset/bitcoin_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_wallet.imageset/bitcoin_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_wallet.imageset/bitcoin_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_wallet.imageset/bitcoin_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_wallet.imageset/bitcoin_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..d45355c25 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "bitcoin_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "bitcoin_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "bitcoin_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_wallet_row.imageset/bitcoin_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_wallet_row.imageset/bitcoin_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_wallet_row.imageset/bitcoin_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_wallet_row.imageset/bitcoin_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_wallet_row.imageset/bitcoin_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_wallet_row.imageset/bitcoin_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_wallet_row.imageset/bitcoin_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_wallet_row.imageset/bitcoin_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_wallet_row.imageset/bitcoin_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_wallet_row.imageset/bitcoin_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_wallet_row.imageset/bitcoin_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bitcoin_wallet_row.imageset/bitcoin_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_notification.imageset/Contents.json new file mode 100644 index 000000000..e0927c659 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "bnb_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "bnb_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "bnb_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_notification.imageset/bnb_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_notification.imageset/bnb_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_notification.imageset/bnb_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_notification.imageset/bnb_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_notification.imageset/bnb_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_notification.imageset/bnb_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_notification.imageset/bnb_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_notification.imageset/bnb_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_notification.imageset/bnb_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_notification.imageset/bnb_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_notification.imageset/bnb_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_notification.imageset/bnb_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_wallet.imageset/Contents.json new file mode 100644 index 000000000..c42cff80e --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "bnb_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "bnb_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "bnb_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_wallet.imageset/bnb_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_wallet.imageset/bnb_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_wallet.imageset/bnb_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_wallet.imageset/bnb_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_wallet.imageset/bnb_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_wallet.imageset/bnb_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_wallet.imageset/bnb_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_wallet.imageset/bnb_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_wallet.imageset/bnb_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_wallet.imageset/bnb_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_wallet.imageset/bnb_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_wallet.imageset/bnb_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..6ff762609 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "bnb_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "bnb_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "bnb_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_wallet_row.imageset/bnb_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_wallet_row.imageset/bnb_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_wallet_row.imageset/bnb_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_wallet_row.imageset/bnb_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_wallet_row.imageset/bnb_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_wallet_row.imageset/bnb_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_wallet_row.imageset/bnb_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_wallet_row.imageset/bnb_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_wallet_row.imageset/bnb_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_wallet_row.imageset/bnb_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_wallet_row.imageset/bnb_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bnb_wallet_row.imageset/bnb_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_notification.imageset/Contents.json new file mode 100644 index 000000000..9c8fbf155 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "busd_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "busd_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "busd_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_notification.imageset/busd_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_notification.imageset/busd_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_notification.imageset/busd_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_notification.imageset/busd_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_notification.imageset/busd_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_notification.imageset/busd_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_notification.imageset/busd_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_notification.imageset/busd_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_notification.imageset/busd_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_notification.imageset/busd_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_notification.imageset/busd_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_notification.imageset/busd_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_wallet.imageset/Contents.json new file mode 100644 index 000000000..e68d756e5 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "busd_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "busd_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "busd_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_wallet.imageset/busd_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_wallet.imageset/busd_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_wallet.imageset/busd_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_wallet.imageset/busd_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_wallet.imageset/busd_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_wallet.imageset/busd_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_wallet.imageset/busd_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_wallet.imageset/busd_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_wallet.imageset/busd_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_wallet.imageset/busd_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_wallet.imageset/busd_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_wallet.imageset/busd_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..4aac23daa --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "busd_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "busd_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "busd_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_wallet_row.imageset/busd_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_wallet_row.imageset/busd_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_wallet_row.imageset/busd_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_wallet_row.imageset/busd_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_wallet_row.imageset/busd_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_wallet_row.imageset/busd_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_wallet_row.imageset/busd_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_wallet_row.imageset/busd_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_wallet_row.imageset/busd_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_wallet_row.imageset/busd_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_wallet_row.imageset/busd_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/busd_wallet_row.imageset/busd_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_notification.imageset/Contents.json new file mode 100644 index 000000000..8c96fa7c2 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "bzz_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "bzz_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "bzz_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_notification.imageset/bzz_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_notification.imageset/bzz_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_notification.imageset/bzz_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_notification.imageset/bzz_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_notification.imageset/bzz_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_notification.imageset/bzz_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_notification.imageset/bzz_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_notification.imageset/bzz_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_notification.imageset/bzz_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_notification.imageset/bzz_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_notification.imageset/bzz_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_notification.imageset/bzz_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_wallet.imageset/Contents.json new file mode 100644 index 000000000..231f1c579 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "bzz_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "bzz_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "bzz_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_wallet.imageset/bzz_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_wallet.imageset/bzz_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_wallet.imageset/bzz_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_wallet.imageset/bzz_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_wallet.imageset/bzz_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_wallet.imageset/bzz_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_wallet.imageset/bzz_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_wallet.imageset/bzz_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_wallet.imageset/bzz_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_wallet.imageset/bzz_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_wallet.imageset/bzz_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_wallet.imageset/bzz_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..8fea4f3c4 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "bzz_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "bzz_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "bzz_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_wallet_row.imageset/bzz_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_wallet_row.imageset/bzz_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_wallet_row.imageset/bzz_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_wallet_row.imageset/bzz_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_wallet_row.imageset/bzz_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_wallet_row.imageset/bzz_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_wallet_row.imageset/bzz_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_wallet_row.imageset/bzz_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_wallet_row.imageset/bzz_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_wallet_row.imageset/bzz_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_wallet_row.imageset/bzz_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/bzz_wallet_row.imageset/bzz_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_notification.imageset/Contents.json new file mode 100644 index 000000000..24693ad00 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "dai_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "dai_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "dai_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_notification.imageset/dai_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_notification.imageset/dai_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_notification.imageset/dai_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_notification.imageset/dai_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_notification.imageset/dai_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_notification.imageset/dai_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_notification.imageset/dai_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_notification.imageset/dai_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_notification.imageset/dai_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_notification.imageset/dai_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_notification.imageset/dai_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_notification.imageset/dai_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_wallet.imageset/Contents.json new file mode 100644 index 000000000..df05b1b9b --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "dai_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "dai_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "dai_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_wallet.imageset/dai_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_wallet.imageset/dai_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_wallet.imageset/dai_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_wallet.imageset/dai_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_wallet.imageset/dai_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_wallet.imageset/dai_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_wallet.imageset/dai_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_wallet.imageset/dai_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_wallet.imageset/dai_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_wallet.imageset/dai_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_wallet.imageset/dai_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_wallet.imageset/dai_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..65c4c0776 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "dai_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "dai_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "dai_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_wallet_row.imageset/dai_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_wallet_row.imageset/dai_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_wallet_row.imageset/dai_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_wallet_row.imageset/dai_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_wallet_row.imageset/dai_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_wallet_row.imageset/dai_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_wallet_row.imageset/dai_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_wallet_row.imageset/dai_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_wallet_row.imageset/dai_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_wallet_row.imageset/dai_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_wallet_row.imageset/dai_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dai_wallet_row.imageset/dai_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_notification.imageset/Contents.json new file mode 100644 index 000000000..739eb574c --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "dash_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "dash_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "dash_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_notification.imageset/dash_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_notification.imageset/dash_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_notification.imageset/dash_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_notification.imageset/dash_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_notification.imageset/dash_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_notification.imageset/dash_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_notification.imageset/dash_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_notification.imageset/dash_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_notification.imageset/dash_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_notification.imageset/dash_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_notification.imageset/dash_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_notification.imageset/dash_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_wallet.imageset/Contents.json new file mode 100644 index 000000000..00c8d1e7c --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "dash_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "dash_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "dash_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_wallet.imageset/dash_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_wallet.imageset/dash_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_wallet.imageset/dash_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_wallet.imageset/dash_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_wallet.imageset/dash_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_wallet.imageset/dash_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_wallet.imageset/dash_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_wallet.imageset/dash_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_wallet.imageset/dash_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_wallet.imageset/dash_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_wallet.imageset/dash_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_wallet.imageset/dash_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..fa5cea204 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "dash_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "dash_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "dash_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_wallet_row.imageset/dash_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_wallet_row.imageset/dash_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_wallet_row.imageset/dash_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_wallet_row.imageset/dash_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_wallet_row.imageset/dash_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_wallet_row.imageset/dash_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_wallet_row.imageset/dash_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_wallet_row.imageset/dash_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_wallet_row.imageset/dash_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_wallet_row.imageset/dash_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_wallet_row.imageset/dash_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/dash_wallet_row.imageset/dash_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_notification.imageset/Contents.json new file mode 100644 index 000000000..7d9b52a7d --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "doge_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "doge_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "doge_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_notification.imageset/doge_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_notification.imageset/doge_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_notification.imageset/doge_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_notification.imageset/doge_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_notification.imageset/doge_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_notification.imageset/doge_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_notification.imageset/doge_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_notification.imageset/doge_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_notification.imageset/doge_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_notification.imageset/doge_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_notification.imageset/doge_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_notification.imageset/doge_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_wallet.imageset/Contents.json new file mode 100644 index 000000000..877e44e7f --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "doge_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "doge_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "doge_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_wallet.imageset/doge_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_wallet.imageset/doge_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_wallet.imageset/doge_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_wallet.imageset/doge_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_wallet.imageset/doge_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_wallet.imageset/doge_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_wallet.imageset/doge_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_wallet.imageset/doge_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_wallet.imageset/doge_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_wallet.imageset/doge_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_wallet.imageset/doge_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_wallet.imageset/doge_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..b8d741acb --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "doge_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "doge_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "doge_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_wallet_row.imageset/doge_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_wallet_row.imageset/doge_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_wallet_row.imageset/doge_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_wallet_row.imageset/doge_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_wallet_row.imageset/doge_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_wallet_row.imageset/doge_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_wallet_row.imageset/doge_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_wallet_row.imageset/doge_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_wallet_row.imageset/doge_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_wallet_row.imageset/doge_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_wallet_row.imageset/doge_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/doge_wallet_row.imageset/doge_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_notification.imageset/Contents.json new file mode 100644 index 000000000..ab467a293 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "ens_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "ens_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "ens_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_notification.imageset/ens_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_notification.imageset/ens_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_notification.imageset/ens_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_notification.imageset/ens_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_notification.imageset/ens_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_notification.imageset/ens_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_notification.imageset/ens_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_notification.imageset/ens_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_notification.imageset/ens_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_notification.imageset/ens_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_notification.imageset/ens_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_notification.imageset/ens_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_wallet.imageset/Contents.json new file mode 100644 index 000000000..2ca4aeddb --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "ens_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "ens_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "ens_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_wallet.imageset/ens_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_wallet.imageset/ens_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_wallet.imageset/ens_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_wallet.imageset/ens_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_wallet.imageset/ens_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_wallet.imageset/ens_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_wallet.imageset/ens_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_wallet.imageset/ens_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_wallet.imageset/ens_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_wallet.imageset/ens_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_wallet.imageset/ens_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_wallet.imageset/ens_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..94c47afe3 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "ens_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "ens_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "ens_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_wallet_row.imageset/ens_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_wallet_row.imageset/ens_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_wallet_row.imageset/ens_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_wallet_row.imageset/ens_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_wallet_row.imageset/ens_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_wallet_row.imageset/ens_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_wallet_row.imageset/ens_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_wallet_row.imageset/ens_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_wallet_row.imageset/ens_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_wallet_row.imageset/ens_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_wallet_row.imageset/ens_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ens_wallet_row.imageset/ens_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_notification.imageset/Contents.json new file mode 100644 index 000000000..1a0ee5dd0 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "ethereum_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "ethereum_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "ethereum_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_notification.imageset/ethereum_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_notification.imageset/ethereum_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_notification.imageset/ethereum_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_notification.imageset/ethereum_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_notification.imageset/ethereum_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_notification.imageset/ethereum_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_notification.imageset/ethereum_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_notification.imageset/ethereum_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_notification.imageset/ethereum_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_notification.imageset/ethereum_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_notification.imageset/ethereum_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_notification.imageset/ethereum_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_wallet.imageset/Contents.json new file mode 100644 index 000000000..54d661414 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "ethereum_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "ethereum_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "ethereum_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_wallet.imageset/ethereum_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_wallet.imageset/ethereum_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_wallet.imageset/ethereum_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_wallet.imageset/ethereum_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_wallet.imageset/ethereum_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_wallet.imageset/ethereum_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_wallet.imageset/ethereum_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_wallet.imageset/ethereum_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_wallet.imageset/ethereum_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_wallet.imageset/ethereum_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_wallet.imageset/ethereum_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_wallet.imageset/ethereum_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..e5d7e5e47 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "ethereum_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "ethereum_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "ethereum_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_wallet_row.imageset/ethereum_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_wallet_row.imageset/ethereum_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_wallet_row.imageset/ethereum_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_wallet_row.imageset/ethereum_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_wallet_row.imageset/ethereum_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_wallet_row.imageset/ethereum_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_wallet_row.imageset/ethereum_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_wallet_row.imageset/ethereum_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_wallet_row.imageset/ethereum_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_wallet_row.imageset/ethereum_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_wallet_row.imageset/ethereum_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ethereum_wallet_row.imageset/ethereum_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_notification.imageset/Contents.json new file mode 100644 index 000000000..08a7c4852 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "floki_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "floki_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "floki_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_notification.imageset/floki_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_notification.imageset/floki_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_notification.imageset/floki_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_notification.imageset/floki_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_notification.imageset/floki_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_notification.imageset/floki_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_notification.imageset/floki_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_notification.imageset/floki_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_notification.imageset/floki_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_notification.imageset/floki_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_notification.imageset/floki_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_notification.imageset/floki_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_wallet.imageset/Contents.json new file mode 100644 index 000000000..c106a006c --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "floki_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "floki_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "floki_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_wallet.imageset/floki_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_wallet.imageset/floki_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_wallet.imageset/floki_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_wallet.imageset/floki_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_wallet.imageset/floki_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_wallet.imageset/floki_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_wallet.imageset/floki_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_wallet.imageset/floki_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_wallet.imageset/floki_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_wallet.imageset/floki_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_wallet.imageset/floki_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_wallet.imageset/floki_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..96d567c93 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "floki_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "floki_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "floki_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_wallet_row.imageset/floki_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_wallet_row.imageset/floki_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_wallet_row.imageset/floki_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_wallet_row.imageset/floki_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_wallet_row.imageset/floki_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_wallet_row.imageset/floki_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_wallet_row.imageset/floki_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_wallet_row.imageset/floki_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_wallet_row.imageset/floki_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_wallet_row.imageset/floki_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_wallet_row.imageset/floki_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/floki_wallet_row.imageset/floki_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_notification.imageset/Contents.json new file mode 100644 index 000000000..62eb6054e --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "flux_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "flux_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "flux_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_notification.imageset/flux_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_notification.imageset/flux_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_notification.imageset/flux_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_notification.imageset/flux_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_notification.imageset/flux_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_notification.imageset/flux_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_notification.imageset/flux_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_notification.imageset/flux_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_notification.imageset/flux_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_notification.imageset/flux_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_notification.imageset/flux_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_notification.imageset/flux_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_wallet.imageset/Contents.json new file mode 100644 index 000000000..7876d02cb --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "flux_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "flux_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "flux_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_wallet.imageset/flux_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_wallet.imageset/flux_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_wallet.imageset/flux_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_wallet.imageset/flux_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_wallet.imageset/flux_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_wallet.imageset/flux_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_wallet.imageset/flux_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_wallet.imageset/flux_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_wallet.imageset/flux_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_wallet.imageset/flux_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_wallet.imageset/flux_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_wallet.imageset/flux_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..ed324ca83 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "flux_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "flux_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "flux_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_wallet_row.imageset/flux_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_wallet_row.imageset/flux_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_wallet_row.imageset/flux_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_wallet_row.imageset/flux_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_wallet_row.imageset/flux_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_wallet_row.imageset/flux_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_wallet_row.imageset/flux_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_wallet_row.imageset/flux_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_wallet_row.imageset/flux_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_wallet_row.imageset/flux_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_wallet_row.imageset/flux_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/flux_wallet_row.imageset/flux_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_notification.imageset/Contents.json new file mode 100644 index 000000000..e2215bad6 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "gt_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "gt_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "gt_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_notification.imageset/gt_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_notification.imageset/gt_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_notification.imageset/gt_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_notification.imageset/gt_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_notification.imageset/gt_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_notification.imageset/gt_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_notification.imageset/gt_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_notification.imageset/gt_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_notification.imageset/gt_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_notification.imageset/gt_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_notification.imageset/gt_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_notification.imageset/gt_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_wallet.imageset/Contents.json new file mode 100644 index 000000000..2c9d58c7f --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "gt_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "gt_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "gt_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_wallet.imageset/gt_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_wallet.imageset/gt_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_wallet.imageset/gt_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_wallet.imageset/gt_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_wallet.imageset/gt_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_wallet.imageset/gt_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_wallet.imageset/gt_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_wallet.imageset/gt_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_wallet.imageset/gt_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_wallet.imageset/gt_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_wallet.imageset/gt_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_wallet.imageset/gt_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..1a7a1ba13 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "gt_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "gt_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "gt_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_wallet_row.imageset/gt_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_wallet_row.imageset/gt_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_wallet_row.imageset/gt_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_wallet_row.imageset/gt_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_wallet_row.imageset/gt_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_wallet_row.imageset/gt_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_wallet_row.imageset/gt_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_wallet_row.imageset/gt_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_wallet_row.imageset/gt_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_wallet_row.imageset/gt_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_wallet_row.imageset/gt_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/gt_wallet_row.imageset/gt_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_notification.imageset/Contents.json new file mode 100644 index 000000000..e85c2fc14 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "hot_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "hot_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "hot_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_notification.imageset/hot_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_notification.imageset/hot_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_notification.imageset/hot_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_notification.imageset/hot_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_notification.imageset/hot_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_notification.imageset/hot_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_notification.imageset/hot_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_notification.imageset/hot_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_notification.imageset/hot_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_notification.imageset/hot_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_notification.imageset/hot_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_notification.imageset/hot_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_wallet.imageset/Contents.json new file mode 100644 index 000000000..4e8e780ae --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "hot_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "hot_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "hot_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_wallet.imageset/hot_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_wallet.imageset/hot_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_wallet.imageset/hot_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_wallet.imageset/hot_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_wallet.imageset/hot_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_wallet.imageset/hot_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_wallet.imageset/hot_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_wallet.imageset/hot_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_wallet.imageset/hot_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_wallet.imageset/hot_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_wallet.imageset/hot_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_wallet.imageset/hot_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..e73fc26df --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "hot_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "hot_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "hot_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_wallet_row.imageset/hot_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_wallet_row.imageset/hot_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_wallet_row.imageset/hot_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_wallet_row.imageset/hot_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_wallet_row.imageset/hot_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_wallet_row.imageset/hot_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_wallet_row.imageset/hot_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_wallet_row.imageset/hot_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_wallet_row.imageset/hot_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_wallet_row.imageset/hot_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_wallet_row.imageset/hot_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/hot_wallet_row.imageset/hot_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_notification.imageset/Contents.json new file mode 100644 index 000000000..a4f396530 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "inj_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "inj_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "inj_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_notification.imageset/inj_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_notification.imageset/inj_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_notification.imageset/inj_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_notification.imageset/inj_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_notification.imageset/inj_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_notification.imageset/inj_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_notification.imageset/inj_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_notification.imageset/inj_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_notification.imageset/inj_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_notification.imageset/inj_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_notification.imageset/inj_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_notification.imageset/inj_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_wallet.imageset/Contents.json new file mode 100644 index 000000000..c20c015c7 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "inj_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "inj_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "inj_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_wallet.imageset/inj_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_wallet.imageset/inj_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_wallet.imageset/inj_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_wallet.imageset/inj_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_wallet.imageset/inj_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_wallet.imageset/inj_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_wallet.imageset/inj_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_wallet.imageset/inj_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_wallet.imageset/inj_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_wallet.imageset/inj_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_wallet.imageset/inj_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_wallet.imageset/inj_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..0033bdbee --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "inj_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "inj_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "inj_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_wallet_row.imageset/inj_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_wallet_row.imageset/inj_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_wallet_row.imageset/inj_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_wallet_row.imageset/inj_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_wallet_row.imageset/inj_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_wallet_row.imageset/inj_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_wallet_row.imageset/inj_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_wallet_row.imageset/inj_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_wallet_row.imageset/inj_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_wallet_row.imageset/inj_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_wallet_row.imageset/inj_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/inj_wallet_row.imageset/inj_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_notification.imageset/Contents.json new file mode 100644 index 000000000..57f98e3ad --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "klayr_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "klayr_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "klayr_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_notification.imageset/klayr_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_notification.imageset/klayr_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_notification.imageset/klayr_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_notification.imageset/klayr_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_notification.imageset/klayr_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_notification.imageset/klayr_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_notification.imageset/klayr_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_notification.imageset/klayr_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_notification.imageset/klayr_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_notification.imageset/klayr_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_notification.imageset/klayr_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_notification.imageset/klayr_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_wallet.imageset/Contents.json new file mode 100644 index 000000000..7ad77d437 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "klayr_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "klayr_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "klayr_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_wallet.imageset/klayr_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_wallet.imageset/klayr_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_wallet.imageset/klayr_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_wallet.imageset/klayr_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_wallet.imageset/klayr_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_wallet.imageset/klayr_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_wallet.imageset/klayr_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_wallet.imageset/klayr_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_wallet.imageset/klayr_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_wallet.imageset/klayr_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_wallet.imageset/klayr_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_wallet.imageset/klayr_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..b1ec20a1b --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "klayr_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "klayr_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "klayr_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_wallet_row.imageset/klayr_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_wallet_row.imageset/klayr_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_wallet_row.imageset/klayr_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_wallet_row.imageset/klayr_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_wallet_row.imageset/klayr_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_wallet_row.imageset/klayr_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_wallet_row.imageset/klayr_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_wallet_row.imageset/klayr_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_wallet_row.imageset/klayr_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_wallet_row.imageset/klayr_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_wallet_row.imageset/klayr_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/klayr_wallet_row.imageset/klayr_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_notification.imageset/Contents.json new file mode 100644 index 000000000..0c2e7651f --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "link_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "link_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "link_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_notification.imageset/link_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_notification.imageset/link_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_notification.imageset/link_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_notification.imageset/link_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_notification.imageset/link_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_notification.imageset/link_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_notification.imageset/link_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_notification.imageset/link_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_notification.imageset/link_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_notification.imageset/link_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_notification.imageset/link_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_notification.imageset/link_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_wallet.imageset/Contents.json new file mode 100644 index 000000000..22c5a7f8e --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "link_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "link_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "link_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_wallet.imageset/link_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_wallet.imageset/link_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_wallet.imageset/link_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_wallet.imageset/link_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_wallet.imageset/link_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_wallet.imageset/link_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_wallet.imageset/link_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_wallet.imageset/link_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_wallet.imageset/link_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_wallet.imageset/link_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_wallet.imageset/link_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_wallet.imageset/link_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..283fc2df1 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "link_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "link_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "link_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_wallet_row.imageset/link_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_wallet_row.imageset/link_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_wallet_row.imageset/link_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_wallet_row.imageset/link_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_wallet_row.imageset/link_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_wallet_row.imageset/link_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_wallet_row.imageset/link_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_wallet_row.imageset/link_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_wallet_row.imageset/link_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_wallet_row.imageset/link_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_wallet_row.imageset/link_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/link_wallet_row.imageset/link_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_notification.imageset/Contents.json new file mode 100644 index 000000000..781852257 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "mana_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "mana_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "mana_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_notification.imageset/mana_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_notification.imageset/mana_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_notification.imageset/mana_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_notification.imageset/mana_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_notification.imageset/mana_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_notification.imageset/mana_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_notification.imageset/mana_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_notification.imageset/mana_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_notification.imageset/mana_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_notification.imageset/mana_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_notification.imageset/mana_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_notification.imageset/mana_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_wallet.imageset/Contents.json new file mode 100644 index 000000000..e4bdb8272 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "mana_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "mana_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "mana_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_wallet.imageset/mana_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_wallet.imageset/mana_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_wallet.imageset/mana_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_wallet.imageset/mana_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_wallet.imageset/mana_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_wallet.imageset/mana_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_wallet.imageset/mana_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_wallet.imageset/mana_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_wallet.imageset/mana_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_wallet.imageset/mana_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_wallet.imageset/mana_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_wallet.imageset/mana_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..43a4046cb --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "mana_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "mana_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "mana_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_wallet_row.imageset/mana_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_wallet_row.imageset/mana_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_wallet_row.imageset/mana_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_wallet_row.imageset/mana_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_wallet_row.imageset/mana_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_wallet_row.imageset/mana_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_wallet_row.imageset/mana_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_wallet_row.imageset/mana_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_wallet_row.imageset/mana_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_wallet_row.imageset/mana_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_wallet_row.imageset/mana_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/mana_wallet_row.imageset/mana_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_notification.imageset/Contents.json new file mode 100644 index 000000000..3383ccca8 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "matic_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "matic_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "matic_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_notification.imageset/matic_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_notification.imageset/matic_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_notification.imageset/matic_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_notification.imageset/matic_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_notification.imageset/matic_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_notification.imageset/matic_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_notification.imageset/matic_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_notification.imageset/matic_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_notification.imageset/matic_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_notification.imageset/matic_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_notification.imageset/matic_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_notification.imageset/matic_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_wallet.imageset/Contents.json new file mode 100644 index 000000000..efe1eb8fa --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "matic_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "matic_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "matic_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_wallet.imageset/matic_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_wallet.imageset/matic_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_wallet.imageset/matic_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_wallet.imageset/matic_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_wallet.imageset/matic_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_wallet.imageset/matic_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_wallet.imageset/matic_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_wallet.imageset/matic_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_wallet.imageset/matic_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_wallet.imageset/matic_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_wallet.imageset/matic_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_wallet.imageset/matic_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..f5d05ffd7 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "matic_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "matic_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "matic_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_wallet_row.imageset/matic_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_wallet_row.imageset/matic_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_wallet_row.imageset/matic_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_wallet_row.imageset/matic_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_wallet_row.imageset/matic_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_wallet_row.imageset/matic_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_wallet_row.imageset/matic_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_wallet_row.imageset/matic_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_wallet_row.imageset/matic_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_wallet_row.imageset/matic_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_wallet_row.imageset/matic_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/matic_wallet_row.imageset/matic_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_notification.imageset/Contents.json new file mode 100644 index 000000000..11a078ee6 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "paxg_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "paxg_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "paxg_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_notification.imageset/paxg_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_notification.imageset/paxg_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_notification.imageset/paxg_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_notification.imageset/paxg_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_notification.imageset/paxg_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_notification.imageset/paxg_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_notification.imageset/paxg_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_notification.imageset/paxg_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_notification.imageset/paxg_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_notification.imageset/paxg_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_notification.imageset/paxg_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_notification.imageset/paxg_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_wallet.imageset/Contents.json new file mode 100644 index 000000000..9a562d42d --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "paxg_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "paxg_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "paxg_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_wallet.imageset/paxg_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_wallet.imageset/paxg_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_wallet.imageset/paxg_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_wallet.imageset/paxg_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_wallet.imageset/paxg_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_wallet.imageset/paxg_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_wallet.imageset/paxg_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_wallet.imageset/paxg_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_wallet.imageset/paxg_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_wallet.imageset/paxg_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_wallet.imageset/paxg_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_wallet.imageset/paxg_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..597115b79 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "paxg_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "paxg_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "paxg_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_wallet_row.imageset/paxg_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_wallet_row.imageset/paxg_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_wallet_row.imageset/paxg_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_wallet_row.imageset/paxg_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_wallet_row.imageset/paxg_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_wallet_row.imageset/paxg_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_wallet_row.imageset/paxg_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_wallet_row.imageset/paxg_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_wallet_row.imageset/paxg_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_wallet_row.imageset/paxg_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_wallet_row.imageset/paxg_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/paxg_wallet_row.imageset/paxg_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_notification.imageset/Contents.json new file mode 100644 index 000000000..b6393cd6d --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_notification.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { "filename" : "qnt_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "qnt_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "qnt_notification@3x.png", "idiom" : "universal", "scale" : "3x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "qnt_notification_dark.png", "idiom": "universal", "scale": "1x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "qnt_notification_dark@2x.png", "idiom": "universal", "scale": "2x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "qnt_notification_dark@3x.png", "idiom": "universal", "scale": "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_notification.imageset/qnt_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_notification.imageset/qnt_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_notification.imageset/qnt_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_notification.imageset/qnt_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_notification.imageset/qnt_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_notification.imageset/qnt_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_notification.imageset/qnt_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_notification.imageset/qnt_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_notification.imageset/qnt_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_notification.imageset/qnt_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_notification.imageset/qnt_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_notification.imageset/qnt_notification@3x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_notification.imageset/qnt_notification_dark.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_notification.imageset/qnt_notification_dark.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_notification.imageset/qnt_notification_dark.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_notification.imageset/qnt_notification_dark.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_notification.imageset/qnt_notification_dark@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_notification.imageset/qnt_notification_dark@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_notification.imageset/qnt_notification_dark@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_notification.imageset/qnt_notification_dark@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_notification.imageset/qnt_notification_dark@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_notification.imageset/qnt_notification_dark@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_notification.imageset/qnt_notification_dark@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_notification.imageset/qnt_notification_dark@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet.imageset/Contents.json new file mode 100644 index 000000000..86d4f3567 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { "filename" : "qnt_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "qnt_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "qnt_wallet@3x.png", "idiom" : "universal", "scale" : "3x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "qnt_wallet_dark.png", "idiom": "universal", "scale": "1x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "qnt_wallet_dark@2x.png", "idiom": "universal", "scale": "2x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "qnt_wallet_dark@3x.png", "idiom": "universal", "scale": "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet.imageset/qnt_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet.imageset/qnt_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet.imageset/qnt_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet.imageset/qnt_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet.imageset/qnt_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet.imageset/qnt_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet.imageset/qnt_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet.imageset/qnt_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet.imageset/qnt_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet.imageset/qnt_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet.imageset/qnt_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet.imageset/qnt_wallet@3x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet.imageset/qnt_wallet_dark.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet.imageset/qnt_wallet_dark.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet.imageset/qnt_wallet_dark.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet.imageset/qnt_wallet_dark.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet.imageset/qnt_wallet_dark@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet.imageset/qnt_wallet_dark@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet.imageset/qnt_wallet_dark@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet.imageset/qnt_wallet_dark@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet.imageset/qnt_wallet_dark@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet.imageset/qnt_wallet_dark@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet.imageset/qnt_wallet_dark@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet.imageset/qnt_wallet_dark@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..832a107ac --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet_row.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { "filename" : "qnt_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "qnt_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "qnt_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "qnt_wallet_row_dark.png", "idiom": "universal", "scale": "1x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "qnt_wallet_row_dark@2x.png", "idiom": "universal", "scale": "2x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "qnt_wallet_row_dark@3x.png", "idiom": "universal", "scale": "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet_row.imageset/qnt_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet_row.imageset/qnt_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet_row.imageset/qnt_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet_row.imageset/qnt_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet_row.imageset/qnt_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet_row.imageset/qnt_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet_row.imageset/qnt_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet_row.imageset/qnt_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet_row.imageset/qnt_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet_row.imageset/qnt_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet_row.imageset/qnt_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet_row.imageset/qnt_wallet_row@3x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet_row.imageset/qnt_wallet_row_dark.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet_row.imageset/qnt_wallet_row_dark.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet_row.imageset/qnt_wallet_row_dark.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet_row.imageset/qnt_wallet_row_dark.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet_row.imageset/qnt_wallet_row_dark@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet_row.imageset/qnt_wallet_row_dark@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet_row.imageset/qnt_wallet_row_dark@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet_row.imageset/qnt_wallet_row_dark@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet_row.imageset/qnt_wallet_row_dark@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet_row.imageset/qnt_wallet_row_dark@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet_row.imageset/qnt_wallet_row_dark@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/qnt_wallet_row.imageset/qnt_wallet_row_dark@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_notification.imageset/Contents.json new file mode 100644 index 000000000..e741e8000 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_notification.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { "filename" : "ren_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "ren_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "ren_notification@3x.png", "idiom" : "universal", "scale" : "3x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "ren_notification_dark.png", "idiom": "universal", "scale": "1x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "ren_notification_dark@2x.png", "idiom": "universal", "scale": "2x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "ren_notification_dark@3x.png", "idiom": "universal", "scale": "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_notification.imageset/ren_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_notification.imageset/ren_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_notification.imageset/ren_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_notification.imageset/ren_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_notification.imageset/ren_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_notification.imageset/ren_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_notification.imageset/ren_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_notification.imageset/ren_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_notification.imageset/ren_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_notification.imageset/ren_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_notification.imageset/ren_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_notification.imageset/ren_notification@3x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_notification.imageset/ren_notification_dark.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_notification.imageset/ren_notification_dark.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_notification.imageset/ren_notification_dark.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_notification.imageset/ren_notification_dark.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_notification.imageset/ren_notification_dark@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_notification.imageset/ren_notification_dark@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_notification.imageset/ren_notification_dark@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_notification.imageset/ren_notification_dark@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_notification.imageset/ren_notification_dark@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_notification.imageset/ren_notification_dark@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_notification.imageset/ren_notification_dark@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_notification.imageset/ren_notification_dark@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet.imageset/Contents.json new file mode 100644 index 000000000..71271b7d2 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { "filename" : "ren_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "ren_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "ren_wallet@3x.png", "idiom" : "universal", "scale" : "3x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "ren_wallet_dark.png", "idiom": "universal", "scale": "1x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "ren_wallet_dark@2x.png", "idiom": "universal", "scale": "2x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "ren_wallet_dark@3x.png", "idiom": "universal", "scale": "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet.imageset/ren_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet.imageset/ren_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet.imageset/ren_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet.imageset/ren_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet.imageset/ren_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet.imageset/ren_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet.imageset/ren_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet.imageset/ren_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet.imageset/ren_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet.imageset/ren_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet.imageset/ren_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet.imageset/ren_wallet@3x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet.imageset/ren_wallet_dark.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet.imageset/ren_wallet_dark.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet.imageset/ren_wallet_dark.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet.imageset/ren_wallet_dark.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet.imageset/ren_wallet_dark@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet.imageset/ren_wallet_dark@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet.imageset/ren_wallet_dark@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet.imageset/ren_wallet_dark@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet.imageset/ren_wallet_dark@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet.imageset/ren_wallet_dark@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet.imageset/ren_wallet_dark@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet.imageset/ren_wallet_dark@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..8a387eb13 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet_row.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { "filename" : "ren_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "ren_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "ren_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "ren_wallet_row_dark.png", "idiom": "universal", "scale": "1x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "ren_wallet_row_dark@2x.png", "idiom": "universal", "scale": "2x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "ren_wallet_row_dark@3x.png", "idiom": "universal", "scale": "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet_row.imageset/ren_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet_row.imageset/ren_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet_row.imageset/ren_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet_row.imageset/ren_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet_row.imageset/ren_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet_row.imageset/ren_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet_row.imageset/ren_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet_row.imageset/ren_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet_row.imageset/ren_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet_row.imageset/ren_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet_row.imageset/ren_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet_row.imageset/ren_wallet_row@3x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet_row.imageset/ren_wallet_row_dark.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet_row.imageset/ren_wallet_row_dark.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet_row.imageset/ren_wallet_row_dark.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet_row.imageset/ren_wallet_row_dark.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet_row.imageset/ren_wallet_row_dark@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet_row.imageset/ren_wallet_row_dark@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet_row.imageset/ren_wallet_row_dark@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet_row.imageset/ren_wallet_row_dark@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet_row.imageset/ren_wallet_row_dark@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet_row.imageset/ren_wallet_row_dark@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet_row.imageset/ren_wallet_row_dark@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/ren_wallet_row.imageset/ren_wallet_row_dark@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_notification.imageset/Contents.json new file mode 100644 index 000000000..a16441ccf --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_notification.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { "filename" : "skl_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "skl_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "skl_notification@3x.png", "idiom" : "universal", "scale" : "3x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "skl_notification_dark.png", "idiom": "universal", "scale": "1x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "skl_notification_dark@2x.png", "idiom": "universal", "scale": "2x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "skl_notification_dark@3x.png", "idiom": "universal", "scale": "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_notification.imageset/skl_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_notification.imageset/skl_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_notification.imageset/skl_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_notification.imageset/skl_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_notification.imageset/skl_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_notification.imageset/skl_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_notification.imageset/skl_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_notification.imageset/skl_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_notification.imageset/skl_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_notification.imageset/skl_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_notification.imageset/skl_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_notification.imageset/skl_notification@3x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_notification.imageset/skl_notification_dark.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_notification.imageset/skl_notification_dark.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_notification.imageset/skl_notification_dark.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_notification.imageset/skl_notification_dark.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_notification.imageset/skl_notification_dark@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_notification.imageset/skl_notification_dark@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_notification.imageset/skl_notification_dark@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_notification.imageset/skl_notification_dark@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_notification.imageset/skl_notification_dark@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_notification.imageset/skl_notification_dark@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_notification.imageset/skl_notification_dark@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_notification.imageset/skl_notification_dark@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet.imageset/Contents.json new file mode 100644 index 000000000..c7b5e437e --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { "filename" : "skl_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "skl_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "skl_wallet@3x.png", "idiom" : "universal", "scale" : "3x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "skl_wallet_dark.png", "idiom": "universal", "scale": "1x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "skl_wallet_dark@2x.png", "idiom": "universal", "scale": "2x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "skl_wallet_dark@3x.png", "idiom": "universal", "scale": "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet.imageset/skl_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet.imageset/skl_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet.imageset/skl_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet.imageset/skl_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet.imageset/skl_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet.imageset/skl_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet.imageset/skl_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet.imageset/skl_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet.imageset/skl_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet.imageset/skl_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet.imageset/skl_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet.imageset/skl_wallet@3x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet.imageset/skl_wallet_dark.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet.imageset/skl_wallet_dark.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet.imageset/skl_wallet_dark.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet.imageset/skl_wallet_dark.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet.imageset/skl_wallet_dark@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet.imageset/skl_wallet_dark@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet.imageset/skl_wallet_dark@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet.imageset/skl_wallet_dark@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet.imageset/skl_wallet_dark@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet.imageset/skl_wallet_dark@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet.imageset/skl_wallet_dark@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet.imageset/skl_wallet_dark@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..b90056693 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet_row.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { "filename" : "skl_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "skl_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "skl_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "skl_wallet_row_dark.png", "idiom": "universal", "scale": "1x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "skl_wallet_row_dark@2x.png", "idiom": "universal", "scale": "2x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "skl_wallet_row_dark@3x.png", "idiom": "universal", "scale": "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet_row.imageset/skl_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet_row.imageset/skl_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet_row.imageset/skl_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet_row.imageset/skl_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet_row.imageset/skl_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet_row.imageset/skl_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet_row.imageset/skl_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet_row.imageset/skl_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet_row.imageset/skl_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet_row.imageset/skl_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet_row.imageset/skl_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet_row.imageset/skl_wallet_row@3x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet_row.imageset/skl_wallet_row_dark.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet_row.imageset/skl_wallet_row_dark.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet_row.imageset/skl_wallet_row_dark.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet_row.imageset/skl_wallet_row_dark.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet_row.imageset/skl_wallet_row_dark@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet_row.imageset/skl_wallet_row_dark@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet_row.imageset/skl_wallet_row_dark@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet_row.imageset/skl_wallet_row_dark@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet_row.imageset/skl_wallet_row_dark@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet_row.imageset/skl_wallet_row_dark@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet_row.imageset/skl_wallet_row_dark@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/skl_wallet_row.imageset/skl_wallet_row_dark@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_notification.imageset/Contents.json new file mode 100644 index 000000000..58200b2c3 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "snt_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "snt_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "snt_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_notification.imageset/snt_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_notification.imageset/snt_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_notification.imageset/snt_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_notification.imageset/snt_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_notification.imageset/snt_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_notification.imageset/snt_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_notification.imageset/snt_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_notification.imageset/snt_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_notification.imageset/snt_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_notification.imageset/snt_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_notification.imageset/snt_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_notification.imageset/snt_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_wallet.imageset/Contents.json new file mode 100644 index 000000000..a277696d8 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "snt_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "snt_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "snt_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_wallet.imageset/snt_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_wallet.imageset/snt_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_wallet.imageset/snt_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_wallet.imageset/snt_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_wallet.imageset/snt_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_wallet.imageset/snt_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_wallet.imageset/snt_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_wallet.imageset/snt_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_wallet.imageset/snt_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_wallet.imageset/snt_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_wallet.imageset/snt_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_wallet.imageset/snt_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..1062619c0 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "snt_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "snt_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "snt_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_wallet_row.imageset/snt_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_wallet_row.imageset/snt_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_wallet_row.imageset/snt_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_wallet_row.imageset/snt_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_wallet_row.imageset/snt_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_wallet_row.imageset/snt_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_wallet_row.imageset/snt_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_wallet_row.imageset/snt_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_wallet_row.imageset/snt_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_wallet_row.imageset/snt_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_wallet_row.imageset/snt_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snt_wallet_row.imageset/snt_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_notification.imageset/Contents.json new file mode 100644 index 000000000..1be8bca07 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_notification.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { "filename" : "snx_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "snx_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "snx_notification@3x.png", "idiom" : "universal", "scale" : "3x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "snx_notification_dark.png", "idiom": "universal", "scale": "1x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "snx_notification_dark@2x.png", "idiom": "universal", "scale": "2x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "snx_notification_dark@3x.png", "idiom": "universal", "scale": "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_notification.imageset/snx_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_notification.imageset/snx_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_notification.imageset/snx_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_notification.imageset/snx_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_notification.imageset/snx_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_notification.imageset/snx_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_notification.imageset/snx_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_notification.imageset/snx_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_notification.imageset/snx_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_notification.imageset/snx_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_notification.imageset/snx_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_notification.imageset/snx_notification@3x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_notification.imageset/snx_notification_dark.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_notification.imageset/snx_notification_dark.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_notification.imageset/snx_notification_dark.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_notification.imageset/snx_notification_dark.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_notification.imageset/snx_notification_dark@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_notification.imageset/snx_notification_dark@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_notification.imageset/snx_notification_dark@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_notification.imageset/snx_notification_dark@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_notification.imageset/snx_notification_dark@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_notification.imageset/snx_notification_dark@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_notification.imageset/snx_notification_dark@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_notification.imageset/snx_notification_dark@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet.imageset/Contents.json new file mode 100644 index 000000000..eb48b02c0 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { "filename" : "snx_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "snx_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "snx_wallet@3x.png", "idiom" : "universal", "scale" : "3x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "snx_wallet_dark.png", "idiom": "universal", "scale": "1x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "snx_wallet_dark@2x.png", "idiom": "universal", "scale": "2x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "snx_wallet_dark@3x.png", "idiom": "universal", "scale": "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet.imageset/snx_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet.imageset/snx_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet.imageset/snx_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet.imageset/snx_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet.imageset/snx_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet.imageset/snx_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet.imageset/snx_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet.imageset/snx_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet.imageset/snx_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet.imageset/snx_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet.imageset/snx_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet.imageset/snx_wallet@3x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet.imageset/snx_wallet_dark.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet.imageset/snx_wallet_dark.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet.imageset/snx_wallet_dark.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet.imageset/snx_wallet_dark.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet.imageset/snx_wallet_dark@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet.imageset/snx_wallet_dark@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet.imageset/snx_wallet_dark@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet.imageset/snx_wallet_dark@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet.imageset/snx_wallet_dark@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet.imageset/snx_wallet_dark@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet.imageset/snx_wallet_dark@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet.imageset/snx_wallet_dark@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..8f58e76d6 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet_row.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { "filename" : "snx_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "snx_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "snx_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "snx_wallet_row_dark.png", "idiom": "universal", "scale": "1x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "snx_wallet_row_dark@2x.png", "idiom": "universal", "scale": "2x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "snx_wallet_row_dark@3x.png", "idiom": "universal", "scale": "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet_row.imageset/snx_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet_row.imageset/snx_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet_row.imageset/snx_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet_row.imageset/snx_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet_row.imageset/snx_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet_row.imageset/snx_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet_row.imageset/snx_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet_row.imageset/snx_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet_row.imageset/snx_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet_row.imageset/snx_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet_row.imageset/snx_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet_row.imageset/snx_wallet_row@3x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet_row.imageset/snx_wallet_row_dark.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet_row.imageset/snx_wallet_row_dark.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet_row.imageset/snx_wallet_row_dark.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet_row.imageset/snx_wallet_row_dark.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet_row.imageset/snx_wallet_row_dark@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet_row.imageset/snx_wallet_row_dark@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet_row.imageset/snx_wallet_row_dark@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet_row.imageset/snx_wallet_row_dark@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet_row.imageset/snx_wallet_row_dark@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet_row.imageset/snx_wallet_row_dark@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet_row.imageset/snx_wallet_row_dark@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/snx_wallet_row.imageset/snx_wallet_row_dark@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_notification.imageset/Contents.json new file mode 100644 index 000000000..dece73300 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "storj_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "storj_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "storj_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_notification.imageset/storj_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_notification.imageset/storj_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_notification.imageset/storj_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_notification.imageset/storj_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_notification.imageset/storj_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_notification.imageset/storj_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_notification.imageset/storj_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_notification.imageset/storj_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_notification.imageset/storj_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_notification.imageset/storj_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_notification.imageset/storj_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_notification.imageset/storj_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_wallet.imageset/Contents.json new file mode 100644 index 000000000..5e8764937 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "storj_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "storj_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "storj_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_wallet.imageset/storj_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_wallet.imageset/storj_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_wallet.imageset/storj_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_wallet.imageset/storj_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_wallet.imageset/storj_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_wallet.imageset/storj_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_wallet.imageset/storj_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_wallet.imageset/storj_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_wallet.imageset/storj_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_wallet.imageset/storj_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_wallet.imageset/storj_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_wallet.imageset/storj_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..4b40a48d2 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "storj_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "storj_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "storj_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_wallet_row.imageset/storj_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_wallet_row.imageset/storj_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_wallet_row.imageset/storj_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_wallet_row.imageset/storj_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_wallet_row.imageset/storj_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_wallet_row.imageset/storj_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_wallet_row.imageset/storj_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_wallet_row.imageset/storj_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_wallet_row.imageset/storj_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_wallet_row.imageset/storj_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_wallet_row.imageset/storj_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/storj_wallet_row.imageset/storj_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_notification.imageset/Contents.json new file mode 100644 index 000000000..d3fa6ac1b --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "tusd_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "tusd_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "tusd_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_notification.imageset/tusd_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_notification.imageset/tusd_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_notification.imageset/tusd_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_notification.imageset/tusd_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_notification.imageset/tusd_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_notification.imageset/tusd_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_notification.imageset/tusd_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_notification.imageset/tusd_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_notification.imageset/tusd_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_notification.imageset/tusd_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_notification.imageset/tusd_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_notification.imageset/tusd_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_wallet.imageset/Contents.json new file mode 100644 index 000000000..9a6aac080 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "tusd_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "tusd_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "tusd_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_wallet.imageset/tusd_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_wallet.imageset/tusd_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_wallet.imageset/tusd_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_wallet.imageset/tusd_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_wallet.imageset/tusd_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_wallet.imageset/tusd_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_wallet.imageset/tusd_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_wallet.imageset/tusd_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_wallet.imageset/tusd_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_wallet.imageset/tusd_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_wallet.imageset/tusd_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_wallet.imageset/tusd_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..dbb0c833b --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "tusd_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "tusd_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "tusd_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_wallet_row.imageset/tusd_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_wallet_row.imageset/tusd_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_wallet_row.imageset/tusd_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_wallet_row.imageset/tusd_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_wallet_row.imageset/tusd_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_wallet_row.imageset/tusd_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_wallet_row.imageset/tusd_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_wallet_row.imageset/tusd_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_wallet_row.imageset/tusd_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_wallet_row.imageset/tusd_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_wallet_row.imageset/tusd_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/tusd_wallet_row.imageset/tusd_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_notification.imageset/Contents.json new file mode 100644 index 000000000..2247ec7a9 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "uni_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "uni_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "uni_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_notification.imageset/uni_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_notification.imageset/uni_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_notification.imageset/uni_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_notification.imageset/uni_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_notification.imageset/uni_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_notification.imageset/uni_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_notification.imageset/uni_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_notification.imageset/uni_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_notification.imageset/uni_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_notification.imageset/uni_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_notification.imageset/uni_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_notification.imageset/uni_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_wallet.imageset/Contents.json new file mode 100644 index 000000000..590a4e18c --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "uni_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "uni_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "uni_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_wallet.imageset/uni_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_wallet.imageset/uni_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_wallet.imageset/uni_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_wallet.imageset/uni_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_wallet.imageset/uni_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_wallet.imageset/uni_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_wallet.imageset/uni_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_wallet.imageset/uni_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_wallet.imageset/uni_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_wallet.imageset/uni_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_wallet.imageset/uni_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_wallet.imageset/uni_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..b7cf5a123 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "uni_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "uni_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "uni_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_wallet_row.imageset/uni_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_wallet_row.imageset/uni_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_wallet_row.imageset/uni_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_wallet_row.imageset/uni_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_wallet_row.imageset/uni_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_wallet_row.imageset/uni_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_wallet_row.imageset/uni_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_wallet_row.imageset/uni_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_wallet_row.imageset/uni_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_wallet_row.imageset/uni_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_wallet_row.imageset/uni_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/uni_wallet_row.imageset/uni_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_notification.imageset/Contents.json new file mode 100644 index 000000000..db90f9048 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "usdc_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "usdc_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "usdc_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_notification.imageset/usdc_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_notification.imageset/usdc_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_notification.imageset/usdc_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_notification.imageset/usdc_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_notification.imageset/usdc_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_notification.imageset/usdc_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_notification.imageset/usdc_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_notification.imageset/usdc_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_notification.imageset/usdc_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_notification.imageset/usdc_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_notification.imageset/usdc_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_notification.imageset/usdc_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_wallet.imageset/Contents.json new file mode 100644 index 000000000..0aa2592e6 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "usdc_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "usdc_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "usdc_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_wallet.imageset/usdc_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_wallet.imageset/usdc_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_wallet.imageset/usdc_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_wallet.imageset/usdc_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_wallet.imageset/usdc_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_wallet.imageset/usdc_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_wallet.imageset/usdc_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_wallet.imageset/usdc_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_wallet.imageset/usdc_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_wallet.imageset/usdc_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_wallet.imageset/usdc_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_wallet.imageset/usdc_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..497f3ab21 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "usdc_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "usdc_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "usdc_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_wallet_row.imageset/usdc_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_wallet_row.imageset/usdc_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_wallet_row.imageset/usdc_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_wallet_row.imageset/usdc_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_wallet_row.imageset/usdc_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_wallet_row.imageset/usdc_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_wallet_row.imageset/usdc_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_wallet_row.imageset/usdc_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_wallet_row.imageset/usdc_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_wallet_row.imageset/usdc_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_wallet_row.imageset/usdc_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdc_wallet_row.imageset/usdc_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_notification.imageset/Contents.json new file mode 100644 index 000000000..c3fc529b5 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "usdp_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "usdp_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "usdp_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_notification.imageset/usdp_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_notification.imageset/usdp_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_notification.imageset/usdp_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_notification.imageset/usdp_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_notification.imageset/usdp_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_notification.imageset/usdp_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_notification.imageset/usdp_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_notification.imageset/usdp_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_notification.imageset/usdp_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_notification.imageset/usdp_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_notification.imageset/usdp_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_notification.imageset/usdp_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_wallet.imageset/Contents.json new file mode 100644 index 000000000..d47b825b8 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "usdp_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "usdp_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "usdp_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_wallet.imageset/usdp_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_wallet.imageset/usdp_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_wallet.imageset/usdp_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_wallet.imageset/usdp_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_wallet.imageset/usdp_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_wallet.imageset/usdp_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_wallet.imageset/usdp_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_wallet.imageset/usdp_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_wallet.imageset/usdp_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_wallet.imageset/usdp_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_wallet.imageset/usdp_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_wallet.imageset/usdp_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..897953d6f --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "usdp_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "usdp_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "usdp_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_wallet_row.imageset/usdp_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_wallet_row.imageset/usdp_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_wallet_row.imageset/usdp_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_wallet_row.imageset/usdp_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_wallet_row.imageset/usdp_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_wallet_row.imageset/usdp_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_wallet_row.imageset/usdp_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_wallet_row.imageset/usdp_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_wallet_row.imageset/usdp_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_wallet_row.imageset/usdp_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_wallet_row.imageset/usdp_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdp_wallet_row.imageset/usdp_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_notification.imageset/Contents.json new file mode 100644 index 000000000..322a6935c --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "usds_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "usds_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "usds_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_notification.imageset/usds_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_notification.imageset/usds_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_notification.imageset/usds_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_notification.imageset/usds_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_notification.imageset/usds_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_notification.imageset/usds_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_notification.imageset/usds_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_notification.imageset/usds_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_notification.imageset/usds_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_notification.imageset/usds_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_notification.imageset/usds_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_notification.imageset/usds_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_wallet.imageset/Contents.json new file mode 100644 index 000000000..57d7c6e95 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "usds_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "usds_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "usds_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_wallet.imageset/usds_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_wallet.imageset/usds_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_wallet.imageset/usds_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_wallet.imageset/usds_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_wallet.imageset/usds_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_wallet.imageset/usds_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_wallet.imageset/usds_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_wallet.imageset/usds_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_wallet.imageset/usds_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_wallet.imageset/usds_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_wallet.imageset/usds_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_wallet.imageset/usds_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..00c5b99e8 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "usds_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "usds_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "usds_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_wallet_row.imageset/usds_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_wallet_row.imageset/usds_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_wallet_row.imageset/usds_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_wallet_row.imageset/usds_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_wallet_row.imageset/usds_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_wallet_row.imageset/usds_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_wallet_row.imageset/usds_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_wallet_row.imageset/usds_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_wallet_row.imageset/usds_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_wallet_row.imageset/usds_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_wallet_row.imageset/usds_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usds_wallet_row.imageset/usds_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_notification.imageset/Contents.json new file mode 100644 index 000000000..5a5eca10b --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "usdt_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "usdt_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "usdt_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_notification.imageset/usdt_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_notification.imageset/usdt_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_notification.imageset/usdt_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_notification.imageset/usdt_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_notification.imageset/usdt_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_notification.imageset/usdt_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_notification.imageset/usdt_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_notification.imageset/usdt_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_notification.imageset/usdt_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_notification.imageset/usdt_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_notification.imageset/usdt_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_notification.imageset/usdt_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_wallet.imageset/Contents.json new file mode 100644 index 000000000..127fbaa21 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "usdt_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "usdt_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "usdt_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_wallet.imageset/usdt_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_wallet.imageset/usdt_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_wallet.imageset/usdt_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_wallet.imageset/usdt_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_wallet.imageset/usdt_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_wallet.imageset/usdt_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_wallet.imageset/usdt_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_wallet.imageset/usdt_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_wallet.imageset/usdt_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_wallet.imageset/usdt_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_wallet.imageset/usdt_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_wallet.imageset/usdt_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..72e57fa30 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "usdt_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "usdt_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "usdt_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_wallet_row.imageset/usdt_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_wallet_row.imageset/usdt_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_wallet_row.imageset/usdt_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_wallet_row.imageset/usdt_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_wallet_row.imageset/usdt_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_wallet_row.imageset/usdt_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_wallet_row.imageset/usdt_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_wallet_row.imageset/usdt_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_wallet_row.imageset/usdt_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_wallet_row.imageset/usdt_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_wallet_row.imageset/usdt_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/usdt_wallet_row.imageset/usdt_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_notification.imageset/Contents.json new file mode 100644 index 000000000..1fadd1091 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_notification.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "verse_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "verse_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "verse_notification@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_notification.imageset/verse_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_notification.imageset/verse_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_notification.imageset/verse_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_notification.imageset/verse_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_notification.imageset/verse_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_notification.imageset/verse_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_notification.imageset/verse_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_notification.imageset/verse_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_notification.imageset/verse_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_notification.imageset/verse_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_notification.imageset/verse_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_notification.imageset/verse_notification@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_wallet.imageset/Contents.json new file mode 100644 index 000000000..c39388101 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_wallet.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "verse_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "verse_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "verse_wallet@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_wallet.imageset/verse_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_wallet.imageset/verse_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_wallet.imageset/verse_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_wallet.imageset/verse_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_wallet.imageset/verse_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_wallet.imageset/verse_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_wallet.imageset/verse_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_wallet.imageset/verse_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_wallet.imageset/verse_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_wallet.imageset/verse_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_wallet.imageset/verse_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_wallet.imageset/verse_wallet@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..447204bf4 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_wallet_row.imageset/Contents.json @@ -0,0 +1,8 @@ +{ + "images" : [ + { "filename" : "verse_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "verse_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "verse_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_wallet_row.imageset/verse_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_wallet_row.imageset/verse_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_wallet_row.imageset/verse_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_wallet_row.imageset/verse_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_wallet_row.imageset/verse_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_wallet_row.imageset/verse_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_wallet_row.imageset/verse_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_wallet_row.imageset/verse_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_wallet_row.imageset/verse_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_wallet_row.imageset/verse_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_wallet_row.imageset/verse_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/verse_wallet_row.imageset/verse_wallet_row@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_notification.imageset/Contents.json new file mode 100644 index 000000000..1beb9acb0 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_notification.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { "filename" : "woo_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "woo_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "woo_notification@3x.png", "idiom" : "universal", "scale" : "3x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "woo_notification_dark.png", "idiom": "universal", "scale": "1x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "woo_notification_dark@2x.png", "idiom": "universal", "scale": "2x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "woo_notification_dark@3x.png", "idiom": "universal", "scale": "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_notification.imageset/woo_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_notification.imageset/woo_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_notification.imageset/woo_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_notification.imageset/woo_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_notification.imageset/woo_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_notification.imageset/woo_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_notification.imageset/woo_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_notification.imageset/woo_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_notification.imageset/woo_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_notification.imageset/woo_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_notification.imageset/woo_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_notification.imageset/woo_notification@3x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_notification.imageset/woo_notification_dark.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_notification.imageset/woo_notification_dark.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_notification.imageset/woo_notification_dark.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_notification.imageset/woo_notification_dark.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_notification.imageset/woo_notification_dark@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_notification.imageset/woo_notification_dark@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_notification.imageset/woo_notification_dark@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_notification.imageset/woo_notification_dark@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_notification.imageset/woo_notification_dark@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_notification.imageset/woo_notification_dark@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_notification.imageset/woo_notification_dark@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_notification.imageset/woo_notification_dark@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet.imageset/Contents.json new file mode 100644 index 000000000..34b8906c5 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { "filename" : "woo_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "woo_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "woo_wallet@3x.png", "idiom" : "universal", "scale" : "3x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "woo_wallet_dark.png", "idiom": "universal", "scale": "1x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "woo_wallet_dark@2x.png", "idiom": "universal", "scale": "2x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "woo_wallet_dark@3x.png", "idiom": "universal", "scale": "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet.imageset/woo_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet.imageset/woo_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet.imageset/woo_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet.imageset/woo_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet.imageset/woo_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet.imageset/woo_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet.imageset/woo_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet.imageset/woo_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet.imageset/woo_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet.imageset/woo_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet.imageset/woo_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet.imageset/woo_wallet@3x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet.imageset/woo_wallet_dark.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet.imageset/woo_wallet_dark.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet.imageset/woo_wallet_dark.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet.imageset/woo_wallet_dark.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet.imageset/woo_wallet_dark@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet.imageset/woo_wallet_dark@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet.imageset/woo_wallet_dark@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet.imageset/woo_wallet_dark@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet.imageset/woo_wallet_dark@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet.imageset/woo_wallet_dark@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet.imageset/woo_wallet_dark@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet.imageset/woo_wallet_dark@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..45ca37bab --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet_row.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { "filename" : "woo_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "woo_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "woo_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "woo_wallet_row_dark.png", "idiom": "universal", "scale": "1x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "woo_wallet_row_dark@2x.png", "idiom": "universal", "scale": "2x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "woo_wallet_row_dark@3x.png", "idiom": "universal", "scale": "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet_row.imageset/woo_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet_row.imageset/woo_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet_row.imageset/woo_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet_row.imageset/woo_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet_row.imageset/woo_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet_row.imageset/woo_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet_row.imageset/woo_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet_row.imageset/woo_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet_row.imageset/woo_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet_row.imageset/woo_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet_row.imageset/woo_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet_row.imageset/woo_wallet_row@3x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet_row.imageset/woo_wallet_row_dark.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet_row.imageset/woo_wallet_row_dark.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet_row.imageset/woo_wallet_row_dark.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet_row.imageset/woo_wallet_row_dark.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet_row.imageset/woo_wallet_row_dark@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet_row.imageset/woo_wallet_row_dark@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet_row.imageset/woo_wallet_row_dark@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet_row.imageset/woo_wallet_row_dark@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet_row.imageset/woo_wallet_row_dark@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet_row.imageset/woo_wallet_row_dark@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet_row.imageset/woo_wallet_row_dark@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/woo_wallet_row.imageset/woo_wallet_row_dark@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_notification.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_notification.imageset/Contents.json new file mode 100644 index 000000000..a32711a6c --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_notification.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { "filename" : "xcn_notification.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "xcn_notification@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "xcn_notification@3x.png", "idiom" : "universal", "scale" : "3x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "xcn_notification_dark.png", "idiom": "universal", "scale": "1x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "xcn_notification_dark@2x.png", "idiom": "universal", "scale": "2x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "xcn_notification_dark@3x.png", "idiom": "universal", "scale": "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_notification.imageset/xcn_notification.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_notification.imageset/xcn_notification.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_notification.imageset/xcn_notification.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_notification.imageset/xcn_notification.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_notification.imageset/xcn_notification@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_notification.imageset/xcn_notification@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_notification.imageset/xcn_notification@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_notification.imageset/xcn_notification@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_notification.imageset/xcn_notification@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_notification.imageset/xcn_notification@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_notification.imageset/xcn_notification@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_notification.imageset/xcn_notification@3x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_notification.imageset/xcn_notification_dark.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_notification.imageset/xcn_notification_dark.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_notification.imageset/xcn_notification_dark.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_notification.imageset/xcn_notification_dark.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_notification.imageset/xcn_notification_dark@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_notification.imageset/xcn_notification_dark@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_notification.imageset/xcn_notification_dark@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_notification.imageset/xcn_notification_dark@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_notification.imageset/xcn_notification_dark@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_notification.imageset/xcn_notification_dark@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_notification.imageset/xcn_notification_dark@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_notification.imageset/xcn_notification_dark@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet.imageset/Contents.json new file mode 100644 index 000000000..fa8d967d4 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { "filename" : "xcn_wallet.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "xcn_wallet@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "xcn_wallet@3x.png", "idiom" : "universal", "scale" : "3x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "xcn_wallet_dark.png", "idiom": "universal", "scale": "1x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "xcn_wallet_dark@2x.png", "idiom": "universal", "scale": "2x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "xcn_wallet_dark@3x.png", "idiom": "universal", "scale": "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet.imageset/xcn_wallet.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet.imageset/xcn_wallet.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet.imageset/xcn_wallet.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet.imageset/xcn_wallet.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet.imageset/xcn_wallet@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet.imageset/xcn_wallet@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet.imageset/xcn_wallet@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet.imageset/xcn_wallet@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet.imageset/xcn_wallet@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet.imageset/xcn_wallet@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet.imageset/xcn_wallet@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet.imageset/xcn_wallet@3x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet.imageset/xcn_wallet_dark.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet.imageset/xcn_wallet_dark.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet.imageset/xcn_wallet_dark.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet.imageset/xcn_wallet_dark.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet.imageset/xcn_wallet_dark@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet.imageset/xcn_wallet_dark@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet.imageset/xcn_wallet_dark@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet.imageset/xcn_wallet_dark@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet.imageset/xcn_wallet_dark@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet.imageset/xcn_wallet_dark@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet.imageset/xcn_wallet_dark@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet.imageset/xcn_wallet_dark@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet_row.imageset/Contents.json b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet_row.imageset/Contents.json new file mode 100644 index 000000000..a67e3c775 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet_row.imageset/Contents.json @@ -0,0 +1,11 @@ +{ + "images" : [ + { "filename" : "xcn_wallet_row.png", "idiom" : "universal", "scale" : "1x" }, + { "filename" : "xcn_wallet_row@2x.png", "idiom" : "universal", "scale" : "2x" }, + { "filename" : "xcn_wallet_row@3x.png", "idiom" : "universal", "scale" : "3x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "xcn_wallet_row_dark.png", "idiom": "universal", "scale": "1x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "xcn_wallet_row_dark@2x.png", "idiom": "universal", "scale": "2x" }, + { "appearances": [{ "appearance": "luminosity", "value": "dark" }], "filename": "xcn_wallet_row_dark@3x.png", "idiom": "universal", "scale": "3x" } + ], + "info": { "author": "xcode", "version": 1 } +} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet_row.imageset/xcn_wallet_row.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet_row.imageset/xcn_wallet_row.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet_row.imageset/xcn_wallet_row.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet_row.imageset/xcn_wallet_row.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet_row.imageset/xcn_wallet_row@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet_row.imageset/xcn_wallet_row@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet_row.imageset/xcn_wallet_row@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet_row.imageset/xcn_wallet_row@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet_row.imageset/xcn_wallet_row@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet_row.imageset/xcn_wallet_row@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet_row.imageset/xcn_wallet_row@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet_row.imageset/xcn_wallet_row@3x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet_row.imageset/xcn_wallet_row_dark.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet_row.imageset/xcn_wallet_row_dark.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet_row.imageset/xcn_wallet_row_dark.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet_row.imageset/xcn_wallet_row_dark.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet_row.imageset/xcn_wallet_row_dark@2x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet_row.imageset/xcn_wallet_row_dark@2x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet_row.imageset/xcn_wallet_row_dark@2x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet_row.imageset/xcn_wallet_row_dark@2x.png diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet_row.imageset/xcn_wallet_row_dark@3x.png b/AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet_row.imageset/xcn_wallet_row_dark@3x.png similarity index 100% rename from CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet_row.imageset/xcn_wallet_row_dark@3x.png rename to AdamantWalletsKit/Sources/AdamantWalletsKit/Wallets.xcassets/xcn_wallet_row.imageset/xcn_wallet_row_dark@3x.png diff --git a/AdamantWalletsKit/Sources/AdamantWalletsKit/WalletsImageProvider.swift b/AdamantWalletsKit/Sources/AdamantWalletsKit/WalletsImageProvider.swift new file mode 100644 index 000000000..5dcc6cec2 --- /dev/null +++ b/AdamantWalletsKit/Sources/AdamantWalletsKit/WalletsImageProvider.swift @@ -0,0 +1,14 @@ +// +// PackageResourceProvider.swift +// AdamantWalletsKit +// +// Created by Владимир Клевцов on 17.1.25.. +// +import Foundation +import UIKit + +public enum WalletsImageProvider { + static public func image(named name: String) -> UIImage? { + return UIImage(named: name, in: .module, with: nil) + } +} diff --git a/BitcoinKit/Package.swift b/BitcoinKit/Package.swift index b34d9ebf8..fca07bd68 100644 --- a/BitcoinKit/Package.swift +++ b/BitcoinKit/Package.swift @@ -11,7 +11,7 @@ let package = Package( ], dependencies: [ .package(name: "OpenSSL", url: "https://github.com/krzyzanowskim/OpenSSL.git", .upToNextMinor(from: "1.1.180")), - .package(name: "Web3swift", url: "https://github.com/skywinder/web3swift.git", .upToNextMajor(from: "3.0.0")) //, + .package(name: "Web3swift", url: "https://github.com/skywinder/web3swift.git", .upToNextMajor(from: "3.0.0")) //, ], targets: [ .target( diff --git a/BitcoinKit/Sources/BitcoinKit/Core/BigNumber.swift b/BitcoinKit/Sources/BitcoinKit/Core/BigNumber.swift index 578af4cc1..b79b02000 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/BigNumber.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/BigNumber.swift @@ -7,10 +7,11 @@ // import Foundation + #if BitcoinKitXcode -import BitcoinKit.Private + import BitcoinKit.Private #else -import BitcoinKitPrivate + import BitcoinKitPrivate #endif public struct BigNumber { @@ -51,8 +52,8 @@ extension BigNumber: Comparable { } } -private extension Int32 { - func toBigNum() -> Data { +extension Int32 { + fileprivate func toBigNum() -> Data { let isNegative: Bool = self < 0 var value: UInt32 = isNegative ? UInt32(-self) : UInt32(self) @@ -80,8 +81,8 @@ private extension Int32 { } } -private extension Data { - func toInt32() -> Int32 { +extension Data { + fileprivate func toInt32() -> Int32 { guard !self.isEmpty else { return 0 } @@ -100,6 +101,6 @@ private extension Data { bytes.append(last) let value: Int32 = Data(bytes).to(type: Int32.self) - return isNegative ? -value: value + return isNegative ? -value : value } } diff --git a/BitcoinKit/Sources/BitcoinKit/Core/BitcoinError.swift b/BitcoinKit/Sources/BitcoinKit/Core/BitcoinError.swift index c9d843a22..c04b49ee4 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/BitcoinError.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/BitcoinError.swift @@ -1,6 +1,6 @@ // // BitcoinError.swift -// +// // // Created by Andrey Golubenko on 06.06.2023. // @@ -13,7 +13,7 @@ enum BitcoinError: LocalizedError { case invalidChecksum case wrongAddressPrefix case list(errors: [Error]) - + var errorDescription: String? { switch self { case .unknownAddressType: @@ -25,7 +25,8 @@ enum BitcoinError: LocalizedError { case .wrongAddressPrefix: return "Wrong address prefix" case let .list(errors): - return errors + return + errors .map { $0.localizedDescription } .joined(separator: ". ") } diff --git a/BitcoinKit/Sources/BitcoinKit/Core/BloomFilter.swift b/BitcoinKit/Sources/BitcoinKit/Core/BloomFilter.swift index 934042920..1506a942b 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/BloomFilter.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/BloomFilter.swift @@ -47,7 +47,7 @@ public struct BloomFilter { public mutating func insert(_ data: Data) { for i in 0..> 3] |= (1 << (7 & nIndex)) } diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Crypto.swift b/BitcoinKit/Sources/BitcoinKit/Core/Crypto.swift index d4d3e362f..ff325f6dc 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Crypto.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Crypto.swift @@ -24,10 +24,11 @@ // import Foundation + #if BitcoinKitXcode -import BitcoinKit.Private + import BitcoinKit.Private #else -import BitcoinKitPrivate + import BitcoinKitPrivate #endif public struct Crypto { @@ -57,17 +58,17 @@ public struct Crypto { public static func sign(_ data: Data, privateKey: PrivateKey) throws -> Data { #if BitcoinKitXcode - return _Crypto.signMessage(data, withPrivateKey: privateKey.data) + return _Crypto.signMessage(data, withPrivateKey: privateKey.data) #else - return try _Crypto.signMessage(data, withPrivateKey: privateKey.data) + return try _Crypto.signMessage(data, withPrivateKey: privateKey.data) #endif } public static func verifySignature(_ signature: Data, message: Data, publicKey: Data) throws -> Bool { #if BitcoinKitXcode - return _Crypto.verifySignature(signature, message: message, publicKey: publicKey) + return _Crypto.verifySignature(signature, message: message, publicKey: publicKey) #else - return try _Crypto.verifySignature(signature, message: message, publicKey: publicKey) + return try _Crypto.verifySignature(signature, message: message, publicKey: publicKey) #endif } diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/AddressType.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/AddressType.swift index 500b19e5c..c13c6fe80 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Keys/AddressType.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/AddressType.swift @@ -27,7 +27,7 @@ import Foundation public enum AddressType { case pubkeyHash case scriptHash - + var versionByte: UInt8 { switch self { case .pubkeyHash: @@ -36,7 +36,7 @@ public enum AddressType { return 8 } } - + var versionByte160: UInt8 { return versionByte + 0 } var versionByte192: UInt8 { return versionByte + 1 } var versionByte224: UInt8 { return versionByte + 2 } diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/Address.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/Address.swift index e412f22d7..6ac9231bc 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/Address.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/Address.swift @@ -1,6 +1,6 @@ // // Address.swift -// +// // // Created by Andrey Golubenko on 05.06.2023. // @@ -15,7 +15,7 @@ public protocol AddressProtocol { } #if os(iOS) || os(tvOS) || os(watchOS) -public typealias Address = AddressProtocol & QRCodeConvertible + public typealias Address = AddressProtocol & QRCodeConvertible #else -public typealias Address = AddressProtocol + public typealias Address = AddressProtocol #endif diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/LegacyAddress.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/LegacyAddress.swift index cdc74ab90..24256aeec 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/LegacyAddress.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/LegacyAddress.swift @@ -32,15 +32,15 @@ public final class LegacyAddress: Address, Equatable { public var scriptType: ScriptType { switch type { - case .pubkeyHash: return .p2pkh - case .scriptHash: return .p2sh + case .pubkeyHash: return .p2pkh + case .scriptHash: return .p2sh } } - + public var qrcodeString: String { stringValue } - + public var lockingScript: Data { switch type { case .pubkeyHash: return OpCode.p2pkhStart + OpCode.push(lockingScriptPayload) + OpCode.p2pkhFinish @@ -54,7 +54,7 @@ public final class LegacyAddress: Address, Equatable { self.stringValue = base58 } - public static func ==(lhs: LegacyAddress, rhs: T) -> Bool { + public static func == (lhs: LegacyAddress, rhs: T) -> Bool { guard let rhs = rhs as? LegacyAddress else { return false } diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/SegWitV0Address.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/SegWitV0Address.swift index f8ebd58dc..269e3ebb4 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/SegWitV0Address.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/SegWitV0Address.swift @@ -4,7 +4,7 @@ public final class SegWitV0Address: Address, Equatable { public let type: AddressType public let lockingScriptPayload: Data public let stringValue: String - + public var qrcodeString: String { stringValue } @@ -26,7 +26,7 @@ public final class SegWitV0Address: Address, Equatable { self.stringValue = bech32 } - static public func ==(lhs: SegWitV0Address, rhs: T) -> Bool { + static public func == (lhs: SegWitV0Address, rhs: T) -> Bool { guard let rhs = rhs as? SegWitV0Address else { return false } diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/TaprootAddress.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/TaprootAddress.swift index a89047ff3..e89bccb9f 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/TaprootAddress.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Addresses/TaprootAddress.swift @@ -5,22 +5,22 @@ public final class TaprootAddress: Address, Equatable { public let stringValue: String public let version: UInt8 public var scriptType = ScriptType.p2tr - + public var lockingScript: Data { OpCode.segWitOutputScript(lockingScriptPayload, versionByte: Int(version)) } - + public var qrcodeString: String { stringValue } - + public init(payload: Data, bech32m: String, version: UInt8) { self.lockingScriptPayload = payload self.stringValue = bech32m self.version = version } - - static public func ==(lhs: TaprootAddress, rhs: T) -> Bool { + + static public func == (lhs: TaprootAddress, rhs: T) -> Bool { guard let rhs = rhs as? TaprootAddress else { return false } diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Bech32.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Bech32.swift index 4d560825f..ec46f245c 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Bech32.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Bech32.swift @@ -14,8 +14,8 @@ import Foundation /// Bech32 checksum implementation public final class Bech32 { static let shared = Bech32() - - private let gen: [UInt32] = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] + + private let gen: [UInt32] = [0x3b6a_57b2, 0x2650_8e6d, 0x1ea1_19fa, 0x3d42_33dd, 0x2a14_62b3] /// Bech32 checksum delimiter private let checksumMarker: String = "1" /// Bech32 character set for encoding @@ -25,11 +25,11 @@ public final class Bech32 { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - 15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, - -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, - 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, - -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, - 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1 + 15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, + -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, + 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, + -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, + 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1 ] public init() {} @@ -46,11 +46,11 @@ public final class Bech32 { } return chk } - + /// Expand a HRP for use in checksum computation. private func expandHrp(_ hrp: String) -> Data { guard let hrpBytes = hrp.data(using: .utf8) else { return Data() } - var result = Data(repeating: 0x00, count: hrpBytes.count*2+1) + var result = Data(repeating: 0x00, count: hrpBytes.count * 2 + 1) for (i, c) in hrpBytes.enumerated() { result[i] = c >> 5 result[i + hrpBytes.count + 1] = c & 0x1f @@ -58,14 +58,14 @@ public final class Bech32 { result[hrp.count] = 0 return result } - + private func extractChecksumWithEncoding(hrp: String, checksum: Data) -> (check: UInt32, encoding: Encoding)? { var data = expandHrp(hrp) data.append(checksum) let check = polymod(data) return Encoding.fromCheck(check).flatMap { (check, $0) } } - + /// Create checksum private func createChecksum(hrp: String, values: Data, encoding: Encoding) -> Data { var enc = expandHrp(hrp) @@ -78,7 +78,7 @@ public final class Bech32 { } return ret } - + /// Encode Bech32 string public func encode(_ hrp: String, values: Data, encoding: Encoding) -> String { let checksum = createChecksum(hrp: hrp, values: values, encoding: encoding) @@ -92,7 +92,7 @@ public final class Bech32 { } return String(data: ret, encoding: .utf8) ?? "" } - + /// Decode Bech32 string public func decode(_ str: String) throws -> (hrp: String, checksum: Data, encoding: Encoding) { guard let strBytes = str.data(using: .utf8) else { @@ -142,10 +142,11 @@ public final class Bech32 { } let hrp = String(str[.. Encoding? { switch check { case Encoding.bech32.checksumXorConstant: return .bech32 diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/AddressConverter.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/AddressConverter.swift index 69b260db3..9fc0a875e 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/AddressConverter.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/AddressConverter.swift @@ -1,12 +1,13 @@ // // AddressConverter.swift -// +// // // Created by Andrey Golubenko on 06.06.2023. // import Foundation +// sourcery: AutoMockable public protocol AddressConverter { func convert(address: String) throws -> Address func convert(lockingScriptPayload: Data, type: ScriptType) throws -> Address @@ -18,43 +19,46 @@ extension AddressConverter { var payload: Data? var validScriptType: ScriptType = ScriptType.unknown let sigScriptCount = signatureScript.count - + var outputAddress: Address? - - if let script = Script(data: signatureScript), // PFromSH input {push-sig}{signature}{push-redeem}{script} + + if let script = Script(data: signatureScript), // PFromSH input {push-sig}{signature}{push-redeem}{script} let chunkData = script.chunks.last?.scriptData, let redeemScript = Script(data: chunkData), - let opCode = redeemScript.chunks.last?.opCode.value { + let opCode = redeemScript.chunks.last?.opCode.value + { // parse PFromSH transaction input var verifyChunkCode: UInt8 = opCode if verifyChunkCode == OpCode.OP_ENDIF, redeemScript.chunks.count > 1, - let opCode = redeemScript.chunks.suffix(2).first?.opCode { - - verifyChunkCode = opCode.value // check pre-last chunk + let opCode = redeemScript.chunks.suffix(2).first?.opCode + { + + verifyChunkCode = opCode.value // check pre-last chunk } if OpCode.pFromShCodes.contains(verifyChunkCode) { - payload = chunkData //full script + payload = chunkData //full script validScriptType = .p2sh } } - + if payload == nil, sigScriptCount >= 106, signatureScript[0] >= 71, signatureScript[0] <= 74 { // parse PFromPKH transaction input let signatureOffset = signatureScript[0] let pubKeyLength = signatureScript[Int(signatureOffset + 1)] - + if (pubKeyLength == 33 || pubKeyLength == 65) && sigScriptCount == signatureOffset + pubKeyLength + 2 { - payload = signatureScript.subdata(in: Int(signatureOffset + 2).. 0x50 && signatureScript[1] < 0x61)), - signatureScript[2] == 0x14 { + signatureScript[1] == 0 || (signatureScript[1] > 0x50 && signatureScript[1] < 0x61), + signatureScript[2] == 0x14 + { // parse PFromWPKH-SH transaction input - payload = signatureScript.subdata(in: 1.. Address { var errors = [Error]() diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/AddressConverterFactory.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/AddressConverterFactory.swift index e0deec1b0..3f5c5214e 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/AddressConverterFactory.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/AddressConverterFactory.swift @@ -1,6 +1,6 @@ // // AddressConverterFactory.swift -// +// // // Created by Andrey Golubenko on 06.06.2023. // @@ -8,17 +8,17 @@ public struct AddressConverterFactory { public func make(network: Network) -> AddressConverter { let segWitAddressConverter = SegWitBech32AddressConverter(prefix: "bc") - + let base58AddressConverter = Base58AddressConverter( addressVersion: network.pubkeyhash, addressScriptVersion: network.scripthash ) - + return AddressConverterChain(concreteConverters: [ segWitAddressConverter, base58AddressConverter ]) } - + public init() {} } diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/Base58AddressConverter.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/Base58AddressConverter.swift index c514e64c0..6fe37de4b 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/Base58AddressConverter.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/Base58AddressConverter.swift @@ -19,7 +19,7 @@ final class Base58AddressConverter: AddressConverter { guard let hex = Base58.decode(address) else { throw BitcoinError.unknownAddressType } - + // check decoded length. Must be 1(version) + 20(KeyHash) + 4(CheckSum) if hex.count != Base58AddressConverter.checkSumLength + 20 + 1 { throw BitcoinError.invalidAddressLength @@ -33,9 +33,9 @@ final class Base58AddressConverter: AddressConverter { let type: AddressType switch hex[0] { - case addressVersion: type = AddressType.pubkeyHash - case addressScriptVersion: type = AddressType.scriptHash - default: throw BitcoinError.wrongAddressPrefix + case addressVersion: type = AddressType.pubkeyHash + case addressScriptVersion: type = AddressType.scriptHash + default: throw BitcoinError.wrongAddressPrefix } let keyHash = hex.dropFirst().dropLast(4) @@ -47,13 +47,13 @@ final class Base58AddressConverter: AddressConverter { let addressType: AddressType switch type { - case .p2pkh, .p2pk: - version = addressVersion - addressType = AddressType.pubkeyHash - case .p2sh, .p2wpkhSh: - version = addressScriptVersion - addressType = AddressType.scriptHash - default: throw BitcoinError.unknownAddressType + case .p2pkh, .p2pk: + version = addressVersion + addressType = AddressType.pubkeyHash + case .p2sh, .p2wpkhSh: + version = addressScriptVersion + addressType = AddressType.scriptHash + default: throw BitcoinError.unknownAddressType } var withVersion = (Data([version])) + lockingScriptPayload @@ -63,7 +63,7 @@ final class Base58AddressConverter: AddressConverter { let base58 = Base58.encode(withVersion) return LegacyAddress(type: addressType, payload: lockingScriptPayload, base58: base58) } - + func convert(publicKey: PublicKey, type: ScriptType) throws -> Address { let keyHash = type == .p2wpkhSh ? publicKey.hashP2wpkhWrappedInP2sh : publicKey.hashP2pkh return try convert(lockingScriptPayload: keyHash, type: type) diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/SegWitBech32AddressConverter.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/SegWitBech32AddressConverter.swift index 8342fa329..9f828b212 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/SegWitBech32AddressConverter.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/Converters/SegWitBech32AddressConverter.swift @@ -1,6 +1,6 @@ // // SegWitBech32AddressConverter.swift -// +// // // Created by Andrey Golubenko on 05.06.2023. // @@ -22,9 +22,9 @@ final class SegWitBech32AddressConverter: AddressConverter { case 0: var type: AddressType = .pubkeyHash switch segWitData.program.count { - case 20: type = .pubkeyHash - case 32: type = .scriptHash - default: break + case 20: type = .pubkeyHash + case 32: type = .scriptHash + default: break } return SegWitV0Address(type: type, payload: segWitData.program, bech32: address) case 1: @@ -41,27 +41,27 @@ final class SegWitBech32AddressConverter: AddressConverter { func convert(lockingScriptPayload: Data, type: ScriptType) throws -> Address { switch type { - case .p2wpkh: - let bech32 = try SegWitBech32.encode(hrp: prefix, version: 0, program: lockingScriptPayload, encoding: .bech32) - return SegWitV0Address(type: AddressType.pubkeyHash, payload: lockingScriptPayload, bech32: bech32) - case .p2wsh: - let bech32 = try SegWitBech32.encode(hrp: prefix, version: 0, program: lockingScriptPayload, encoding: .bech32) - return SegWitV0Address(type: AddressType.scriptHash, payload: lockingScriptPayload, bech32: bech32) - case .p2tr: - let bech32 = try SegWitBech32.encode(hrp: prefix, version: 1, program: lockingScriptPayload, encoding: .bech32m) - return TaprootAddress(payload: lockingScriptPayload, bech32m: bech32, version: 1) - default: throw BitcoinError.unknownAddressType + case .p2wpkh: + let bech32 = try SegWitBech32.encode(hrp: prefix, version: 0, program: lockingScriptPayload, encoding: .bech32) + return SegWitV0Address(type: AddressType.pubkeyHash, payload: lockingScriptPayload, bech32: bech32) + case .p2wsh: + let bech32 = try SegWitBech32.encode(hrp: prefix, version: 0, program: lockingScriptPayload, encoding: .bech32) + return SegWitV0Address(type: AddressType.scriptHash, payload: lockingScriptPayload, bech32: bech32) + case .p2tr: + let bech32 = try SegWitBech32.encode(hrp: prefix, version: 1, program: lockingScriptPayload, encoding: .bech32m) + return TaprootAddress(payload: lockingScriptPayload, bech32m: bech32, version: 1) + default: throw BitcoinError.unknownAddressType } } - + func convert(publicKey: PublicKey, type: ScriptType) throws -> Address { switch type { - case .p2wpkh, .p2wsh: - return try convert(lockingScriptPayload: publicKey.hashP2pkh, type: type) -// case .p2tr: -// return try convert(lockingScriptPayload: publicKey.convertedForP2tr, type: type) - default: - throw BitcoinError.unknownAddressType + case .p2wpkh, .p2wsh: + return try convert(lockingScriptPayload: publicKey.hashP2pkh, type: type) + // case .p2tr: + // return try convert(lockingScriptPayload: publicKey.convertedForP2tr, type: type) + default: + throw BitcoinError.unknownAddressType } } } diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/HDPrivateKey.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/HDPrivateKey.swift index 51d990ff7..ba044d8e7 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Keys/HDPrivateKey.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/HDPrivateKey.swift @@ -24,10 +24,11 @@ // import Foundation + #if BitcoinKitXcode -import BitcoinKit.Private + import BitcoinKit.Private #else -import BitcoinKitPrivate + import BitcoinKitPrivate #endif public class HDPrivateKey { @@ -91,14 +92,30 @@ public class HDPrivateKey { public func derived(at index: UInt32, hardened: Bool = false) throws -> HDPrivateKey { // As we use explicit parameter "hardened", do not allow higher bit set. - if (0x80000000 & index) != 0 { + if (0x8000_0000 & index) != 0 { fatalError("invalid child index") } - guard let derivedKey = _HDKey(privateKey: raw, publicKey: extendedPublicKey().raw, chainCode: chainCode, depth: depth, fingerprint: fingerprint, childIndex: childIndex).derived(at: index, hardened: hardened) else { + guard + let derivedKey = _HDKey( + privateKey: raw, + publicKey: extendedPublicKey().raw, + chainCode: chainCode, + depth: depth, + fingerprint: fingerprint, + childIndex: childIndex + ).derived(at: index, hardened: hardened) + else { throw DerivationError.derivationFailed } - return HDPrivateKey(privateKey: derivedKey.privateKey!, chainCode: derivedKey.chainCode, network: network, depth: derivedKey.depth, fingerprint: derivedKey.fingerprint, childIndex: derivedKey.childIndex) + return HDPrivateKey( + privateKey: derivedKey.privateKey!, + chainCode: derivedKey.chainCode, + network: network, + depth: derivedKey.depth, + fingerprint: derivedKey.fingerprint, + childIndex: derivedKey.childIndex + ) } } @@ -109,7 +126,7 @@ extension HDPrivateKey: CustomStringConvertible { } #if os(iOS) || os(tvOS) || os(watchOS) -extension HDPrivateKey: QRCodeConvertible {} + extension HDPrivateKey: QRCodeConvertible {} #endif public enum DerivationError: Error { diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/HDPublicKey.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/HDPublicKey.swift index f0ef0ea3b..167ca8e00 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Keys/HDPublicKey.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/HDPublicKey.swift @@ -24,10 +24,11 @@ // import Foundation + #if BitcoinKitXcode -import BitcoinKit.Private + import BitcoinKit.Private #else -import BitcoinKitPrivate + import BitcoinKitPrivate #endif public class HDPublicKey { @@ -66,13 +67,23 @@ public class HDPublicKey { public func derived(at index: UInt32) throws -> HDPublicKey { // As we use explicit parameter "hardened", do not allow higher bit set. - if (0x80000000 & index) != 0 { + if (0x8000_0000 & index) != 0 { fatalError("invalid child index") } - guard let derivedKey = _HDKey(privateKey: nil, publicKey: raw, chainCode: chainCode, depth: depth, fingerprint: fingerprint, childIndex: childIndex).derived(at: index, hardened: false) else { + guard + let derivedKey = _HDKey(privateKey: nil, publicKey: raw, chainCode: chainCode, depth: depth, fingerprint: fingerprint, childIndex: childIndex) + .derived(at: index, hardened: false) + else { throw DerivationError.derivationFailed } - return HDPublicKey(raw: derivedKey.publicKey!, chainCode: derivedKey.chainCode, network: network, depth: derivedKey.depth, fingerprint: derivedKey.fingerprint, childIndex: derivedKey.childIndex) + return HDPublicKey( + raw: derivedKey.publicKey!, + chainCode: derivedKey.chainCode, + network: network, + depth: derivedKey.depth, + fingerprint: derivedKey.fingerprint, + childIndex: derivedKey.childIndex + ) } } @@ -82,5 +93,5 @@ extension HDPublicKey: CustomStringConvertible { } } #if os(iOS) || os(tvOS) || os(watchOS) -extension HDPublicKey: QRCodeConvertible {} + extension HDPublicKey: QRCodeConvertible {} #endif diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/PrivateKey.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/PrivateKey.swift index d59e1efae..dbf11310c 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Keys/PrivateKey.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/PrivateKey.swift @@ -24,10 +24,11 @@ // import Foundation + #if BitcoinKitXcode -import BitcoinKit.Private + import BitcoinKit.Private #else -import BitcoinKitPrivate + import BitcoinKitPrivate #endif public struct PrivateKey { @@ -74,7 +75,7 @@ public struct PrivateKey { var status: Int32 = 0 repeat { status = key.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, count, $0) } - } while (status != 0 || !check([UInt8](key))) + } while status != 0 || !check([UInt8](key)) self.data = key } @@ -156,7 +157,7 @@ extension PrivateKey: CustomStringConvertible { } #if os(iOS) || os(tvOS) || os(watchOS) -extension PrivateKey: QRCodeConvertible {} + extension PrivateKey: QRCodeConvertible {} #endif public enum PrivateKeyError: Error { diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/PublicKey.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/PublicKey.swift index 8d7deebb6..063b152ba 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Keys/PublicKey.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/PublicKey.swift @@ -24,10 +24,11 @@ // import Foundation + #if BitcoinKitXcode -import BitcoinKit.Private + import BitcoinKit.Private #else -import BitcoinKitPrivate + import BitcoinKitPrivate #endif public struct PublicKey { @@ -45,11 +46,13 @@ public struct PublicKey { let header = data[0] self.isCompressed = (header == 0x02 || header == 0x03) hashP2pkh = Crypto.sha256ripemd160(data) - - hashP2wpkhWrappedInP2sh = Crypto.sha256ripemd160(OpCode.segWitOutputScript( - hashP2pkh, - versionByte: .zero - )) + + hashP2wpkhWrappedInP2sh = Crypto.sha256ripemd160( + OpCode.segWitOutputScript( + hashP2pkh, + versionByte: .zero + ) + ) } } @@ -66,5 +69,5 @@ extension PublicKey: CustomStringConvertible { } #if os(iOS) || os(tvOS) || os(watchOS) -extension PublicKey: QRCodeConvertible {} + extension PublicKey: QRCodeConvertible {} #endif diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/QRCodeConvertible.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/QRCodeConvertible.swift index a0bc811e0..ffd9e99a4 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Keys/QRCodeConvertible.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/QRCodeConvertible.swift @@ -23,22 +23,22 @@ // #if os(iOS) || os(tvOS) || os(watchOS) -import UIKit + import UIKit -public protocol QRCodeConvertible { - var qrcodeString: String { get } - func qrImage(size: CGSize) -> UIImage? -} + public protocol QRCodeConvertible { + var qrcodeString: String { get } + func qrImage(size: CGSize) -> UIImage? + } -extension QRCodeConvertible { - public func qrImage(size: CGSize = CGSize(width: 200, height: 200)) -> UIImage? { - return QRCodeGenerator.generate(from: qrcodeString, size: size) + extension QRCodeConvertible { + public func qrImage(size: CGSize = CGSize(width: 200, height: 200)) -> UIImage? { + return QRCodeGenerator.generate(from: qrcodeString, size: size) + } } -} -extension CustomStringConvertible where Self: QRCodeConvertible { - public var qrcodeString: String { - return description + extension CustomStringConvertible where Self: QRCodeConvertible { + public var qrcodeString: String { + return description + } } -} #endif diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/QRCodeGenerator.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/QRCodeGenerator.swift index 4eb781617..4e80071c7 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Keys/QRCodeGenerator.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/QRCodeGenerator.swift @@ -23,37 +23,37 @@ // #if os(iOS) || os(tvOS) || os(watchOS) -import UIKit + import UIKit -public struct QRCodeGenerator { - private static func generateCGImage(from string: String) -> CGImage? { - let parameters: [String: Any] = [ - "inputMessage": string.data(using: .utf8)!, - "inputCorrectionLevel": "L" - ] - guard let image = CIFilter(name: "CIQRCodeGenerator", parameters: parameters)?.outputImage else { - return nil - } + public struct QRCodeGenerator { + private static func generateCGImage(from string: String) -> CGImage? { + let parameters: [String: Any] = [ + "inputMessage": string.data(using: .utf8)!, + "inputCorrectionLevel": "L" + ] + guard let image = CIFilter(name: "CIQRCodeGenerator", parameters: parameters)?.outputImage else { + return nil + } - return CIContext(options: nil).createCGImage(image, from: image.extent) - } + return CIContext(options: nil).createCGImage(image, from: image.extent) + } - private static func generateNonInterpolatedUIImage(from cgImage: CGImage, size: CGSize) -> UIImage? { - UIGraphicsBeginImageContextWithOptions(size, true, 0) - guard let context = UIGraphicsGetCurrentContext() else { return nil } - context.interpolationQuality = .none - context.translateBy(x: 0, y: size.height) - context.scaleBy(x: 1.0, y: -1.0) - context.draw(cgImage, in: context.boundingBoxOfClipPath) - let uiImage = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - return uiImage - } + private static func generateNonInterpolatedUIImage(from cgImage: CGImage, size: CGSize) -> UIImage? { + UIGraphicsBeginImageContextWithOptions(size, true, 0) + guard let context = UIGraphicsGetCurrentContext() else { return nil } + context.interpolationQuality = .none + context.translateBy(x: 0, y: size.height) + context.scaleBy(x: 1.0, y: -1.0) + context.draw(cgImage, in: context.boundingBoxOfClipPath) + let uiImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return uiImage + } - public static func generate(from string: String, size: CGSize = CGSize(width: 200, height: 200)) -> UIImage? { - guard let cgImage: CGImage = generateCGImage(from: string) else { return nil } - guard let uiImage: UIImage = generateNonInterpolatedUIImage(from: cgImage, size: size) else { return nil } - return uiImage + public static func generate(from string: String, size: CGSize = CGSize(width: 200, height: 200)) -> UIImage? { + guard let cgImage: CGImage = generateCGImage(from: string) else { return nil } + guard let uiImage: UIImage = generateNonInterpolatedUIImage(from: cgImage, size: size) else { return nil } + return uiImage + } } -} #endif diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Keys/SegwitAddrCoder.swift b/BitcoinKit/Sources/BitcoinKit/Core/Keys/SegwitAddrCoder.swift index 9bc281511..280081506 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Keys/SegwitAddrCoder.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Keys/SegwitAddrCoder.swift @@ -1,6 +1,6 @@ // // SegwitAddrCoder.swift -// +// // // Created by Anton Boyarkin on 28.05.2022. // @@ -11,8 +11,8 @@ import Foundation public class SegwitAddrCoder { private let bech32 = Bech32New() - public init() { } - + public init() {} + /// Convert from one power-of-2 number base to another private func convertBits(from: Int, to: Int, pad: Bool, idata: Data) throws -> Data { var acc: Int = 0 @@ -32,12 +32,12 @@ public class SegwitAddrCoder { if bits != 0 { odata.append(UInt8((acc << (to - bits)) & maxv)) } - } else if (bits >= from || ((acc << (to - bits)) & maxv) != 0) { + } else if bits >= from || ((acc << (to - bits)) & maxv) != 0 { throw CoderError.bitsConversionFailed } return odata } - + /// Decode segwit address public func decode(hrp: String, addr: String) throws -> (version: Int, program: Data) { let dec = try bech32.decode(addr) @@ -59,13 +59,13 @@ public class SegwitAddrCoder { } return (Int(dec.checksum[0]), conv) } - + /// Encode segwit address public func encode(hrp: String, version: Int, program: Data) throws -> String { var enc = Data([UInt8(version)]) enc.append(try convertBits(from: 8, to: 5, pad: true, idata: program)) let result = bech32.encode(hrp, values: enc) - guard let _ = try? decode(hrp: hrp, addr: result) else { + guard (try? decode(hrp: hrp, addr: result)) != nil else { throw CoderError.encodingCheckFailed } return result @@ -77,13 +77,13 @@ extension SegwitAddrCoder { case bitsConversionFailed case hrpMismatch(String, String) case checksumSizeTooLow - + case dataSizeMismatch(Int) case segwitVersionNotSupported(UInt8) case segwitV0ProgramSizeMismatch(Int) - + case encodingCheckFailed - + public var errorDescription: String? { switch self { case .bitsConversionFailed: @@ -107,7 +107,7 @@ extension SegwitAddrCoder { /// Bech32 checksum implementation public class Bech32New { - private let gen: [UInt32] = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] + private let gen: [UInt32] = [0x3b6a_57b2, 0x2650_8e6d, 0x1ea1_19fa, 0x3d42_33dd, 0x2a14_62b3] /// Bech32 checksum delimiter private let checksumMarker: String = "1" /// Bech32 character set for encoding @@ -117,15 +117,15 @@ public class Bech32New { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, - 15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, - -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, - 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, - -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, - 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1 + 15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, + -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, + 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, + -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, + 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1 ] - public init() { } - + public init() {} + /// Find the polynomial with value coefficients mod the generator as 30-bit. private func polymod(_ values: Data) -> UInt32 { var chk: UInt32 = 1 @@ -138,11 +138,11 @@ public class Bech32New { } return chk } - + /// Expand a HRP for use in checksum computation. private func expandHrp(_ hrp: String) -> Data { guard let hrpBytes = hrp.data(using: .utf8) else { return Data() } - var result = Data(repeating: 0x00, count: hrpBytes.count*2+1) + var result = Data(repeating: 0x00, count: hrpBytes.count * 2 + 1) for (i, c) in hrpBytes.enumerated() { result[i] = c >> 5 result[i + hrpBytes.count + 1] = c & 0x1f @@ -150,14 +150,14 @@ public class Bech32New { result[hrp.count] = 0 return result } - + /// Verify checksum private func verifyChecksum(hrp: String, checksum: Data) -> Bool { var data = expandHrp(hrp) data.append(checksum) return polymod(data) == 1 } - + /// Create checksum private func createChecksum(hrp: String, values: Data) -> Data { var enc = expandHrp(hrp) @@ -170,7 +170,7 @@ public class Bech32New { } return ret } - + /// Encode Bech32 string public func encode(_ hrp: String, values: Data) -> String { let checksum = createChecksum(hrp: hrp, values: values) @@ -184,7 +184,7 @@ public class Bech32New { } return String(data: ret, encoding: .utf8) ?? "" } - + /// Decode Bech32 string public func decode(_ str: String) throws -> (hrp: String, checksum: Data) { guard let strBytes = str.data(using: .utf8) else { @@ -236,7 +236,7 @@ public class Bech32New { guard verifyChecksum(hrp: hrp, checksum: values) else { throw DecodingError.checksumMismatch } - return (hrp, Data(values[..<(vSize-6)])) + return (hrp, Data(values[..<(vSize - 6)])) } } @@ -249,10 +249,10 @@ extension Bech32New { case incorrectHrpSize case incorrectChecksumSize case stringLengthExceeded - + case invalidCharacter case checksumMismatch - + public var errorDescription: String? { switch self { case .checksumMismatch: diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Mnemonic.swift b/BitcoinKit/Sources/BitcoinKit/Core/Mnemonic.swift index 146473411..99e251da6 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Mnemonic.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Mnemonic.swift @@ -24,10 +24,11 @@ // import Foundation + #if BitcoinKitXcode -import BitcoinKit.Private + import BitcoinKit.Private #else -import BitcoinKitPrivate + import BitcoinKitPrivate #endif public struct Mnemonic { diff --git a/BitcoinKit/Sources/BitcoinKit/Core/MurmurHash.swift b/BitcoinKit/Sources/BitcoinKit/Core/MurmurHash.swift index e60e91067..870087a40 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/MurmurHash.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/MurmurHash.swift @@ -31,8 +31,8 @@ public struct MurmurHash { } public static func hashValue(_ bytes: Data, _ seed: UInt32) -> UInt32 { - let c1: UInt32 = 0xcc9e2d51 - let c2: UInt32 = 0x1b873593 + let c1: UInt32 = 0xcc9e_2d51 + let c2: UInt32 = 0x1b87_3593 let byteCount = bytes.count @@ -49,12 +49,12 @@ public struct MurmurHash { h1 = h1 ^ k1 h1 = rotateLeft(h1, 13) - h1 = h1 &* 5 &+ 0xe6546b64 + h1 = h1 &* 5 &+ 0xe654_6b64 } let remaining = byteCount & 3 if remaining != 0 { var k1 = UInt32(0) - for r in 0 ..< remaining { + for r in 0..> 16) - h1 = h1 &* 0x85ebca6b + h1 = h1 &* 0x85eb_ca6b h1 ^= (h1 >> 13) - h1 = h1 &* 0xc2b2ae35 + h1 = h1 &* 0xc2b2_ae35 h1 ^= (h1 >> 16) return h1 diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Network.swift b/BitcoinKit/Sources/BitcoinKit/Core/Network.swift index e12ac73e4..d26b3b57d 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Network.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Network.swift @@ -53,7 +53,7 @@ open class Network { extension Network: Equatable { // swiftlint:disable operator_whitespace - public static func ==(lhs: Network, rhs: Network) -> Bool { + public static func == (lhs: Network, rhs: Network) -> Bool { return lhs.name == rhs.name } } @@ -63,7 +63,7 @@ public struct Checkpoint { public let hash: Data public let timestamp: UInt32? public let target: UInt32? - + public init(height: Int32, hash: Data, timestamp: UInt32? = nil, target: UInt32? = nil) { self.height = height self.hash = hash @@ -77,24 +77,29 @@ public class BTCMainnet: Mainnet { return "bitcoin" } override public var magic: UInt32 { - return 0xf9beb4d9 + return 0xf9be_b4d9 } public override var dnsSeeds: [String] { return [ - "seed.bitcoin.sipa.be", // Pieter Wuille - "dnsseed.bluematt.me", // Matt Corallo - "dnsseed.bitcoin.dashjr.org", // Luke Dashjr - "seed.bitcoinstats.com", // Chris Decker - "seed.bitnodes.io", // Addy Yeow - "bitseed.xf2.org", // Jeff Garzik - "seed.bitcoin.jonasschnelli.ch", // Jonas Schnelli - "bitcoin.bloqseeds.net", // Bloq - "seed.ob1.io" // OpenBazaar + "seed.bitcoin.sipa.be", // Pieter Wuille + "dnsseed.bluematt.me", // Matt Corallo + "dnsseed.bitcoin.dashjr.org", // Luke Dashjr + "seed.bitcoinstats.com", // Chris Decker + "seed.bitnodes.io", // Addy Yeow + "bitseed.xf2.org", // Jeff Garzik + "seed.bitcoin.jonasschnelli.ch", // Jonas Schnelli + "bitcoin.bloqseeds.net", // Bloq + "seed.ob1.io" // OpenBazaar ] } override public var checkpoints: [Checkpoint] { return super.checkpoints + [ - Checkpoint(height: 463_680, hash: Data(Data(hex: "000000000000000000431a2f4619afe62357cd16589b638bb638f2992058d88e")!.reversed()), timestamp: 1_493_259_601, target: 0x18021b3e) + Checkpoint( + height: 463_680, + hash: Data(Data(hex: "000000000000000000431a2f4619afe62357cd16589b638bb638f2992058d88e")!.reversed()), + timestamp: 1_493_259_601, + target: 0x1802_1b3e + ) ] } } @@ -104,20 +109,25 @@ public class BTCTestnet: Testnet { return "bitcoin" } override public var magic: UInt32 { - return 0x0b110907 + return 0x0b11_0907 } public override var dnsSeeds: [String] { return [ - "testnet-seed.bitcoin.jonasschnelli.ch", // Jonas Schnelli - "testnet-seed.bluematt.me", // Matt Corallo - "testnet-seed.bitcoin.petertodd.org", // Peter Todd - "testnet-seed.bitcoin.schildbach.de", // Andreas Schildbach - "bitcoin-testnet.bloqseeds.net" // Bloq + "testnet-seed.bitcoin.jonasschnelli.ch", // Jonas Schnelli + "testnet-seed.bluematt.me", // Matt Corallo + "testnet-seed.bitcoin.petertodd.org", // Peter Todd + "testnet-seed.bitcoin.schildbach.de", // Andreas Schildbach + "bitcoin-testnet.bloqseeds.net" // Bloq ] } override public var checkpoints: [Checkpoint] { return super.checkpoints + [ - Checkpoint(height: 1_108_800, hash: Data(Data(hex: "00000000000288d9a219419d0607fb67cc324d4b6d2945ca81eaa5e739fab81e")!.reversed()), timestamp: 1_296_688_602, target: 0x1b09ecf0) + Checkpoint( + height: 1_108_800, + hash: Data(Data(hex: "00000000000288d9a219419d0607fb67cc324d4b6d2945ca81eaa5e739fab81e")!.reversed()), + timestamp: 1_296_688_602, + target: 0x1b09_ecf0 + ) ] } } @@ -127,21 +137,26 @@ public class BCHMainnet: Mainnet { return "bitcoincash" } override public var magic: UInt32 { - return 0xe3e1f3e8 + return 0xe3e1_f3e8 } public override var dnsSeeds: [String] { return [ - "seed.bitcoinabc.org", // - Bitcoin ABC seeder - "seed-abc.bitcoinforks.org", // - bitcoinforks seeders - "btccash-seeder.bitcoinunlimited.info", // - BU seeder - "seed.bitprim.org", // - Bitprim - "seed.deadalnix.me", // - Amaury SÉCHET - "seeder.criptolayer.net" // - criptolayer.net + "seed.bitcoinabc.org", // - Bitcoin ABC seeder + "seed-abc.bitcoinforks.org", // - bitcoinforks seeders + "btccash-seeder.bitcoinunlimited.info", // - BU seeder + "seed.bitprim.org", // - Bitprim + "seed.deadalnix.me", // - Amaury SÉCHET + "seeder.criptolayer.net" // - criptolayer.net ] } override public var checkpoints: [Checkpoint] { return super.checkpoints + [ - Checkpoint(height: 478_559, hash: Data(Data(hex: "000000000000000000651ef99cb9fcbe0dadde1d424bd9f15ff20136191a5eec")!.reversed()), timestamp: 1_501_611_161, target: 0x18021b3e) + Checkpoint( + height: 478_559, + hash: Data(Data(hex: "000000000000000000651ef99cb9fcbe0dadde1d424bd9f15ff20136191a5eec")!.reversed()), + timestamp: 1_501_611_161, + target: 0x1802_1b3e + ) ] } } @@ -151,7 +166,7 @@ public class BCHTestnet: Testnet { return "bchtest" } override public var magic: UInt32 { - return 0xf4e5f3f4 + return 0xf4e5_f3f4 } public override var dnsSeeds: [String] { return [ @@ -164,8 +179,18 @@ public class BCHTestnet: Testnet { } override public var checkpoints: [Checkpoint] { return super.checkpoints + [ - Checkpoint(height: 1_200_000, hash: Data(Data(hex: "00000000d91bdbb5394bcf457c0f0b7a7e43eb978e2d881b6c2a4c2756abc558")!.reversed()), timestamp: 1_296_688_602, target: 0x1b09ecf0), - Checkpoint(height: 1_240_000, hash: Data(Data(hex: "0000000002a2bbefefa5aa5f0b7e95957537693808e753f4b4a8e26c5257891d")!.reversed()), timestamp: 1_296_688_602, target: 0x1b09ecf0) + Checkpoint( + height: 1_200_000, + hash: Data(Data(hex: "00000000d91bdbb5394bcf457c0f0b7a7e43eb978e2d881b6c2a4c2756abc558")!.reversed()), + timestamp: 1_296_688_602, + target: 0x1b09_ecf0 + ), + Checkpoint( + height: 1_240_000, + hash: Data(Data(hex: "0000000002a2bbefefa5aa5f0b7e95957537693808e753f4b4a8e26c5257891d")!.reversed()), + timestamp: 1_296_688_602, + target: 0x1b09_ecf0 + ) ] } } @@ -187,10 +212,10 @@ public class Mainnet: Network { return 0x05 } override public var xpubkey: UInt32 { - return 0x0488b21e + return 0x0488_b21e } override public var xprivkey: UInt32 { - return 0x0488ade4 + return 0x0488_ade4 } public override var port: UInt32 { return 8333 @@ -199,30 +224,150 @@ public class Mainnet: Network { /// difficulty transition boundaries in order to verify the block difficulty at the immediately following transition override public var checkpoints: [Checkpoint] { return [ - Checkpoint(height: 0, hash: Data(Data(hex: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f")!.reversed()), timestamp: 1_231_006_505, target: 0x1d00ffff), - Checkpoint(height: 20_160, hash: Data(Data(hex: "000000000f1aef56190aee63d33a373e6487132d522ff4cd98ccfc96566d461e")!.reversed()), timestamp: 1_248_481_816, target: 0x1d00ffff), - Checkpoint(height: 40_320, hash: Data(Data(hex: "0000000045861e169b5a961b7034f8de9e98022e7a39100dde3ae3ea240d7245")!.reversed()), timestamp: 1_266_191_579, target: 0x1c654657), - Checkpoint(height: 60_480, hash: Data(Data(hex: "000000000632e22ce73ed38f46d5b408ff1cff2cc9e10daaf437dfd655153837")!.reversed()), timestamp: 1_276_298_786, target: 0x1c0eba64), - Checkpoint(height: 80_640, hash: Data(Data(hex: "0000000000307c80b87edf9f6a0697e2f01db67e518c8a4d6065d1d859a3a659")!.reversed()), timestamp: 1_284_861_847, target: 0x1b4766ed), - Checkpoint(height: 100_800, hash: Data(Data(hex: "000000000000e383d43cc471c64a9a4a46794026989ef4ff9611d5acb704e47a")!.reversed()), timestamp: 1_294_031_411, target: 0x1b0404cb), - Checkpoint(height: 120_960, hash: Data(Data(hex: "0000000000002c920cf7e4406b969ae9c807b5c4f271f490ca3de1b0770836fc")!.reversed()), timestamp: 1_304_131_980, target: 0x1b0098fa), - Checkpoint(height: 141_120, hash: Data(Data(hex: "00000000000002d214e1af085eda0a780a8446698ab5c0128b6392e189886114")!.reversed()), timestamp: 1_313_451_894, target: 0x1a094a86), - Checkpoint(height: 161_280, hash: Data(Data(hex: "00000000000005911fe26209de7ff510a8306475b75ceffd434b68dc31943b99")!.reversed()), timestamp: 1_326_047_176, target: 0x1a0d69d7), - Checkpoint(height: 181_440, hash: Data(Data(hex: "00000000000000e527fc19df0992d58c12b98ef5a17544696bbba67812ef0e64")!.reversed()), timestamp: 1_337_883_029, target: 0x1a0a8b5f), - Checkpoint(height: 201_600, hash: Data(Data(hex: "00000000000003a5e28bef30ad31f1f9be706e91ae9dda54179a95c9f9cd9ad0")!.reversed()), timestamp: 1_349_226_660, target: 0x1a057e08), - Checkpoint(height: 221_760, hash: Data(Data(hex: "00000000000000fc85dd77ea5ed6020f9e333589392560b40908d3264bd1f401")!.reversed()), timestamp: 1_361_148_470, target: 0x1a04985c), - Checkpoint(height: 241_920, hash: Data(Data(hex: "00000000000000b79f259ad14635739aaf0cc48875874b6aeecc7308267b50fa")!.reversed()), timestamp: 1_371_418_654, target: 0x1a00de15), - Checkpoint(height: 262_080, hash: Data(Data(hex: "000000000000000aa77be1c33deac6b8d3b7b0757d02ce72fffddc768235d0e2")!.reversed()), timestamp: 1_381_070_552, target: 0x1916b0ca), - Checkpoint(height: 282_240, hash: Data(Data(hex: "0000000000000000ef9ee7529607286669763763e0c46acfdefd8a2306de5ca8")!.reversed()), timestamp: 1_390_570_126, target: 0x1901f52c), - Checkpoint(height: 302_400, hash: Data(Data(hex: "0000000000000000472132c4daaf358acaf461ff1c3e96577a74e5ebf91bb170")!.reversed()), timestamp: 1_400_928_750, target: 0x18692842), - Checkpoint(height: 322_560, hash: Data(Data(hex: "000000000000000002df2dd9d4fe0578392e519610e341dd09025469f101cfa1")!.reversed()), timestamp: 1_411_680_080, target: 0x181fb893), - Checkpoint(height: 342_720, hash: Data(Data(hex: "00000000000000000f9cfece8494800d3dcbf9583232825da640c8703bcd27e7")!.reversed()), timestamp: 1_423_496_415, target: 0x1818bb87), - Checkpoint(height: 362_880, hash: Data(Data(hex: "000000000000000014898b8e6538392702ffb9450f904c80ebf9d82b519a77d5")!.reversed()), timestamp: 1_435_475_246, target: 0x1816418e), - Checkpoint(height: 383_040, hash: Data(Data(hex: "00000000000000000a974fa1a3f84055ad5ef0b2f96328bc96310ce83da801c9")!.reversed()), timestamp: 1_447_236_692, target: 0x1810b289), - Checkpoint(height: 403_200, hash: Data(Data(hex: "000000000000000000c4272a5c68b4f55e5af734e88ceab09abf73e9ac3b6d01")!.reversed()), timestamp: 1_458_292_068, target: 0x1806a4c3), - Checkpoint(height: 423_360, hash: Data(Data(hex: "000000000000000001630546cde8482cc183708f076a5e4d6f51cd24518e8f85")!.reversed()), timestamp: 1_470_163_842, target: 0x18057228), - Checkpoint(height: 443_520, hash: Data(Data(hex: "00000000000000000345d0c7890b2c81ab5139c6e83400e5bed00d23a1f8d239")!.reversed()), timestamp: 1_481_765_313, target: 0x18038b85), - Checkpoint(height: 463_680, hash: Data(Data(hex: "000000000000000000431a2f4619afe62357cd16589b638bb638f2992058d88e")!.reversed()), timestamp: 1_493_259_601, target: 0x18021b3e) + Checkpoint( + height: 0, + hash: Data(Data(hex: "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f")!.reversed()), + timestamp: 1_231_006_505, + target: 0x1d00_ffff + ), + Checkpoint( + height: 20_160, + hash: Data(Data(hex: "000000000f1aef56190aee63d33a373e6487132d522ff4cd98ccfc96566d461e")!.reversed()), + timestamp: 1_248_481_816, + target: 0x1d00_ffff + ), + Checkpoint( + height: 40_320, + hash: Data(Data(hex: "0000000045861e169b5a961b7034f8de9e98022e7a39100dde3ae3ea240d7245")!.reversed()), + timestamp: 1_266_191_579, + target: 0x1c65_4657 + ), + Checkpoint( + height: 60_480, + hash: Data(Data(hex: "000000000632e22ce73ed38f46d5b408ff1cff2cc9e10daaf437dfd655153837")!.reversed()), + timestamp: 1_276_298_786, + target: 0x1c0e_ba64 + ), + Checkpoint( + height: 80_640, + hash: Data(Data(hex: "0000000000307c80b87edf9f6a0697e2f01db67e518c8a4d6065d1d859a3a659")!.reversed()), + timestamp: 1_284_861_847, + target: 0x1b47_66ed + ), + Checkpoint( + height: 100_800, + hash: Data(Data(hex: "000000000000e383d43cc471c64a9a4a46794026989ef4ff9611d5acb704e47a")!.reversed()), + timestamp: 1_294_031_411, + target: 0x1b04_04cb + ), + Checkpoint( + height: 120_960, + hash: Data(Data(hex: "0000000000002c920cf7e4406b969ae9c807b5c4f271f490ca3de1b0770836fc")!.reversed()), + timestamp: 1_304_131_980, + target: 0x1b00_98fa + ), + Checkpoint( + height: 141_120, + hash: Data(Data(hex: "00000000000002d214e1af085eda0a780a8446698ab5c0128b6392e189886114")!.reversed()), + timestamp: 1_313_451_894, + target: 0x1a09_4a86 + ), + Checkpoint( + height: 161_280, + hash: Data(Data(hex: "00000000000005911fe26209de7ff510a8306475b75ceffd434b68dc31943b99")!.reversed()), + timestamp: 1_326_047_176, + target: 0x1a0d_69d7 + ), + Checkpoint( + height: 181_440, + hash: Data(Data(hex: "00000000000000e527fc19df0992d58c12b98ef5a17544696bbba67812ef0e64")!.reversed()), + timestamp: 1_337_883_029, + target: 0x1a0a_8b5f + ), + Checkpoint( + height: 201_600, + hash: Data(Data(hex: "00000000000003a5e28bef30ad31f1f9be706e91ae9dda54179a95c9f9cd9ad0")!.reversed()), + timestamp: 1_349_226_660, + target: 0x1a05_7e08 + ), + Checkpoint( + height: 221_760, + hash: Data(Data(hex: "00000000000000fc85dd77ea5ed6020f9e333589392560b40908d3264bd1f401")!.reversed()), + timestamp: 1_361_148_470, + target: 0x1a04_985c + ), + Checkpoint( + height: 241_920, + hash: Data(Data(hex: "00000000000000b79f259ad14635739aaf0cc48875874b6aeecc7308267b50fa")!.reversed()), + timestamp: 1_371_418_654, + target: 0x1a00_de15 + ), + Checkpoint( + height: 262_080, + hash: Data(Data(hex: "000000000000000aa77be1c33deac6b8d3b7b0757d02ce72fffddc768235d0e2")!.reversed()), + timestamp: 1_381_070_552, + target: 0x1916_b0ca + ), + Checkpoint( + height: 282_240, + hash: Data(Data(hex: "0000000000000000ef9ee7529607286669763763e0c46acfdefd8a2306de5ca8")!.reversed()), + timestamp: 1_390_570_126, + target: 0x1901_f52c + ), + Checkpoint( + height: 302_400, + hash: Data(Data(hex: "0000000000000000472132c4daaf358acaf461ff1c3e96577a74e5ebf91bb170")!.reversed()), + timestamp: 1_400_928_750, + target: 0x1869_2842 + ), + Checkpoint( + height: 322_560, + hash: Data(Data(hex: "000000000000000002df2dd9d4fe0578392e519610e341dd09025469f101cfa1")!.reversed()), + timestamp: 1_411_680_080, + target: 0x181f_b893 + ), + Checkpoint( + height: 342_720, + hash: Data(Data(hex: "00000000000000000f9cfece8494800d3dcbf9583232825da640c8703bcd27e7")!.reversed()), + timestamp: 1_423_496_415, + target: 0x1818_bb87 + ), + Checkpoint( + height: 362_880, + hash: Data(Data(hex: "000000000000000014898b8e6538392702ffb9450f904c80ebf9d82b519a77d5")!.reversed()), + timestamp: 1_435_475_246, + target: 0x1816_418e + ), + Checkpoint( + height: 383_040, + hash: Data(Data(hex: "00000000000000000a974fa1a3f84055ad5ef0b2f96328bc96310ce83da801c9")!.reversed()), + timestamp: 1_447_236_692, + target: 0x1810_b289 + ), + Checkpoint( + height: 403_200, + hash: Data(Data(hex: "000000000000000000c4272a5c68b4f55e5af734e88ceab09abf73e9ac3b6d01")!.reversed()), + timestamp: 1_458_292_068, + target: 0x1806_a4c3 + ), + Checkpoint( + height: 423_360, + hash: Data(Data(hex: "000000000000000001630546cde8482cc183708f076a5e4d6f51cd24518e8f85")!.reversed()), + timestamp: 1_470_163_842, + target: 0x1805_7228 + ), + Checkpoint( + height: 443_520, + hash: Data(Data(hex: "00000000000000000345d0c7890b2c81ab5139c6e83400e5bed00d23a1f8d239")!.reversed()), + timestamp: 1_481_765_313, + target: 0x1803_8b85 + ), + Checkpoint( + height: 463_680, + hash: Data(Data(hex: "000000000000000000431a2f4619afe62357cd16589b638bb638f2992058d88e")!.reversed()), + timestamp: 1_493_259_601, + target: 0x1802_1b3e + ) ] } // These hashes are genesis blocks' ones @@ -248,27 +393,82 @@ public class Testnet: Network { return 0xc4 } override public var xpubkey: UInt32 { - return 0x043587cf + return 0x0435_87cf } override public var xprivkey: UInt32 { - return 0x04358394 + return 0x0435_8394 } public override var port: UInt32 { return 18_333 } override public var checkpoints: [Checkpoint] { return [ - Checkpoint(height: 0, hash: Data(Data(hex: "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943")!.reversed()), timestamp: 1_376_543_922, target: 0x1d00ffff), - Checkpoint(height: 100_800, hash: Data(Data(hex: "0000000000a33112f86f3f7b0aa590cb4949b84c2d9c673e9e303257b3be9000")!.reversed()), timestamp: 1_393_813_869, target: 0x1c00d907), - Checkpoint(height: 201_600, hash: Data(Data(hex: "0000000000376bb71314321c45de3015fe958543afcbada242a3b1b072498e38")!.reversed()), timestamp: 1_413_766_239, target: 0x1b602ac0), - Checkpoint(height: 302_400, hash: Data(Data(hex: "0000000000001c93ebe0a7c33426e8edb9755505537ef9303a023f80be29d32d")!.reversed()), timestamp: 1_431_821_666, target: 0x1a33605e), - Checkpoint(height: 403_200, hash: Data(Data(hex: "0000000000ef8b05da54711e2106907737741ac0278d59f358303c71d500f3c4")!.reversed()), timestamp: 1_436_951_946, target: 0x1c02346c), - Checkpoint(height: 504_000, hash: Data(Data(hex: "0000000000005d105473c916cd9d16334f017368afea6bcee71629e0fcf2f4f5")!.reversed()), timestamp: 1_447_484_641, target: 0x1b00ab86), - Checkpoint(height: 604_800, hash: Data(Data(hex: "00000000000008653c7e5c00c703c5a9d53b318837bb1b3586a3d060ce6fff2e")!.reversed()), timestamp: 1_455_728_685, target: 0x1a092a20), - Checkpoint(height: 705_600, hash: Data(Data(hex: "00000000004ee3bc2e2dd06c31f2d7a9c3e471ec0251924f59f222e5e9c37e12")!.reversed()), timestamp: 1_462_006_183, target: 0x1c0ffff0), - Checkpoint(height: 806_400, hash: Data(Data(hex: "0000000000000faf114ff29df6dbac969c6b4a3b407cd790d3a12742b50c2398")!.reversed()), timestamp: 1_469_705_562, target: 0x1a34e280), - Checkpoint(height: 907_200, hash: Data(Data(hex: "0000000000166938e6f172a21fe69fe335e33565539e74bf74eeb00d2022c226")!.reversed()), timestamp: 1_476_926_743, target: 0x1c00ffff ), - Checkpoint(height: 1_008_000, hash: Data(Data(hex: "000000000000390aca616746a9456a0d64c1bd73661fd60a51b5bf1c92bae5a0")!.reversed()), timestamp: 1_490_751_239, target: 0x1a52ccc0) + Checkpoint( + height: 0, + hash: Data(Data(hex: "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943")!.reversed()), + timestamp: 1_376_543_922, + target: 0x1d00_ffff + ), + Checkpoint( + height: 100_800, + hash: Data(Data(hex: "0000000000a33112f86f3f7b0aa590cb4949b84c2d9c673e9e303257b3be9000")!.reversed()), + timestamp: 1_393_813_869, + target: 0x1c00_d907 + ), + Checkpoint( + height: 201_600, + hash: Data(Data(hex: "0000000000376bb71314321c45de3015fe958543afcbada242a3b1b072498e38")!.reversed()), + timestamp: 1_413_766_239, + target: 0x1b60_2ac0 + ), + Checkpoint( + height: 302_400, + hash: Data(Data(hex: "0000000000001c93ebe0a7c33426e8edb9755505537ef9303a023f80be29d32d")!.reversed()), + timestamp: 1_431_821_666, + target: 0x1a33_605e + ), + Checkpoint( + height: 403_200, + hash: Data(Data(hex: "0000000000ef8b05da54711e2106907737741ac0278d59f358303c71d500f3c4")!.reversed()), + timestamp: 1_436_951_946, + target: 0x1c02_346c + ), + Checkpoint( + height: 504_000, + hash: Data(Data(hex: "0000000000005d105473c916cd9d16334f017368afea6bcee71629e0fcf2f4f5")!.reversed()), + timestamp: 1_447_484_641, + target: 0x1b00_ab86 + ), + Checkpoint( + height: 604_800, + hash: Data(Data(hex: "00000000000008653c7e5c00c703c5a9d53b318837bb1b3586a3d060ce6fff2e")!.reversed()), + timestamp: 1_455_728_685, + target: 0x1a09_2a20 + ), + Checkpoint( + height: 705_600, + hash: Data(Data(hex: "00000000004ee3bc2e2dd06c31f2d7a9c3e471ec0251924f59f222e5e9c37e12")!.reversed()), + timestamp: 1_462_006_183, + target: 0x1c0f_fff0 + ), + Checkpoint( + height: 806_400, + hash: Data(Data(hex: "0000000000000faf114ff29df6dbac969c6b4a3b407cd790d3a12742b50c2398")!.reversed()), + timestamp: 1_469_705_562, + target: 0x1a34_e280 + ), + Checkpoint( + height: 907_200, + hash: Data(Data(hex: "0000000000166938e6f172a21fe69fe335e33565539e74bf74eeb00d2022c226")!.reversed()), + timestamp: 1_476_926_743, + target: 0x1c00_ffff + ), + Checkpoint( + height: 1_008_000, + hash: Data(Data(hex: "000000000000390aca616746a9456a0d64c1bd73661fd60a51b5bf1c92bae5a0")!.reversed()), + timestamp: 1_490_751_239, + target: 0x1a52_ccc0 + ) ] } override public var genesisBlock: Data { diff --git a/BitcoinKit/Sources/BitcoinKit/Core/SegWitBech32.swift b/BitcoinKit/Sources/BitcoinKit/Core/SegWitBech32.swift index 99c0b94ba..503cae9c9 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/SegWitBech32.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/SegWitBech32.swift @@ -32,12 +32,12 @@ public class SegWitBech32 { if bits != 0 { odata.append(UInt8((acc << (to - bits)) & maxv)) } - } else if (bits >= from || ((acc << (to - bits)) & maxv) != 0) { + } else if bits >= from || ((acc << (to - bits)) & maxv) != 0 { throw CoderError.bitsConversionFailed } return odata } - + /// Decode segwit address public static func decode(hrp: String, addr: String, hasAdvanced: Bool = true) throws -> (version: UInt8, program: Data) { let dec = try Bech32.shared.decode(addr) @@ -63,13 +63,13 @@ public class SegWitBech32 { } return (dec.checksum[0], conv) } - + /// Encode segwit address public static func encode(hrp: String, version: UInt8, program: Data, encoding: Bech32.Encoding) throws -> String { var enc = Data([version]) enc.append(try convertBits(from: 8, to: 5, pad: true, idata: program)) let result = Bech32.shared.encode(hrp, values: enc, encoding: encoding) - guard let _ = try? decode(hrp: hrp, addr: result) else { + guard (try? decode(hrp: hrp, addr: result)) != nil else { throw CoderError.encodingCheckFailed } return result @@ -84,14 +84,14 @@ extension SegWitBech32 { case bitsConversionFailed case hrpMismatch(String, String) case checksumSizeTooLow - + case dataSizeMismatch(Int) case segwitVersionNotSupported(UInt8) case segwitV0ProgramSizeMismatch(Int) case segwitVersionAndEncodingMismatch case encodingCheckFailed - + public var errorDescription: String? { switch self { case .bitsConversionFailed: diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Serialization.swift b/BitcoinKit/Sources/BitcoinKit/Core/Serialization.swift index 743e32151..74d2c171a 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Serialization.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Serialization.swift @@ -29,12 +29,12 @@ import Foundation // swiftlint:disable operator_whitespace protocol BinaryConvertible { - static func +(lhs: Data, rhs: Self) -> Data - static func +=(lhs: inout Data, rhs: Self) + static func + (lhs: Data, rhs: Self) -> Data + static func += (lhs: inout Data, rhs: Self) } extension BinaryConvertible { - static func +(lhs: Data, rhs: Self) -> Data { + static func + (lhs: Data, rhs: Self) -> Data { var value = rhs let data = withUnsafePointer(to: &value) { ptr -> Data in return Data(buffer: UnsafeBufferPointer(start: ptr, count: 1)) @@ -42,7 +42,7 @@ extension BinaryConvertible { return lhs + data } - static func +=(lhs: inout Data, rhs: Self) { + static func += (lhs: inout Data, rhs: Self) { lhs = lhs + rhs } } @@ -58,19 +58,19 @@ extension Int64: BinaryConvertible {} extension Int: BinaryConvertible {} extension Bool: BinaryConvertible { - static func +(lhs: Data, rhs: Bool) -> Data { + static func + (lhs: Data, rhs: Bool) -> Data { return lhs + (rhs ? UInt8(0x01) : UInt8(0x00)).littleEndian } } extension String: BinaryConvertible { - static func +(lhs: Data, rhs: String) -> Data { + static func + (lhs: Data, rhs: String) -> Data { guard let data = rhs.data(using: .ascii) else { return lhs } return lhs + data } } -func +(lhs: Data, rhs: OpCodeProtocol) -> Data { +func + (lhs: Data, rhs: OpCodeProtocol) -> Data { return lhs + rhs.value } func += (lhs: inout Data, rhs: OpCodeProtocol) { @@ -78,7 +78,7 @@ func += (lhs: inout Data, rhs: OpCodeProtocol) { } extension Data: BinaryConvertible { - static func +(lhs: Data, rhs: Data) -> Data { + static func + (lhs: Data, rhs: Data) -> Data { var data = Data() data.append(lhs) data.append(rhs) diff --git a/BitcoinKit/Sources/BitcoinKit/Core/SighashType.swift b/BitcoinKit/Sources/BitcoinKit/Core/SighashType.swift index 1633b6614..fcb856679 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/SighashType.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/SighashType.swift @@ -24,13 +24,13 @@ import Foundation -private let SIGHASH_ALL: UInt8 = 0x01 // 00000001 -private let SIGHASH_NONE: UInt8 = 0x02 // 00000010 -private let SIGHASH_SINGLE: UInt8 = 0x03 // 00000011 -private let SIGHASH_FORK_ID: UInt8 = 0x40 // 01000000 -private let SIGHASH_ANYONECANPAY: UInt8 = 0x80 // 10000000 +private let SIGHASH_ALL: UInt8 = 0x01 // 00000001 +private let SIGHASH_NONE: UInt8 = 0x02 // 00000010 +private let SIGHASH_SINGLE: UInt8 = 0x03 // 00000011 +private let SIGHASH_FORK_ID: UInt8 = 0x40 // 01000000 +private let SIGHASH_ANYONECANPAY: UInt8 = 0x80 // 10000000 -private let SIGHASH_OUTPUT_MASK: UInt8 = 0x1f // 00011111 +private let SIGHASH_OUTPUT_MASK: UInt8 = 0x1f // 00011111 public struct SighashType { fileprivate let uint8: UInt8 @@ -59,21 +59,21 @@ public struct SighashType { } public struct BCH { - public static let ALL: SighashType = SighashType(SIGHASH_FORK_ID + SIGHASH_ALL) // 01000001 - public static let NONE: SighashType = SighashType(SIGHASH_FORK_ID + SIGHASH_NONE) // 01000010 - public static let SINGLE: SighashType = SighashType(SIGHASH_FORK_ID + SIGHASH_SINGLE) // 01000011 - public static let ALL_ANYONECANPAY: SighashType = SighashType(SIGHASH_FORK_ID + SIGHASH_ALL + SIGHASH_ANYONECANPAY) // 11000001 - public static let NONE_ANYONECANPAY: SighashType = SighashType(SIGHASH_FORK_ID + SIGHASH_NONE + SIGHASH_ANYONECANPAY) // 11000010 - public static let SINGLE_ANYONECANPAY: SighashType = SighashType(SIGHASH_FORK_ID + SIGHASH_SINGLE + SIGHASH_ANYONECANPAY) // 11000011 + public static let ALL: SighashType = SighashType(SIGHASH_FORK_ID + SIGHASH_ALL) // 01000001 + public static let NONE: SighashType = SighashType(SIGHASH_FORK_ID + SIGHASH_NONE) // 01000010 + public static let SINGLE: SighashType = SighashType(SIGHASH_FORK_ID + SIGHASH_SINGLE) // 01000011 + public static let ALL_ANYONECANPAY: SighashType = SighashType(SIGHASH_FORK_ID + SIGHASH_ALL + SIGHASH_ANYONECANPAY) // 11000001 + public static let NONE_ANYONECANPAY: SighashType = SighashType(SIGHASH_FORK_ID + SIGHASH_NONE + SIGHASH_ANYONECANPAY) // 11000010 + public static let SINGLE_ANYONECANPAY: SighashType = SighashType(SIGHASH_FORK_ID + SIGHASH_SINGLE + SIGHASH_ANYONECANPAY) // 11000011 } public struct BTC { - public static let ALL: SighashType = SighashType(SIGHASH_ALL) // 00000001 - public static let NONE: SighashType = SighashType(SIGHASH_NONE) // 00000010 - public static let SINGLE: SighashType = SighashType(SIGHASH_SINGLE) // 00000011 - public static let ALL_ANYONECANPAY: SighashType = SighashType(SIGHASH_ALL + SIGHASH_ANYONECANPAY) // 10000001 - public static let NONE_ANYONECANPAY: SighashType = SighashType(SIGHASH_NONE + SIGHASH_ANYONECANPAY) // 10000010 - public static let SINGLE_ANYONECANPAY: SighashType = SighashType(SIGHASH_SINGLE + SIGHASH_ANYONECANPAY) // 10000011 + public static let ALL: SighashType = SighashType(SIGHASH_ALL) // 00000001 + public static let NONE: SighashType = SighashType(SIGHASH_NONE) // 00000010 + public static let SINGLE: SighashType = SighashType(SIGHASH_SINGLE) // 00000011 + public static let ALL_ANYONECANPAY: SighashType = SighashType(SIGHASH_ALL + SIGHASH_ANYONECANPAY) // 10000001 + public static let NONE_ANYONECANPAY: SighashType = SighashType(SIGHASH_NONE + SIGHASH_ANYONECANPAY) // 10000010 + public static let SINGLE_ANYONECANPAY: SighashType = SighashType(SIGHASH_SINGLE + SIGHASH_ANYONECANPAY) // 10000011 } } diff --git a/BitcoinKit/Sources/BitcoinKit/Core/Transaction+SignatureHash.swift b/BitcoinKit/Sources/BitcoinKit/Core/Transaction+SignatureHash.swift index 31f97b27f..92864fddc 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/Transaction+SignatureHash.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/Transaction+SignatureHash.swift @@ -42,7 +42,8 @@ extension Transaction { internal func getSequenceHash(hashType: SighashType) -> Data { if !hashType.isAnyoneCanPay && !hashType.isSingle - && !hashType.isNone { + && !hashType.isNone + { // If none of the ANYONECANPAY, SINGLE, NONE sighash type is set, hashSequence is the double SHA256 of the serialization of nSequence of all inputs let serializedSequence: Data = inputs.reduce(Data()) { $0 + $1.sequence } return Crypto.sha256sha256(serializedSequence) @@ -54,7 +55,8 @@ extension Transaction { internal func getOutputsHash(index: Int, hashType: SighashType) -> Data { if !hashType.isSingle - && !hashType.isNone { + && !hashType.isNone + { // If the sighash type is neither SINGLE nor NONE, hashOutputs is the double SHA256 of the serialization of all output amounts (8-byte little endian) paired up with their scriptPubKey (serialized as scripts inside CTxOuts) let serializedOutputs: Data = outputs.reduce(Data()) { $0 + $1.serialized() } return Crypto.sha256sha256(serializedOutputs) diff --git a/BitcoinKit/Sources/BitcoinKit/Core/TransactionSignatureSerializer.swift b/BitcoinKit/Sources/BitcoinKit/Core/TransactionSignatureSerializer.swift index 9bf344838..ceb19813f 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/TransactionSignatureSerializer.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/TransactionSignatureSerializer.swift @@ -76,10 +76,12 @@ public struct TransactionSignatureSerializer { outputsToSerialize = tx.outputs } - let tmp = Transaction(version: tx.version, - inputs: inputsToSerialize, - outputs: outputsToSerialize, - lockTime: tx.lockTime) + let tmp = Transaction( + version: tx.version, + inputs: inputsToSerialize, + outputs: outputsToSerialize, + lockTime: tx.lockTime + ) return tmp.serialized() } } diff --git a/BitcoinKit/Sources/BitcoinKit/Core/UnitsAndLimits.swift b/BitcoinKit/Sources/BitcoinKit/Core/UnitsAndLimits.swift index 99c06c923..2712ab0ff 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/UnitsAndLimits.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/UnitsAndLimits.swift @@ -31,7 +31,7 @@ let BTC_BIP16_TIMESTAMP: UInt32 = 1_333_238_400 let BTC_MAX_SCRIPT_SIZE: Int = 10_000 // Maximum number of bytes per "pushdata" operation -let BTC_MAX_SCRIPT_ELEMENT_SIZE: Int = 520; // bytes +let BTC_MAX_SCRIPT_ELEMENT_SIZE: Int = 520 // bytes // Number of public keys allowed for OP_CHECKMULTISIG let BTC_MAX_KEYS_FOR_CHECKMULTISIG: Int = 20 diff --git a/BitcoinKit/Sources/BitcoinKit/Core/WordList.swift b/BitcoinKit/Sources/BitcoinKit/Core/WordList.swift index 57bd4decd..cc445d920 100644 --- a/BitcoinKit/Sources/BitcoinKit/Core/WordList.swift +++ b/BitcoinKit/Sources/BitcoinKit/Core/WordList.swift @@ -2082,7 +2082,7 @@ class WordList { """ return words.split(separator: "\n") }() - static var japanese: [String.SubSequence] = { + static var japanese: [String.SubSequence] = { let words = """ あいこくしん @@ -14406,7 +14406,7 @@ class WordList { """ return words.split(separator: "\n") }() - static var italian: [String.SubSequence] = { + static var italian: [String.SubSequence] = { let words = """ abaco diff --git a/BitcoinKit/Sources/BitcoinKit/Messages/AddressMessage.swift b/BitcoinKit/Sources/BitcoinKit/Messages/AddressMessage.swift index 2776e186b..6e2b682a4 100644 --- a/BitcoinKit/Sources/BitcoinKit/Messages/AddressMessage.swift +++ b/BitcoinKit/Sources/BitcoinKit/Messages/AddressMessage.swift @@ -38,7 +38,7 @@ public struct AddressMessage { let count = byteStream.read(VarInt.self) var addressList = [NetworkAddress]() for _ in 0.. Data { var data = Data() data += version @@ -88,12 +88,12 @@ public struct BlockHeader { data += transactionCount.serialized() return data } - + public static func deserialize(_ data: Data) -> BlockHeader { let byteStream = ByteStream(data) return deserialize(byteStream) } - + static func deserialize(_ byteStream: ByteStream) -> BlockHeader { let version = byteStream.read(Int32.self) let prevBlock = byteStream.read(Data.self, count: 32) @@ -102,6 +102,14 @@ public struct BlockHeader { let bits = byteStream.read(UInt32.self) let nonce = byteStream.read(UInt32.self) let transactionCount = byteStream.read(VarInt.self) - return BlockHeader(version: version, prevBlock: prevBlock, merkleRoot: merkleRoot, timestamp: timestamp, bits: bits, nonce: nonce, transactionCount: transactionCount) + return BlockHeader( + version: version, + prevBlock: prevBlock, + merkleRoot: merkleRoot, + timestamp: timestamp, + bits: bits, + nonce: nonce, + transactionCount: transactionCount + ) } } diff --git a/BitcoinKit/Sources/BitcoinKit/Messages/MerkleBlockMessage.swift b/BitcoinKit/Sources/BitcoinKit/Messages/MerkleBlockMessage.swift index f16b5ee49..436404d4b 100644 --- a/BitcoinKit/Sources/BitcoinKit/Messages/MerkleBlockMessage.swift +++ b/BitcoinKit/Sources/BitcoinKit/Messages/MerkleBlockMessage.swift @@ -82,6 +82,18 @@ public struct MerkleBlockMessage { for _ in 0.. Transaction { + + static public func createNewTransaction( + toAddress: Address, + amount: UInt64, + fee: UInt64, + changeAddress: Address, + utxos: [UnspentTransaction], + lockTime: UInt32 = 0, + keys: [PrivateKey] + ) -> Transaction { let unsignedTx = createUnsignedTx(toAddress: toAddress, amount: amount, fee: fee, changeAddress: changeAddress, utxos: utxos, lockTime: 0) let signedTransaction = signTx(unsignedTx: unsignedTx, keys: keys) return signedTransaction } - + static private func selectTx(from utxos: [UnspentTransaction], amount: UInt64, fee: UInt64) -> [UnspentTransaction] { - + var selected = [UnspentTransaction]() - + let target = amount + fee var transferAmount: UInt64 = 0 utxos.forEach { transaction in let amount = transaction.output.value - if (transferAmount < target) { + if transferAmount < target { transferAmount += amount - + selected.append(transaction) } } - + return selected } - - static private func createUnsignedTx(toAddress: Address, amount: UInt64, fee: UInt64, changeAddress: Address, utxos: [UnspentTransaction], lockTime: UInt32 = 0) -> UnsignedTransaction { + + static private func createUnsignedTx( + toAddress: Address, + amount: UInt64, + fee: UInt64, + changeAddress: Address, + utxos: [UnspentTransaction], + lockTime: UInt32 = 0 + ) -> UnsignedTransaction { let utxos = selectTx(from: utxos, amount: amount, fee: fee) let totalAmount: UInt64 = UInt64(utxos.reduce(0) { $0 + $1.output.value }) let change: UInt64 = totalAmount - amount - fee - + let lockingScriptTo = toAddress.lockingScript let lockingScriptChange = changeAddress.lockingScript - + var outputs = [TransactionOutput]() outputs.append(TransactionOutput(value: amount, lockingScript: lockingScriptTo)) if change > 0 { outputs.append(TransactionOutput(value: change, lockingScript: lockingScriptChange)) } - + let unsignedInputs = utxos.map { TransactionInput(previousOutput: $0.outpoint, signatureScript: Data(), sequence: UInt32.max) } let tx = BitcoinKit.Transaction(version: 1, inputs: unsignedInputs, outputs: outputs, lockTime: lockTime) return UnsignedTransaction(tx: tx, utxos: utxos) } - + static private func signTx(unsignedTx: UnsignedTransaction, keys: [PrivateKey]) -> Transaction { var inputsToSign = unsignedTx.tx.inputs var transactionToSign: BitcoinKit.Transaction { - return BitcoinKit.Transaction(version: unsignedTx.tx.version, inputs: inputsToSign, outputs: unsignedTx.tx.outputs, lockTime: unsignedTx.tx.lockTime) + return BitcoinKit.Transaction( + version: unsignedTx.tx.version, + inputs: inputsToSign, + outputs: unsignedTx.tx.outputs, + lockTime: unsignedTx.tx.lockTime + ) } - + // Signing let hashType = SighashType.BTC.ALL for (i, utxo) in unsignedTx.utxos.enumerated() { let pubkeyHash: Data = Script.getPublicKeyHash(from: utxo.output.lockingScript) - + let keysOfUtxo: [PrivateKey] = keys.filter { $0.publicKey().hashP2pkh == pubkeyHash } guard let key = keysOfUtxo.first else { print("No keys to this txout : \(utxo.output.value)") continue } print("Value of signing txout : \(utxo.output.value)") - + let sighash: Data = transactionToSign.signatureHash(for: utxo.output, inputIndex: i, hashType: SighashType.BTC.ALL) let signature: Data = try! BitcoinKit.Crypto.sign(sighash, privateKey: key) let txin = inputsToSign[i] let pubkey = key.publicKey() - + let unlockingScript = Script.buildPublicKeyUnlockingScript(signature: signature, pubkey: pubkey, hashType: hashType) - + inputsToSign[i] = TransactionInput(previousOutput: txin.previousOutput, signatureScript: unlockingScript, sequence: txin.sequence) } return transactionToSign diff --git a/BitcoinKit/Sources/BitcoinKit/Messages/TransactionInput.swift b/BitcoinKit/Sources/BitcoinKit/Messages/TransactionInput.swift index c9cfa6e92..d119fb192 100644 --- a/BitcoinKit/Sources/BitcoinKit/Messages/TransactionInput.swift +++ b/BitcoinKit/Sources/BitcoinKit/Messages/TransactionInput.swift @@ -36,7 +36,7 @@ public struct TransactionInput { public let signatureScript: Data /// Transaction version as defined by the sender. Intended for "replacement" of transactions when information is updated before inclusion into a block. public let sequence: UInt32 - + public var address: String? public init(previousOutput: TransactionOutPoint, signatureScript: Data, sequence: UInt32) { @@ -58,7 +58,7 @@ public struct TransactionInput { data += sequence return data } - + public mutating func unpack(with network: Network, addressConverter: AddressConverter) { address = addressConverter.extract(from: signatureScript, with: network)?.stringValue } diff --git a/BitcoinKit/Sources/BitcoinKit/Messages/TransactionOutput.swift b/BitcoinKit/Sources/BitcoinKit/Messages/TransactionOutput.swift index eb063de1f..7d1bc41de 100644 --- a/BitcoinKit/Sources/BitcoinKit/Messages/TransactionOutput.swift +++ b/BitcoinKit/Sources/BitcoinKit/Messages/TransactionOutput.swift @@ -34,7 +34,7 @@ public struct TransactionOutput { } /// Usually contains the public key as a Bitcoin script setting up conditions to claim this output public let lockingScript: Data - + public var address: String? public func scriptCode() -> Data { @@ -48,7 +48,7 @@ public struct TransactionOutput { self.value = value self.lockingScript = lockingScript } - + public init() { self.init(value: 0, lockingScript: Data()) } @@ -60,7 +60,7 @@ public struct TransactionOutput { data += lockingScript return data } - + public mutating func unpack(with network: Network) { if Script.isPublicKeyHashOut(self.lockingScript) { let pubKeyHash = Script.getPublicKeyHash(from: self.lockingScript) diff --git a/BitcoinKit/Sources/BitcoinKit/Messages/VarInt.swift b/BitcoinKit/Sources/BitcoinKit/Messages/VarInt.swift index 5e8403ba6..2d8cba616 100644 --- a/BitcoinKit/Sources/BitcoinKit/Messages/VarInt.swift +++ b/BitcoinKit/Sources/BitcoinKit/Messages/VarInt.swift @@ -43,7 +43,7 @@ public struct VarInt: ExpressibleByIntegerLiteral { 0xfd : 253 0xfe : 254 0xff : 255 - + 0~252 : 1-byte(0x00 ~ 0xfc) 253 ~ 65535: 3-byte(0xfd00fd ~ 0xfdffff) 65536 ~ 4294967295 : 5-byte(0xfe010000 ~ 0xfeffffffff) @@ -59,10 +59,10 @@ public struct VarInt: ExpressibleByIntegerLiteral { case 253...0xffff: length = 2 data = Data() + UInt8(0xfd).littleEndian + UInt16(value).littleEndian - case 0x10000...0xffffffff: + case 0x10000...0xffff_ffff: length = 4 data = Data() + UInt8(0xfe).littleEndian + UInt32(value).littleEndian - case 0x100000000...0xffffffffffffffff: + case 0x1_0000_0000...0xffff_ffff_ffff_ffff: fallthrough default: length = 8 diff --git a/BitcoinKit/Sources/BitcoinKit/Messages/VersionMessage.swift b/BitcoinKit/Sources/BitcoinKit/Messages/VersionMessage.swift index 387656a42..e3cda0215 100644 --- a/BitcoinKit/Sources/BitcoinKit/Messages/VersionMessage.swift +++ b/BitcoinKit/Sources/BitcoinKit/Messages/VersionMessage.swift @@ -71,17 +71,47 @@ public struct VersionMessage { let timestamp = byteStream.read(Int64.self) let yourAddress = NetworkAddress.deserialize(byteStream) guard byteStream.availableBytes > 0 else { - return VersionMessage(version: version, services: services, timestamp: timestamp, yourAddress: yourAddress, myAddress: nil, nonce: nil, userAgent: nil, startHeight: nil, relay: nil) + return VersionMessage( + version: version, + services: services, + timestamp: timestamp, + yourAddress: yourAddress, + myAddress: nil, + nonce: nil, + userAgent: nil, + startHeight: nil, + relay: nil + ) } let myAddress = NetworkAddress.deserialize(byteStream) let nonce = byteStream.read(UInt64.self) let userAgent = byteStream.read(VarString.self) let startHeight = byteStream.read(Int32.self) guard byteStream.availableBytes > 0 else { - return VersionMessage(version: version, services: services, timestamp: timestamp, yourAddress: yourAddress, myAddress: myAddress, nonce: nonce, userAgent: userAgent, startHeight: startHeight, relay: nil) + return VersionMessage( + version: version, + services: services, + timestamp: timestamp, + yourAddress: yourAddress, + myAddress: myAddress, + nonce: nonce, + userAgent: userAgent, + startHeight: startHeight, + relay: nil + ) } let relay = byteStream.read(Bool.self) - return VersionMessage(version: version, services: services, timestamp: timestamp, yourAddress: yourAddress, myAddress: myAddress, nonce: nonce, userAgent: userAgent, startHeight: startHeight, relay: relay) + return VersionMessage( + version: version, + services: services, + timestamp: timestamp, + yourAddress: yourAddress, + myAddress: myAddress, + nonce: nonce, + userAgent: userAgent, + startHeight: startHeight, + relay: relay + ) } } diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_0NOTEQUAL.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_0NOTEQUAL.swift index 3068a0d09..91d6a76f8 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_0NOTEQUAL.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_0NOTEQUAL.swift @@ -29,7 +29,7 @@ public struct OP0NotEqual: OpCodeProtocol { public var name: String { return "OP_0NOTEQUAL" } // (in -- out) - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try context.assertStackHeightGreaterThanOrEqual(1) let input = try context.number(at: -1) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_1ADD.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_1ADD.swift index 0000eb02c..083d82fda 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_1ADD.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_1ADD.swift @@ -29,7 +29,7 @@ public struct Op1Add: OpCodeProtocol { public var name: String { return "OP_1ADD" } // (in -- out) - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try context.assertStackHeightGreaterThanOrEqual(1) let input = try context.number(at: -1) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_2DIV.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_2DIV.swift index 4447ee35c..5bfd9a365 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_2DIV.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_2DIV.swift @@ -33,7 +33,7 @@ public struct Op2Div: OpCodeProtocol { } // (in -- out) - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try context.assertStackHeightGreaterThanOrEqual(1) let input = try context.number(at: -1) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_2MUL.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_2MUL.swift index bacdeac52..f7395c74d 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_2MUL.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_2MUL.swift @@ -33,7 +33,7 @@ public struct Op2Mul: OpCodeProtocol { } // (in -- out) - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try context.assertStackHeightGreaterThanOrEqual(1) let input = try context.number(at: -1) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_ABS.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_ABS.swift index 13609eb79..bcfb5e197 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_ABS.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_ABS.swift @@ -30,7 +30,7 @@ public struct OpAbsolute: OpCodeProtocol { public var name: String { return "OP_ABS" } // (in -- out) - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try context.assertStackHeightGreaterThanOrEqual(1) let input = try context.number(at: -1) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_ADD.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_ADD.swift index 68262ed17..d338fe77b 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_ADD.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_ADD.swift @@ -29,7 +29,7 @@ public struct OpAdd: OpCodeProtocol { public var name: String { return "OP_ADD" } // (x1 x2 -- out) - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try context.assertStackHeightGreaterThanOrEqual(2) let x1 = try context.number(at: -2) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_BOOLAND.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_BOOLAND.swift index 19ea9ce3a..67214707b 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_BOOLAND.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_BOOLAND.swift @@ -29,7 +29,7 @@ public struct OpBoolAnd: OpCodeProtocol { public var name: String { return "OP_BOOLAND" } // (x1 x2 -- out) - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try context.assertStackHeightGreaterThanOrEqual(2) let x1 = context.data(at: -2) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_BOOLOR.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_BOOLOR.swift index 5b22d6fd3..e3e5afa29 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_BOOLOR.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_BOOLOR.swift @@ -29,7 +29,7 @@ public struct OpBoolOr: OpCodeProtocol { public var name: String { return "OP_BOOLOR" } // (x1 x2 -- out) - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try context.assertStackHeightGreaterThanOrEqual(2) let x1 = context.data(at: -2) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_GREATERTHAN.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_GREATERTHAN.swift index 555d07d09..39c104623 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_GREATERTHAN.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_GREATERTHAN.swift @@ -29,7 +29,7 @@ public struct OpGreaterThan: OpCodeProtocol { public var name: String { return "OP_GREATERTHAN" } // (x1 x2 -- out) - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try context.assertStackHeightGreaterThanOrEqual(2) let x1 = try context.number(at: -2) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_GREATERTHANOREQUAL.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_GREATERTHANOREQUAL.swift index 80b544ccb..802c78cbb 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_GREATERTHANOREQUAL.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_GREATERTHANOREQUAL.swift @@ -29,7 +29,7 @@ public struct OpGreaterThanOrEqual: OpCodeProtocol { public var name: String { return "OP_GREATERTHANOREQUAL" } // (x1 x2 -- out) - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try context.assertStackHeightGreaterThanOrEqual(2) let x1 = try context.number(at: -2) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_LESSTHAN.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_LESSTHAN.swift index 09adabcb4..60e14b7a8 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_LESSTHAN.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_LESSTHAN.swift @@ -29,7 +29,7 @@ public struct OpLessThan: OpCodeProtocol { public var name: String { return "OP_LESSTHAN" } // (x1 x2 -- out) - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try context.assertStackHeightGreaterThanOrEqual(2) let x1 = try context.number(at: -2) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_LESSTHANOREQUAL.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_LESSTHANOREQUAL.swift index 8de257429..fbb72c9b5 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_LESSTHANOREQUAL.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_LESSTHANOREQUAL.swift @@ -29,7 +29,7 @@ public struct OpLessThanOrEqual: OpCodeProtocol { public var name: String { return "OP_LESSTHANOREQUAL" } // (x1 x2 -- out) - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try context.assertStackHeightGreaterThanOrEqual(2) let x1 = try context.number(at: -2) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_LSHIFT.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_LSHIFT.swift index fb310a81e..33ad0200f 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_LSHIFT.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_LSHIFT.swift @@ -33,7 +33,7 @@ public struct OpLShift: OpCodeProtocol { } // (x1 x2 -- out) - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try context.assertStackHeightGreaterThanOrEqual(2) let x1 = try context.number(at: -2) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_MAX.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_MAX.swift index 177b986b2..9962d2b8c 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_MAX.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_MAX.swift @@ -30,7 +30,7 @@ public struct OpMax: OpCodeProtocol { public var name: String { return "OP_MAX" } // (x1 x2 -- out) - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try context.assertStackHeightGreaterThanOrEqual(2) let x1 = try context.number(at: -2) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_MOD.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_MOD.swift index af8ae9b4f..c95b54b18 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_MOD.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_MOD.swift @@ -29,7 +29,7 @@ public struct OpMod: OpCodeProtocol { public var name: String { return "OP_MOD" } // (x1 x2 -- out) - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try context.assertStackHeightGreaterThanOrEqual(2) let x1 = try context.number(at: -2) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_MUL.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_MUL.swift index bf4f185f8..a66c51021 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_MUL.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_MUL.swift @@ -33,7 +33,7 @@ public struct OpMul: OpCodeProtocol { } // (x1 x2 -- out) - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try context.assertStackHeightGreaterThanOrEqual(2) let x1 = try context.number(at: -2) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_NEGATE.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_NEGATE.swift index 2ebafc404..9473faa7d 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_NEGATE.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_NEGATE.swift @@ -29,7 +29,7 @@ public struct OpNegate: OpCodeProtocol { public var name: String { return "OP_NEGATE" } // (in -- out) - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try context.assertStackHeightGreaterThanOrEqual(1) let input = try context.number(at: -1) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_NOT.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_NOT.swift index bff4f6e29..d90844a80 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_NOT.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_NOT.swift @@ -29,7 +29,7 @@ public struct OpNot: OpCodeProtocol { public var name: String { return "OP_NOT" } // (in -- out) - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try context.assertStackHeightGreaterThanOrEqual(1) let input = try context.number(at: -1) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_NUMEQUAL.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_NUMEQUAL.swift index 41b6e0740..264ad8704 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_NUMEQUAL.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_NUMEQUAL.swift @@ -29,7 +29,7 @@ public struct OpNumEqual: OpCodeProtocol { public var name: String { return "OP_NUMEQUAL" } // (x1 x2 -- out) - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try context.assertStackHeightGreaterThanOrEqual(2) let x1 = try context.number(at: -2) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_NUMEQUALVERIFY.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_NUMEQUALVERIFY.swift index cff68b0f2..a2a33c350 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_NUMEQUALVERIFY.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_NUMEQUALVERIFY.swift @@ -30,7 +30,7 @@ public struct OpNumEqualVerify: OpCodeProtocol { // input : x1 x2 // output : - / fail - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try OpCode.OP_NUMEQUAL.mainProcess(context) do { try OpCode.OP_VERIFY.mainProcess(context) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_NUMNOTEQUAL.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_NUMNOTEQUAL.swift index 11e228d5a..4f378e52a 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_NUMNOTEQUAL.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_NUMNOTEQUAL.swift @@ -29,7 +29,7 @@ public struct OpNumNotEqual: OpCodeProtocol { public var name: String { return "OP_NUMNOTEQUAL" } // (x1 x2 -- out) - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try context.assertStackHeightGreaterThanOrEqual(2) let x1 = try context.number(at: -2) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_RSHIFT.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_RSHIFT.swift index 776f9509d..fabd92f19 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_RSHIFT.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_RSHIFT.swift @@ -33,7 +33,7 @@ public struct OpRShift: OpCodeProtocol { } // (x1 x2 -- out) - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try context.assertStackHeightGreaterThanOrEqual(2) let x1 = try context.number(at: -2) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_SUB.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_SUB.swift index e0cccecee..74bc164e3 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_SUB.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_SUB.swift @@ -29,7 +29,7 @@ public struct OpSub: OpCodeProtocol { public var name: String { return "OP_SUB" } // (x1 x2 -- out) - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try context.assertStackHeightGreaterThanOrEqual(2) let x1 = try context.number(at: -2) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_WITHIN.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_WITHIN.swift index 31f577cb0..2aaa8a7da 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_WITHIN.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Arithmetic/OP_WITHIN.swift @@ -29,7 +29,7 @@ public struct OpWithin: OpCodeProtocol { public var name: String { return "OP_WITHIN" } // (x1 min max -- out) - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try context.assertStackHeightGreaterThanOrEqual(3) let x = try context.number(at: -3) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Bitwise Logic/OP_EQUALVERIFY.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Bitwise Logic/OP_EQUALVERIFY.swift index d51c11ed8..0532b3451 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Bitwise Logic/OP_EQUALVERIFY.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Bitwise Logic/OP_EQUALVERIFY.swift @@ -31,7 +31,7 @@ public struct OpEqualVerify: OpCodeProtocol { // input : x1 x2 // output : - / fail - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try OpCode.OP_EQUAL.mainProcess(context) do { try OpCode.OP_VERIFY.mainProcess(context) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Crypto/OP_CHECKMULTISIG.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Crypto/OP_CHECKMULTISIG.swift index 63544f4da..145610f72 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Crypto/OP_CHECKMULTISIG.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Crypto/OP_CHECKMULTISIG.swift @@ -38,7 +38,7 @@ public struct OpCheckMultiSig: OpCodeProtocol { // input : x sig1 sig2 ... pub1 pub2 // output : true / false - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { // Get numPublicKeys with validation try context.assertStackHeightGreaterThanOrEqual(1) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Crypto/OP_CHECKMULTISIGVERIFY.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Crypto/OP_CHECKMULTISIGVERIFY.swift index a878da0cd..087fc6d12 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Crypto/OP_CHECKMULTISIGVERIFY.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Crypto/OP_CHECKMULTISIGVERIFY.swift @@ -31,7 +31,7 @@ public struct OpCheckMultiSigVerify: OpCodeProtocol { // input : x sig1 sig2 ... pub1 pub2 // output : Nothing / fail - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try OpCode.OP_CHECKMULTISIG.mainProcess(context) do { try OpCode.OP_VERIFY.mainProcess(context) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Crypto/OP_CHECKSIGVERIFY.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Crypto/OP_CHECKSIGVERIFY.swift index b1f325a56..f24ce25bc 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Crypto/OP_CHECKSIGVERIFY.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Crypto/OP_CHECKSIGVERIFY.swift @@ -31,7 +31,7 @@ public struct OpCheckSigVerify: OpCodeProtocol { // input : sig pubkey // output : Nothing / fail - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try OpCode.OP_CHECKSIG.mainProcess(context) do { try OpCode.OP_VERIFY.mainProcess(context) diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Crypto/OP_HASH160.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Crypto/OP_HASH160.swift index 0ebd75675..7aaefb399 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Crypto/OP_HASH160.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Crypto/OP_HASH160.swift @@ -31,7 +31,7 @@ public struct OpHash160: OpCodeProtocol { // input : in // output : hash - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { try context.assertStackHeightGreaterThanOrEqual(1) let data: Data = context.stack.removeLast() diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Crypto/OP_RIPEMD160.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Crypto/OP_RIPEMD160.swift index 825f9cb02..22dee7d38 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Crypto/OP_RIPEMD160.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Crypto/OP_RIPEMD160.swift @@ -19,7 +19,7 @@ public struct OpRipemd160: OpCodeProtocol { try context.assertStackHeightGreaterThanOrEqual(1) let data: Data = context.stack.removeLast() - let hash: Data = Crypto.ripemd160(data) + let hash: Data = Crypto.ripemd160(data) context.stack.append(hash) } } diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Crypto/OP_SHA256.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Crypto/OP_SHA256.swift index d043bfa5e..f5edd643a 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Crypto/OP_SHA256.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Crypto/OP_SHA256.swift @@ -19,7 +19,7 @@ public struct OpSha256: OpCodeProtocol { try context.assertStackHeightGreaterThanOrEqual(1) let data: Data = context.stack.removeLast() - let hash: Data = Crypto.sha256(data) + let hash: Data = Crypto.sha256(data) context.stack.append(hash) } } diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Flow Control/OP_RETURN.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Flow Control/OP_RETURN.swift index f8fd09f16..75f47b97a 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Flow Control/OP_RETURN.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Flow Control/OP_RETURN.swift @@ -28,7 +28,7 @@ public struct OpReturn: OpCodeProtocol { public var value: UInt8 { return 0x6a } public var name: String { return "OP_RETURN" } - public func mainProcess(_ context: ScriptExecutionContext) throws { + public func mainProcess(_ context: ScriptExecutionContext) throws { throw OpCodeExecutionError.error("OP_RETURN was encountered") } } diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Lock Time/OP_CHECKLOCKTIMEVERIFY.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Lock Time/OP_CHECKLOCKTIMEVERIFY.swift index 6feedaac6..2f7c2016b 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Lock Time/OP_CHECKLOCKTIMEVERIFY.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Lock Time/OP_CHECKLOCKTIMEVERIFY.swift @@ -54,12 +54,14 @@ public struct OpCheckLockTimeVerify: OpCodeProtocol { // There are two kinds of nLockTime: lock-by-blockheight and lock-by-blocktime, distinguished by whether nLockTime < LOCKTIME_THRESHOLD. // // We want to compare apples to apples, so fail the script unless the type of nLockTime being tested is the same as the nLockTime in the transaction. - guard (tx.lockTime < BTC_LOCKTIME_THRESHOLD && nLockTime < BTC_LOCKTIME_THRESHOLD) || - (tx.lockTime >= BTC_LOCKTIME_THRESHOLD && nLockTime >= BTC_LOCKTIME_THRESHOLD) else { + guard + (tx.lockTime < BTC_LOCKTIME_THRESHOLD && nLockTime < BTC_LOCKTIME_THRESHOLD) + || (tx.lockTime >= BTC_LOCKTIME_THRESHOLD && nLockTime >= BTC_LOCKTIME_THRESHOLD) + else { throw OpCodeExecutionError.error("tx.lockTime and nLockTime should be the same kind.") } - guard nLockTime <= tx.lockTime else { + guard nLockTime <= tx.lockTime else { throw OpCodeExecutionError.error("The top stack item is greater than the transaction's nLockTime field") } @@ -72,7 +74,7 @@ public struct OpCheckLockTimeVerify: OpCodeProtocol { // Alternatively we could test all inputs, but testing just this input // minimizes the data required to prove correct CHECKLOCKTIMEVERIFY // execution. - let SEQUENCE_FINAL: UInt32 = 0xffffffff + let SEQUENCE_FINAL: UInt32 = 0xffff_ffff guard txin.sequence != SEQUENCE_FINAL else { throw OpCodeExecutionError.error("The input's nSequence field is equal to 0xffffffff.") } diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Lock Time/OP_CHECKSEQUENCEVERIFY.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Lock Time/OP_CHECKSEQUENCEVERIFY.swift index 303c50c74..173f087db 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Lock Time/OP_CHECKSEQUENCEVERIFY.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Lock Time/OP_CHECKSEQUENCEVERIFY.swift @@ -57,17 +57,19 @@ public struct OpCheckSequenceVerify: OpCodeProtocol { } let SEQUENCE_LOCKTIME_TYPE_FLAG: UInt32 = (1 << 22) - let SEQUENCE_LOCKTIME_MASK: UInt32 = 0x0000ffff + let SEQUENCE_LOCKTIME_MASK: UInt32 = 0x0000_ffff let nLockTimeMask: UInt32 = SEQUENCE_LOCKTIME_TYPE_FLAG | SEQUENCE_LOCKTIME_MASK let txToSequenceMasked: UInt32 = txToSequence & nLockTimeMask let nSequenceMasked: UInt32 = nSequence & nLockTimeMask - guard (txToSequenceMasked < SEQUENCE_LOCKTIME_TYPE_FLAG && nSequenceMasked < SEQUENCE_LOCKTIME_TYPE_FLAG) || - (txToSequenceMasked >= SEQUENCE_LOCKTIME_TYPE_FLAG && nSequenceMasked >= SEQUENCE_LOCKTIME_TYPE_FLAG) else { - throw OpCodeExecutionError.error("txToSequenceMasked and nSequenceMasked should be the same kind.") + guard + (txToSequenceMasked < SEQUENCE_LOCKTIME_TYPE_FLAG && nSequenceMasked < SEQUENCE_LOCKTIME_TYPE_FLAG) + || (txToSequenceMasked >= SEQUENCE_LOCKTIME_TYPE_FLAG && nSequenceMasked >= SEQUENCE_LOCKTIME_TYPE_FLAG) + else { + throw OpCodeExecutionError.error("txToSequenceMasked and nSequenceMasked should be the same kind.") } - guard nSequence <= txToSequenceMasked else { + guard nSequence <= txToSequenceMasked else { throw OpCodeExecutionError.error("The top stack item is greater than the transaction's nSequence field") } } diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Push Data/OP_PUSHDATA.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Push Data/OP_PUSHDATA.swift index d65de84e8..226f41c81 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Push Data/OP_PUSHDATA.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Push Data/OP_PUSHDATA.swift @@ -23,6 +23,7 @@ // import Foundation + // Any opcode with value < PUSHDATA1 is a length of the string to be pushed on the stack. // So opcode 0x01 is followed by 1 byte of data, 0x09 by 9 bytes and so on up to 0x4b (75 bytes) // PUSHDATA opcode is followed by N-byte length of the string that follows. diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Stack/OP_2ROT.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Stack/OP_2ROT.swift index 5f0f64120..4ef7975ca 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Stack/OP_2ROT.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/OP_CODE/Stack/OP_2ROT.swift @@ -36,7 +36,7 @@ public struct Op2Rot: OpCodeProtocol { let x1: Data = context.data(at: -6) let x2: Data = context.data(at: -5) let count: Int = context.stack.count - context.stack.removeSubrange(count - 6 ..< count - 4) + context.stack.removeSubrange(count - 6.. OpCode { @@ -47,9 +45,9 @@ public struct OpCodeFactory { /** Returns the OpCode which a given name. Returns OP_INVALIDOPCODE for unknown names. - + - parameter name: String corresponding to the OpCode - + - returns: The OpCode corresponding to name */ public static func get(with name: String) -> OpCode { @@ -62,9 +60,9 @@ public struct OpCodeFactory { /** Returns OP_1NEGATE, OP_0 .. OP_16 for ints from -1 to 16. Returns OP_INVALIDOPCODE for other ints. - + - parameter smallInteger: Int value from -1 to 16 - + - returns: The OpCode corresponding to smallInteger */ public typealias SmallInteger = Int @@ -84,9 +82,9 @@ public struct OpCodeFactory { /** Converts opcode OP_ or OP_1NEGATE to an Int value. If incorrect opcode is given, Int.max is returned. - + - parameter opcode: OpCode which can be OP_ or OP_1NEGATE - + - returns: Int value correspondint to OpCode */ public static func smallInteger(from opcode: OpCode) -> SmallInteger { diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/Opcode.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/Opcode.swift index 2d38b4a78..a36bca5bd 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/Opcode.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/Opcode.swift @@ -26,21 +26,29 @@ import Foundation public enum OpCode: OpCodeProtocol { // swiftlint:disable:next line_length - case OP_0, OP_FALSE, OP_PUSHDATA1, OP_PUSHDATA2, OP_PUSHDATA4, OP_1NEGATE, OP_RESERVED, OP_1, OP_TRUE, OP_2, OP_3, OP_4, OP_5, OP_6, OP_7, OP_8, OP_9, OP_10, OP_11, OP_12, OP_13, OP_14, OP_15, OP_16, OP_NOP, OP_VER, OP_IF, OP_NOTIF, OP_VERIF, OP_VERNOTIF, OP_ELSE, OP_ENDIF, OP_VERIFY, OP_RETURN, OP_TOALTSTACK, OP_FROMALTSTACK, OP_2DROP, OP_2DUP, OP_3DUP, OP_2OVER, OP_2ROT, OP_2SWAP, OP_IFDUP, OP_DEPTH, OP_DROP, OP_DUP, OP_NIP, OP_OVER, OP_PICK, OP_ROLL, OP_ROT, OP_SWAP, OP_TUCK, OP_CAT, OP_SIZE, OP_SPLIT, OP_NUM2BIN, OP_BIN2NUM, OP_INVERT, OP_AND, OP_OR, OP_XOR, OP_EQUAL, OP_EQUALVERIFY, OP_RESERVED1, OP_RESERVED2, OP_1ADD, OP_1SUB, OP_2MUL, OP_2DIV, OP_NEGATE, OP_ABS, OP_NOT, OP_0NOTEQUAL, OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_MOD, OP_LSHIFT, OP_RSHIFT, OP_BOOLAND, OP_BOOLOR, OP_NUMEQUAL, OP_NUMEQUALVERIFY, OP_NUMNOTEQUAL, OP_LESSTHAN, OP_GREATERTHAN, OP_LESSTHANOREQUAL, OP_GREATERTHANOREQUAL, OP_MIN, OP_MAX, OP_WITHIN, OP_RIPEMD160, OP_SHA1, OP_SHA256, OP_HASH160, OP_HASH256, OP_CODESEPARATOR, OP_CHECKSIG, OP_CHECKSIGVERIFY, OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY, OP_CHECKLOCKTIMEVERIFY, OP_CHECKSEQUENCEVERIFY, OP_PUBKEYHASH, OP_PUBKEY, OP_INVALIDOPCODE, OP_NOP1, OP_NOP4, OP_NOP5, OP_NOP6, OP_NOP7, OP_NOP8, OP_NOP9, OP_NOP10 - + case OP_0, OP_FALSE, OP_PUSHDATA1, OP_PUSHDATA2, OP_PUSHDATA4, OP_1NEGATE, OP_RESERVED, OP_1, OP_TRUE, OP_2, OP_3, OP_4, OP_5, OP_6, OP_7, OP_8, OP_9, + OP_10, OP_11, OP_12, OP_13, OP_14, OP_15, OP_16, OP_NOP, OP_VER, OP_IF, OP_NOTIF, OP_VERIF, OP_VERNOTIF, OP_ELSE, OP_ENDIF, OP_VERIFY, OP_RETURN, + OP_TOALTSTACK, OP_FROMALTSTACK, OP_2DROP, OP_2DUP, OP_3DUP, OP_2OVER, OP_2ROT, OP_2SWAP, OP_IFDUP, OP_DEPTH, OP_DROP, OP_DUP, OP_NIP, OP_OVER, OP_PICK, + OP_ROLL, OP_ROT, OP_SWAP, OP_TUCK, OP_CAT, OP_SIZE, OP_SPLIT, OP_NUM2BIN, OP_BIN2NUM, OP_INVERT, OP_AND, OP_OR, OP_XOR, OP_EQUAL, OP_EQUALVERIFY, + OP_RESERVED1, OP_RESERVED2, OP_1ADD, OP_1SUB, OP_2MUL, OP_2DIV, OP_NEGATE, OP_ABS, OP_NOT, OP_0NOTEQUAL, OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_MOD, + OP_LSHIFT, OP_RSHIFT, OP_BOOLAND, OP_BOOLOR, OP_NUMEQUAL, OP_NUMEQUALVERIFY, OP_NUMNOTEQUAL, OP_LESSTHAN, OP_GREATERTHAN, OP_LESSTHANOREQUAL, + OP_GREATERTHANOREQUAL, OP_MIN, OP_MAX, OP_WITHIN, OP_RIPEMD160, OP_SHA1, OP_SHA256, OP_HASH160, OP_HASH256, OP_CODESEPARATOR, OP_CHECKSIG, + OP_CHECKSIGVERIFY, OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY, OP_CHECKLOCKTIMEVERIFY, OP_CHECKSEQUENCEVERIFY, OP_PUBKEYHASH, OP_PUBKEY, OP_INVALIDOPCODE, + OP_NOP1, OP_NOP4, OP_NOP5, OP_NOP6, OP_NOP7, OP_NOP8, OP_NOP9, OP_NOP10 + static let p2pkhStart = Data([OpCode.OP_DUP.value, OpCode.OP_HASH160.value]) static let p2pkhFinish = Data([OpCode.OP_EQUALVERIFY.value, OpCode.OP_CHECKSIG.value]) static let p2pkFinish = Data([OpCode.OP_CHECKSIG.value]) static let p2shStart = Data([OpCode.OP_HASH160.value]) static let p2shFinish = Data([OpCode.OP_EQUAL.value]) - + static let pFromShCodes = [ OpCode.OP_CHECKSIG.value, OpCode.OP_CHECKSIGVERIFY.value, OpCode.OP_CHECKMULTISIG.value, OpCode.OP_CHECKMULTISIGVERIFY.value ] - + static let pushData1: UInt8 = 0x4c static let pushData2: UInt8 = 0x4d static let pushData4: UInt8 = 0x4e @@ -54,7 +62,7 @@ public enum OpCode: OpCodeProtocol { case .OP_PUSHDATA2: return OpPushData2() case .OP_PUSHDATA4: return OpPushData4() case .OP_1NEGATE: return Op1Negate() - case .OP_RESERVED: return OpReserved() // reserved and fail if executed + case .OP_RESERVED: return OpReserved() // reserved and fail if executed case .OP_1: return OpN(1) case .OP_TRUE: return OpCode.OP_1.opcode case .OP_2: return OpN(2) @@ -120,8 +128,8 @@ public enum OpCode: OpCodeProtocol { case .OP_XOR: return OpXor() case .OP_EQUAL: return OpEqual() case .OP_EQUALVERIFY: return OpEqualVerify() - case .OP_RESERVED1: return OpReserved1() // reserved and fail if executed - case .OP_RESERVED2: return OpReserved2() // reserved and fail if executed + case .OP_RESERVED1: return OpReserved1() // reserved and fail if executed + case .OP_RESERVED2: return OpReserved2() // reserved and fail if executed // 6. Arithmetic case .OP_1ADD: return Op1Add() @@ -165,8 +173,8 @@ public enum OpCode: OpCodeProtocol { case .OP_CHECKMULTISIGVERIFY: return OpCheckMultiSigVerify() // Lock Times - case .OP_CHECKLOCKTIMEVERIFY: return OpCheckLockTimeVerify() // previously OP_NOP2 - case .OP_CHECKSEQUENCEVERIFY: return OpCheckSequenceVerify() // previously OP_NOP3 + case .OP_CHECKLOCKTIMEVERIFY: return OpCheckLockTimeVerify() // previously OP_NOP2 + case .OP_CHECKSEQUENCEVERIFY: return OpCheckSequenceVerify() // previously OP_NOP3 // Pseudo Words case .OP_PUBKEYHASH: return OpPubkeyHash() @@ -186,7 +194,17 @@ public enum OpCode: OpCodeProtocol { } // swiftlint:disable:next line_length - internal static let list: [OpCode] = [OP_0, OP_FALSE, OP_PUSHDATA1, OP_PUSHDATA2, OP_PUSHDATA4, OP_1NEGATE, OP_RESERVED, OP_1, OP_TRUE, OP_2, OP_3, OP_4, OP_5, OP_6, OP_7, OP_8, OP_9, OP_10, OP_11, OP_12, OP_13, OP_14, OP_15, OP_16, OP_NOP, OP_VER, OP_IF, OP_NOTIF, OP_VERIF, OP_VERNOTIF, OP_ELSE, OP_ENDIF, OP_VERIFY, OP_RETURN, OP_TOALTSTACK, OP_FROMALTSTACK, OP_2DROP, OP_2DUP, OP_3DUP, OP_2OVER, OP_2ROT, OP_2SWAP, OP_IFDUP, OP_DEPTH, OP_DROP, OP_DUP, OP_NIP, OP_OVER, OP_PICK, OP_ROLL, OP_ROT, OP_SWAP, OP_TUCK, OP_CAT, OP_SIZE, OP_SPLIT, OP_NUM2BIN, OP_INVERT, OP_AND, OP_OR, OP_XOR, OP_EQUAL, OP_EQUALVERIFY, OP_RESERVED1, OP_RESERVED2, OP_BIN2NUM, OP_1ADD, OP_1SUB, OP_2MUL, OP_2DIV, OP_NEGATE, OP_ABS, OP_NOT, OP_0NOTEQUAL, OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_MOD, OP_LSHIFT, OP_RSHIFT, OP_BOOLAND, OP_BOOLOR, OP_NUMEQUAL, OP_NUMEQUALVERIFY, OP_NUMNOTEQUAL, OP_LESSTHAN, OP_GREATERTHAN, OP_LESSTHANOREQUAL, OP_GREATERTHANOREQUAL, OP_MIN, OP_MAX, OP_WITHIN, OP_RIPEMD160, OP_SHA1, OP_SHA256, OP_HASH160, OP_HASH256, OP_CODESEPARATOR, OP_CHECKSIG, OP_CHECKSIGVERIFY, OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY, OP_CHECKLOCKTIMEVERIFY, OP_CHECKSEQUENCEVERIFY, OP_PUBKEYHASH, OP_PUBKEY, OP_INVALIDOPCODE, OP_NOP1, OP_NOP4, OP_NOP5, OP_NOP6, OP_NOP7, OP_NOP8, OP_NOP9, OP_NOP10] + internal static let list: [OpCode] = [ + OP_0, OP_FALSE, OP_PUSHDATA1, OP_PUSHDATA2, OP_PUSHDATA4, OP_1NEGATE, OP_RESERVED, OP_1, OP_TRUE, OP_2, OP_3, OP_4, OP_5, OP_6, OP_7, OP_8, OP_9, OP_10, + OP_11, OP_12, OP_13, OP_14, OP_15, OP_16, OP_NOP, OP_VER, OP_IF, OP_NOTIF, OP_VERIF, OP_VERNOTIF, OP_ELSE, OP_ENDIF, OP_VERIFY, OP_RETURN, + OP_TOALTSTACK, OP_FROMALTSTACK, OP_2DROP, OP_2DUP, OP_3DUP, OP_2OVER, OP_2ROT, OP_2SWAP, OP_IFDUP, OP_DEPTH, OP_DROP, OP_DUP, OP_NIP, OP_OVER, OP_PICK, + OP_ROLL, OP_ROT, OP_SWAP, OP_TUCK, OP_CAT, OP_SIZE, OP_SPLIT, OP_NUM2BIN, OP_INVERT, OP_AND, OP_OR, OP_XOR, OP_EQUAL, OP_EQUALVERIFY, OP_RESERVED1, + OP_RESERVED2, OP_BIN2NUM, OP_1ADD, OP_1SUB, OP_2MUL, OP_2DIV, OP_NEGATE, OP_ABS, OP_NOT, OP_0NOTEQUAL, OP_ADD, OP_SUB, OP_MUL, OP_DIV, OP_MOD, + OP_LSHIFT, OP_RSHIFT, OP_BOOLAND, OP_BOOLOR, OP_NUMEQUAL, OP_NUMEQUALVERIFY, OP_NUMNOTEQUAL, OP_LESSTHAN, OP_GREATERTHAN, OP_LESSTHANOREQUAL, + OP_GREATERTHANOREQUAL, OP_MIN, OP_MAX, OP_WITHIN, OP_RIPEMD160, OP_SHA1, OP_SHA256, OP_HASH160, OP_HASH256, OP_CODESEPARATOR, OP_CHECKSIG, + OP_CHECKSIGVERIFY, OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY, OP_CHECKLOCKTIMEVERIFY, OP_CHECKSEQUENCEVERIFY, OP_PUBKEYHASH, OP_PUBKEY, OP_INVALIDOPCODE, + OP_NOP1, OP_NOP4, OP_NOP5, OP_NOP6, OP_NOP7, OP_NOP8, OP_NOP9, OP_NOP10 + ] public var name: String { return opcode.name @@ -203,7 +221,7 @@ public enum OpCode: OpCodeProtocol { public func mainProcess(_ context: ScriptExecutionContext) throws { try opcode.mainProcess(context) } - + public static func push(_ value: Int) -> Data { guard value != 0 else { return Data([0]) @@ -213,7 +231,7 @@ public enum OpCode: OpCodeProtocol { } return Data([UInt8(value + 0x50)]) } - + public static func push(_ data: Data) -> Data { let length = data.count var bytes = Data() @@ -222,13 +240,13 @@ public enum OpCode: OpCodeProtocol { case 0x00...0x4b: bytes = Data([UInt8(length)]) case 0x4c...0xff: bytes = Data([OpCode.pushData1]) + UInt8(length).littleEndian case 0x0100...0xffff: bytes = Data([OpCode.pushData2]) + UInt16(length).littleEndian - case 0x10000...0xffffffff: bytes = Data([OpCode.pushData4]) + UInt32(length).littleEndian + case 0x10000...0xffff_ffff: bytes = Data([OpCode.pushData4]) + UInt32(length).littleEndian default: return data } return bytes + data } - + public static func segWitOutputScript(_ data: Data, versionByte: Int = 0) -> Data { return OpCode.push(versionByte) + OpCode.push(data) } diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/Script.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/Script.swift index 2e36f82c4..84df7dfce 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/Script.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/Script.swift @@ -178,7 +178,7 @@ public class Script { return false } return opcode(at: 0) == OpCode.OP_HASH160 - && pushedData(at: 1)?.count == 20 // this is enough to match the exact byte template, any other encoding will be larger. + && pushedData(at: 1)?.count == 20 // this is enough to match the exact byte template, any other encoding will be larger. && opcode(at: 2) == OpCode.OP_EQUAL } @@ -281,10 +281,12 @@ public class Script { @discardableResult public func append(_ opcode: OpCode) throws -> Script { - let invalidOpCodes: [OpCode] = [.OP_PUSHDATA1, - .OP_PUSHDATA2, - .OP_PUSHDATA4, - .OP_INVALIDOPCODE] + let invalidOpCodes: [OpCode] = [ + .OP_PUSHDATA1, + .OP_PUSHDATA2, + .OP_PUSHDATA4, + .OP_INVALIDOPCODE + ] guard !invalidOpCodes.contains(where: { $0 == opcode }) else { throw ScriptError.error("\(opcode.name) cannot be executed alone.") } @@ -409,9 +411,8 @@ extension Script { } public static func isPublicKeyHashOut(_ script: Data) -> Bool { - return script.count == 25 && - script[0] == OpCode.OP_DUP && script[1] == OpCode.OP_HASH160 && script[2] == 20 && - script[23] == OpCode.OP_EQUALVERIFY && script[24] == OpCode.OP_CHECKSIG + return script.count == 25 && script[0] == OpCode.OP_DUP && script[1] == OpCode.OP_HASH160 && script[2] == 20 && script[23] == OpCode.OP_EQUALVERIFY + && script[24] == OpCode.OP_CHECKSIG } public static func getPublicKeyHash(from script: Data) -> Data { diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptChunk.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptChunk.swift index 024152509..8156cb523 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptChunk.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptChunk.swift @@ -127,7 +127,7 @@ public struct DataChunk: ScriptChunk { public var string: String { var string: String guard !data.isEmpty else { - return "OP_0" // Empty data is encoded as OP_0. + return "OP_0" // Empty data is encoded as OP_0. } if isASCIIData(data: data) { @@ -165,13 +165,13 @@ public struct DataChunk: ScriptChunk { public var isDataCompact: Bool { switch opCode.value { case ...OpCode.OP_PUSHDATA1.value: - return true // length fits in one byte under OP_PUSHDATA1. + return true // length fits in one byte under OP_PUSHDATA1. case OpCode.OP_PUSHDATA1.value: - return data.count >= OpCode.OP_PUSHDATA1.value // length should not be less than OP_PUSHDATA1 + return data.count >= OpCode.OP_PUSHDATA1.value // length should not be less than OP_PUSHDATA1 case OpCode.OP_PUSHDATA2.value: - return data.count > (0xff) // length should not fit in one byte + return data.count > (0xff) // length should not fit in one byte case OpCode.OP_PUSHDATA4.value: - return data.count > (0xffff) // length should not fit in two bytes + return data.count > (0xffff) // length should not fit in two bytes default: return false } diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptChunkHelper.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptChunkHelper.swift index 1edb8ee02..fe1123eef 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptChunkHelper.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptChunkHelper.swift @@ -44,7 +44,7 @@ public struct ScriptChunkHelper { } else if data.count <= (0xffff) && (preferredLengthEncoding == -1 || preferredLengthEncoding == 2) { scriptData += OpCode.OP_PUSHDATA2 scriptData += UInt16(data.count) - } else if UInt64(data.count) <= 0xffffffff && (preferredLengthEncoding == -1 || preferredLengthEncoding == 4) { + } else if UInt64(data.count) <= 0xffff_ffff && (preferredLengthEncoding == -1 || preferredLengthEncoding == 4) { scriptData += OpCode.OP_PUSHDATA4 scriptData += UInt64(data.count) } else { @@ -109,7 +109,7 @@ public struct ScriptChunkHelper { _ = scriptData.withUnsafeBytes { memcpy(&dataLength, $0 + offset + MemoryLayout.size(ofValue: opcode), MemoryLayout.size(ofValue: dataLength)) } - dataLength = CFSwapInt32LittleToHost(dataLength) // CoreBitcoin uses CFSwapInt16LittleToHost(dataLength) + dataLength = CFSwapInt32LittleToHost(dataLength) // CoreBitcoin uses CFSwapInt16LittleToHost(dataLength) chunkLength = MemoryLayout.size(ofValue: opcode) + MemoryLayout.size(ofValue: dataLength) + Int(dataLength) default: // cannot happen because it's opcode diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptExecutionContext.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptExecutionContext.swift index 423e5aea2..c61c41cbf 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptExecutionContext.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptExecutionContext.swift @@ -45,7 +45,7 @@ public class ScriptExecutionContext { public private(set) var transaction: Transaction? public private(set) var utxoToVerify: TransactionOutput? public private(set) var txinToVerify: TransactionInput? - public private(set) var inputIndex: UInt32 = 0xffffffff + public private(set) var inputIndex: UInt32 = 0xffff_ffff // A timestamp of the current block. Default is current timestamp. // This is used to test for P2SH scripts or other changes in the protocol that may happen in the future. diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptFactory.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptFactory.swift index cf9eab7ac..1e5586331 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptFactory.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptFactory.swift @@ -37,25 +37,25 @@ public struct ScriptFactory { } // MARK: - Standard -public extension ScriptFactory.Standard { - static func buildP2PK(publickey: PublicKey) -> Script? { +extension ScriptFactory.Standard { + public static func buildP2PK(publickey: PublicKey) -> Script? { return try? Script() .appendData(publickey.data) .append(.OP_CHECKSIG) } - static func buildMultiSig(publicKeys: [PublicKey]) -> Script? { + public static func buildMultiSig(publicKeys: [PublicKey]) -> Script? { return Script(publicKeys: publicKeys, signaturesRequired: UInt(publicKeys.count)) } - static func buildMultiSig(publicKeys: [PublicKey], signaturesRequired: UInt) -> Script? { + public static func buildMultiSig(publicKeys: [PublicKey], signaturesRequired: UInt) -> Script? { return Script(publicKeys: publicKeys, signaturesRequired: signaturesRequired) } } // MARK: - LockTime -public extension ScriptFactory.LockTime { +extension ScriptFactory.LockTime { // Base - static func build(script: Script, lockDate: Date) -> Script? { + public static func build(script: Script, lockDate: Date) -> Script? { return try? Script() .appendData(lockDate.bigNumData) .append(.OP_CHECKLOCKTIMEVERIFY) @@ -63,15 +63,15 @@ public extension ScriptFactory.LockTime { .appendScript(script) } - static func build(script: Script, lockIntervalSinceNow: TimeInterval) -> Script? { + public static func build(script: Script, lockIntervalSinceNow: TimeInterval) -> Script? { let lockDate = Date(timeIntervalSinceNow: lockIntervalSinceNow) return build(script: script, lockDate: lockDate) } } // MARK: - OpReturn -public extension ScriptFactory.OpReturn { - static func build(text: String) -> Script? { +extension ScriptFactory.OpReturn { + public static func build(text: String) -> Script? { let MAX_OP_RETURN_DATA_SIZE: Int = 220 guard let data = text.data(using: .utf8), data.count <= MAX_OP_RETURN_DATA_SIZE else { return nil @@ -83,8 +83,8 @@ public extension ScriptFactory.OpReturn { } // MARK: - Condition -public extension ScriptFactory.Condition { - static func build(scripts: [Script]) -> Script? { +extension ScriptFactory.Condition { + public static func build(scripts: [Script]) -> Script? { guard !scripts.isEmpty else { return nil @@ -134,46 +134,46 @@ public extension ScriptFactory.Condition { OP_EQUALVERIFYs OP_CHECKSIG */ -public extension ScriptFactory.HashedTimeLockedContract { +extension ScriptFactory.HashedTimeLockedContract { // Base - static func build(recipient: Address, sender: Address, lockDate: Date, hash: Data, hashOp: HashOperator) -> Script? { + public static func build(recipient: Address, sender: Address, lockDate: Date, hash: Data, hashOp: HashOperator) -> Script? { guard hash.count == hashOp.hashSize else { return nil } return try? Script() .append(.OP_IF) - .append(hashOp.opcode) - .appendData(hash) - .append(.OP_EQUALVERIFY) - .append(.OP_DUP) - .append(.OP_HASH160) - .appendData(recipient.lockingScriptPayload) + .append(hashOp.opcode) + .appendData(hash) + .append(.OP_EQUALVERIFY) + .append(.OP_DUP) + .append(.OP_HASH160) + .appendData(recipient.lockingScriptPayload) .append(.OP_ELSE) - .appendData(lockDate.bigNumData) - .append(.OP_CHECKLOCKTIMEVERIFY) - .append(.OP_DROP) - .append(.OP_DUP) - .append(.OP_HASH160) - .appendData(sender.lockingScriptPayload) + .appendData(lockDate.bigNumData) + .append(.OP_CHECKLOCKTIMEVERIFY) + .append(.OP_DROP) + .append(.OP_DUP) + .append(.OP_HASH160) + .appendData(sender.lockingScriptPayload) .append(.OP_ENDIF) .append(.OP_EQUALVERIFY) .append(.OP_CHECKSIG) } // convenience - static func build(recipient: Address, sender: Address, lockIntervalSinceNow: TimeInterval, hash: Data, hashOp: HashOperator) -> Script? { + public static func build(recipient: Address, sender: Address, lockIntervalSinceNow: TimeInterval, hash: Data, hashOp: HashOperator) -> Script? { let lockDate = Date(timeIntervalSinceNow: lockIntervalSinceNow) return build(recipient: recipient, sender: sender, lockDate: lockDate, hash: hash, hashOp: hashOp) } - static func build(recipient: Address, sender: Address, lockIntervalSinceNow: TimeInterval, secret: Data, hashOp: HashOperator) -> Script? { + public static func build(recipient: Address, sender: Address, lockIntervalSinceNow: TimeInterval, secret: Data, hashOp: HashOperator) -> Script? { let hash = hashOp.hash(secret) let lockDate = Date(timeIntervalSinceNow: lockIntervalSinceNow) return build(recipient: recipient, sender: sender, lockDate: lockDate, hash: hash, hashOp: hashOp) } - static func build(recipient: Address, sender: Address, lockDate: Date, secret: Data, hashOp: HashOperator) -> Script? { + public static func build(recipient: Address, sender: Address, lockDate: Date, secret: Data, hashOp: HashOperator) -> Script? { let hash = hashOp.hash(secret) return build(recipient: recipient, sender: sender, lockDate: lockDate, hash: hash, hashOp: hashOp) } @@ -209,8 +209,8 @@ final public class HashOperatorHash160: HashOperator { } // MARK: - Utility Extension -private extension Date { - var bigNumData: Data { +extension Date { + fileprivate var bigNumData: Data { let dateUnix: TimeInterval = timeIntervalSince1970 let bn = BigNumber(Int32(dateUnix).littleEndian) return bn.data diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptMachine.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptMachine.swift index c8699303e..4c6c73e95 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptMachine.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptMachine.swift @@ -25,8 +25,8 @@ import Foundation public enum ScriptVerification { - case StrictEncoding // enforce strict conformance to DER and SEC2 for signatures and pubkeys (aka SCRIPT_VERIFY_STRICTENC) - case EvenS // enforce lower S values (below curve halforder) in signatures (aka SCRIPT_VERIFY_EVEN_S, depends on STRICTENC) + case StrictEncoding // enforce strict conformance to DER and SEC2 for signatures and pubkeys (aka SCRIPT_VERIFY_STRICTENC) + case EvenS // enforce lower S values (below curve halforder) in signatures (aka SCRIPT_VERIFY_EVEN_S, depends on STRICTENC) } public enum ScriptMachineError: Error { @@ -41,9 +41,14 @@ public enum ScriptMachineError: Error { // You can -copy a machine which will copy all the parameters and the stack state. public struct ScriptMachine { - public init() { } + public init() {} - public static func verifyTransaction(signedTx: Transaction, inputIndex: UInt32, utxo: TransactionOutput, blockTimeStamp: UInt32 = UInt32(NSTimeIntervalSince1970)) throws -> Bool { + public static func verifyTransaction( + signedTx: Transaction, + inputIndex: UInt32, + utxo: TransactionOutput, + blockTimeStamp: UInt32 = UInt32(NSTimeIntervalSince1970) + ) throws -> Bool { // Sanity check: transaction and its input should be consistent. guard inputIndex < signedTx.inputs.count else { throw ScriptMachineError.exception("Transaction and valid inputIndex are required for script verification.") @@ -117,8 +122,8 @@ public struct ScriptMachine { } } -private extension Array { - subscript (normalized index: Int) -> Element { +extension Array { + fileprivate subscript(normalized index: Int) -> Element { return (index < 0) ? self[count + index] : self[index] } } diff --git a/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptType.swift b/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptType.swift index f93e3a850..5e78ccb67 100644 --- a/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptType.swift +++ b/BitcoinKit/Sources/BitcoinKit/Scripts/ScriptType.swift @@ -9,7 +9,7 @@ import Foundation public enum ScriptType: Int { case unknown, p2pkh, p2pk, p2multi, p2sh, p2wsh, p2wpkh, p2wpkhSh, p2tr - + var size: Int { switch self { case .p2pk: return 35 @@ -22,7 +22,7 @@ public enum ScriptType: Int { case .unknown, .p2multi: return .zero } } - + var witness: Bool { self == .p2wpkh || self == .p2wpkhSh || self == .p2wsh || self == .p2tr } diff --git a/BitcoinKit/Sources/BitcoinKit/Wallet/Protocol/BitcoinKitDataStoreProtocol.swift b/BitcoinKit/Sources/BitcoinKit/Wallet/Protocol/BitcoinKitDataStoreProtocol.swift index 520df5e17..aca323983 100644 --- a/BitcoinKit/Sources/BitcoinKit/Wallet/Protocol/BitcoinKitDataStoreProtocol.swift +++ b/BitcoinKit/Sources/BitcoinKit/Wallet/Protocol/BitcoinKitDataStoreProtocol.swift @@ -36,7 +36,7 @@ internal enum DataStoreKey: String { case wif, utxos, transactions } -internal extension BitcoinKitDataStoreProtocol { +extension BitcoinKitDataStoreProtocol { func getString(forKey key: DataStoreKey) -> String? { return getString(forKey: key.rawValue) } diff --git a/BitcoinKit/Sources/BitcoinKitPrivate/BitcoinKit.Private.swift b/BitcoinKit/Sources/BitcoinKitPrivate/BitcoinKit.Private.swift index 459e12ab9..ff91c4389 100644 --- a/BitcoinKit/Sources/BitcoinKitPrivate/BitcoinKit.Private.swift +++ b/BitcoinKit/Sources/BitcoinKitPrivate/BitcoinKit.Private.swift @@ -19,7 +19,7 @@ public class _Hash { } return Data(result) } - + public static func sha256(_ data: Data) -> Data { var result = [UInt8](repeating: 0, count: Int(SHA256_DIGEST_LENGTH)) data.withUnsafeBytes { (ptr: UnsafePointer) in @@ -36,11 +36,11 @@ public class _Hash { } return Data(result) } - + static func sha256ripemd160(_ data: Data) -> Data { return ripemd160(sha256(data)) } - + public static func hmacsha512(_ data: Data, key: Data) -> Data { var result = [UInt8](repeating: 0, count: Int(SHA512_DIGEST_LENGTH)) var length: UInt32 = UInt32(SHA512_DIGEST_LENGTH) @@ -56,7 +56,7 @@ public class _Hash { public class _Key { public static func computePublicKey(fromPrivateKey privateKey: Data, compression: Bool) -> Data { - + let ctx = BN_CTX_new() defer { BN_CTX_free(ctx) @@ -66,7 +66,7 @@ public class _Key { EC_KEY_free(key) } let group = EC_KEY_get0_group(key) - + let prv = BN_new() defer { BN_free(prv) @@ -75,7 +75,7 @@ public class _Key { BN_bin2bn(ptr, Int32(privateKey.count), prv) return } - + let pub = EC_POINT_new(group) defer { EC_POINT_free(pub) @@ -83,7 +83,7 @@ public class _Key { EC_POINT_mul(group, pub, prv, nil, nil, ctx) EC_KEY_set_private_key(key, prv) EC_KEY_set_public_key(key, pub) - + if compression { EC_KEY_set_conv_form(key, POINT_CONVERSION_COMPRESSED) var ptr: UnsafeMutablePointer? @@ -100,7 +100,7 @@ public class _Key { return Data(result) } } - public static func deriveKey(_ password: Data, salt: Data, iterations:Int, keyLength: Int) -> Data { + public static func deriveKey(_ password: Data, salt: Data, iterations: Int, keyLength: Int) -> Data { var result = [UInt8](repeating: 0, count: keyLength) password.withUnsafeBytes { (passwordPtr: UnsafePointer) in salt.withUnsafeBytes { (saltPtr: UnsafePointer) in @@ -119,7 +119,7 @@ public class _HDKey { public let depth: UInt8 public let fingerprint: UInt32 public let childIndex: UInt32 - + public init(privateKey: Data?, publicKey: Data?, chainCode: Data, depth: UInt8, fingerprint: UInt32, childIndex: UInt32) { self.privateKey = privateKey self.publicKey = publicKey @@ -129,30 +129,30 @@ public class _HDKey { self.childIndex = childIndex } public func derived(at index: UInt32, hardened: Bool) -> _HDKey? { - + let ctx = BN_CTX_new() defer { BN_CTX_free(ctx) } var data = Data() if hardened { - data.append(0) // padding + data.append(0) // padding data += privateKey ?? Data() } else { data += publicKey ?? Data() } - - var childIndex = UInt32(hardened ? (0x80000000 | index) : index).bigEndian + + var childIndex = UInt32(hardened ? (0x8000_0000 | index) : index).bigEndian withUnsafePointer(to: &childIndex) { data.append(UnsafeBufferPointer(start: $0, count: 1)) } let digest = _Hash.hmacsha512(data, key: self.chainCode) let derivedPrivateKey = digest[0..<32] - let derivedChainCode = digest[32..<(32+32)] + let derivedChainCode = digest[32..<(32 + 32)] var curveOrder = BN_new() defer { BN_free(curveOrder) } BN_hex2bn(&curveOrder, "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141") - + let factor = BN_new() defer { BN_free(factor) @@ -165,7 +165,7 @@ public class _HDKey { if BN_cmp(factor, curveOrder) >= 0 { return nil } - + if let privateKey = self.privateKey { let privateKeyNum = BN_new()! defer { @@ -176,15 +176,15 @@ public class _HDKey { return } BN_mod_add(privateKeyNum, privateKeyNum, factor, curveOrder, ctx) - + // Check for invalid derivation. if BN_is_zero(privateKeyNum) == 1 { return nil } -// if privateKeyNum.pointee.top == 0 { // BN_is_zero -// return nil -// } - let numBytes = ((BN_num_bits(privateKeyNum)+7)/8) // BN_num_bytes + // if privateKeyNum.pointee.top == 0 { // BN_is_zero + // return nil + // } + let numBytes = ((BN_num_bits(privateKeyNum) + 7) / 8) // BN_num_bytes var result = [UInt8](repeating: 0, count: Int(numBytes)) BN_bn2bin(privateKeyNum, &result) let fingerprintData = _Hash.sha256ripemd160(publicKey ?? Data()) @@ -192,18 +192,20 @@ public class _HDKey { [UInt32](UnsafeBufferPointer(start: $0, count: fingerprintData.count)) } let reusltData = Data(result) - return _HDKey(privateKey: reusltData, - publicKey: reusltData, - chainCode: derivedChainCode, - depth: depth + 1, - fingerprint: fingerprintArray[0], - childIndex: childIndex) + return _HDKey( + privateKey: reusltData, + publicKey: reusltData, + chainCode: derivedChainCode, + depth: depth + 1, + fingerprint: fingerprintArray[0], + childIndex: childIndex + ) } else if let publicKey = self.publicKey { let publicKeyNum = BN_new() defer { BN_free(publicKeyNum) } - + publicKey.withUnsafeBytes { (ptr: UnsafePointer) in BN_bin2bn(ptr, Int32(publicKey.count), publicKeyNum) return @@ -215,7 +217,7 @@ public class _HDKey { } EC_POINT_bn2point(group, publicKeyNum, point, ctx) EC_POINT_mul(group, point, factor, point, BN_value_one(), ctx) - + // Check for invalid derivation. if EC_POINT_is_at_infinity(group, point) == 1 { return nil @@ -232,12 +234,14 @@ public class _HDKey { [UInt32](UnsafeBufferPointer(start: $0, count: fingerprintData.count)) } let reusltData = Data(result) - return _HDKey(privateKey: reusltData, - publicKey: reusltData, - chainCode: derivedChainCode, - depth: depth + 1, - fingerprint: fingerprintArray[0], - childIndex: childIndex) + return _HDKey( + privateKey: reusltData, + publicKey: reusltData, + chainCode: derivedChainCode, + depth: depth + 1, + fingerprint: fingerprintArray[0], + childIndex: childIndex + ) } else { return nil } @@ -248,49 +252,51 @@ public class _Crypto { public static func signMessage(_ data: Data, withPrivateKey privateKey: Data) throws -> Data { let ctx = secp256k1_context_create(UInt32(SECP256K1_CONTEXT_SIGN))! defer { secp256k1_context_destroy(ctx) } - + let signature = UnsafeMutablePointer.allocate(capacity: 1) defer { signature.deallocate() } let status = data.withUnsafeBytes { (ptr: UnsafePointer) in privateKey.withUnsafeBytes { secp256k1_ecdsa_sign(ctx, signature, ptr, $0, nil, nil) } } guard status == 1 else { throw CryptoError.signFailed } - + let normalizedsig = UnsafeMutablePointer.allocate(capacity: 1) defer { normalizedsig.deallocate() } secp256k1_ecdsa_signature_normalize(ctx, normalizedsig, signature) - + var length: size_t = 128 var der = Data(count: length) - guard der.withUnsafeMutableBytes({ return secp256k1_ecdsa_signature_serialize_der(ctx, $0, &length, normalizedsig) }) == 1 else { throw CryptoError.noEnoughSpace } + guard der.withUnsafeMutableBytes({ return secp256k1_ecdsa_signature_serialize_der(ctx, $0, &length, normalizedsig) }) == 1 else { + throw CryptoError.noEnoughSpace + } der.count = length - + return der } - + public static func verifySignature(_ signature: Data, message: Data, publicKey: Data) throws -> Bool { let ctx = secp256k1_context_create(UInt32(SECP256K1_CONTEXT_VERIFY))! defer { secp256k1_context_destroy(ctx) } - + let signaturePointer = UnsafeMutablePointer.allocate(capacity: 1) defer { signaturePointer.deallocate() } guard signature.withUnsafeBytes({ secp256k1_ecdsa_signature_parse_der(ctx, signaturePointer, $0, signature.count) }) == 1 else { throw CryptoError.signatureParseFailed } - + let pubkeyPointer = UnsafeMutablePointer.allocate(capacity: 1) defer { pubkeyPointer.deallocate() } guard publicKey.withUnsafeBytes({ secp256k1_ec_pubkey_parse(ctx, pubkeyPointer, $0, publicKey.count) }) == 1 else { throw CryptoError.publicKeyParseFailed } - + guard message.withUnsafeBytes({ secp256k1_ecdsa_verify(ctx, signaturePointer, $0, pubkeyPointer) }) == 1 else { return false } - + return true } - + public enum CryptoError: Error { case signFailed case noEnoughSpace diff --git a/CommonKit/.swiftpm/xcode/xcshareddata/xcbaselines/CommonKitTests.xcbaseline/096E20D1-E7E0-41CE-A0E1-51611374B8F0.plist b/CommonKit/.swiftpm/xcode/xcshareddata/xcbaselines/CommonKitTests.xcbaseline/096E20D1-E7E0-41CE-A0E1-51611374B8F0.plist new file mode 100644 index 000000000..a0fc3b256 --- /dev/null +++ b/CommonKit/.swiftpm/xcode/xcshareddata/xcbaselines/CommonKitTests.xcbaseline/096E20D1-E7E0-41CE-A0E1-51611374B8F0.plist @@ -0,0 +1,22 @@ + + + + + classNames + + HexBytesTests + + test_hexBytes_performance + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.027400 + baselineIntegrationDisplayName + Time + + + + + + diff --git a/CommonKit/.swiftpm/xcode/xcshareddata/xcbaselines/CommonKitTests.xcbaseline/E6A8A214-5DA6-46B7-BBFC-56A2DDE26780.plist b/CommonKit/.swiftpm/xcode/xcshareddata/xcbaselines/CommonKitTests.xcbaseline/E6A8A214-5DA6-46B7-BBFC-56A2DDE26780.plist new file mode 100644 index 000000000..7aaecd2de --- /dev/null +++ b/CommonKit/.swiftpm/xcode/xcshareddata/xcbaselines/CommonKitTests.xcbaseline/E6A8A214-5DA6-46B7-BBFC-56A2DDE26780.plist @@ -0,0 +1,22 @@ + + + + + classNames + + HexBytesTests + + test_hexBytes_performance() + + com.apple.XCTPerformanceMetric_WallClockTime + + baselineAverage + 0.029200 + baselineIntegrationDisplayName + Local Baseline + + + + + + diff --git a/CommonKit/.swiftpm/xcode/xcshareddata/xcbaselines/CommonKitTests.xcbaseline/Info.plist b/CommonKit/.swiftpm/xcode/xcshareddata/xcbaselines/CommonKitTests.xcbaseline/Info.plist new file mode 100644 index 000000000..2531a66d4 --- /dev/null +++ b/CommonKit/.swiftpm/xcode/xcshareddata/xcbaselines/CommonKitTests.xcbaseline/Info.plist @@ -0,0 +1,83 @@ + + + + + runDestinationsByUUID + + 096E20D1-E7E0-41CE-A0E1-51611374B8F0 + + localComputer + + busSpeedInMHz + 0 + cpuCount + 1 + cpuKind + Apple M1 + cpuSpeedInMHz + 0 + logicalCPUCoresPerPackage + 8 + modelCode + MacBookPro17,1 + physicalCPUCoresPerPackage + 8 + platformIdentifier + com.apple.platform.macosx + + targetArchitecture + arm64 + targetDevice + + busSpeedInMHz + 0 + cpuCount + 0 + cpuKind + + cpuSpeedInMHz + 0 + logicalCPUCoresPerPackage + 0 + modelCode + iPhone15,4 + physicalCPUCoresPerPackage + 0 + platformIdentifier + com.apple.platform.iphonesimulator + + + E6A8A214-5DA6-46B7-BBFC-56A2DDE26780 + + localComputer + + busSpeedInMHz + 0 + cpuCount + 1 + cpuKind + Apple M1 + cpuSpeedInMHz + 0 + logicalCPUCoresPerPackage + 8 + modelCode + MacBookPro17,1 + physicalCPUCoresPerPackage + 8 + platformIdentifier + com.apple.platform.macosx + + targetArchitecture + arm64 + targetDevice + + modelCode + iPhone15,4 + platformIdentifier + com.apple.platform.iphonesimulator + + + + + diff --git a/CommonKit/Package.swift b/CommonKit/Package.swift index 551da4b6d..6b006293b 100644 --- a/CommonKit/Package.swift +++ b/CommonKit/Package.swift @@ -19,7 +19,7 @@ let package = Package( ], dependencies: [ .package( - url: "https://github.com/krzyzanowskim/CryptoSwift.git", + url: "https://github.com/Adamant-im/CryptoSwift.git", .upToNextMinor(from: "1.5.0") ), .package( @@ -54,7 +54,8 @@ let package = Package( url: "https://github.com/apple/swift-async-algorithms", .upToNextMinor(from: "1.0.0") ), - .package(path: "../BitcoinKit") + .package(path: "../BitcoinKit"), + .package(path: "../AdamantWalletsKit") ], targets: [ // Targets are the basic building blocks of a package, defining a module or a test suite. @@ -71,7 +72,8 @@ let package = Package( "KeychainAccess", "RNCryptor", "Alamofire", - "BitcoinKit" + "BitcoinKit", + .product(name: "AdamantWalletsKit", package: "AdamantWalletsKit") ], resources: [ .process("./Assets/GitData.plist") diff --git a/CommonKit/Scripts/CoinsScript.rb b/CommonKit/Scripts/CoinsScript.rb deleted file mode 100755 index 3a94f2bea..000000000 --- a/CommonKit/Scripts/CoinsScript.rb +++ /dev/null @@ -1,348 +0,0 @@ -#!/usr/bin/ruby -#encoding: utf-8 -Encoding.default_external = Encoding::UTF_8 -Encoding.default_internal = Encoding::UTF_8 - -require "json" - -class Coins - - def createSwiftVariable(name, value, type, isStatic) - prefix = isStatic ? "static " : "" - text = " #{prefix}var #{name}: #{type} { - #{value} - } - " - return text - end - - # Get Health Check file - def get_health_check_params_from(json) - symbol = json["symbol"] - - # node_additional_info - node_additional_info = json["nodes"]["healthCheck"] - services = json["services"] - - if !services.nil? - service_additional_info = services["healthCheck"] - end - - normal_update_interval = nil - crucial_update_interval = nil - on_screen_update_interval = nil - threshold = nil - normal_service_update_interval = nil - crucial_service_update_interval = nil - on_screen_service_update_interval = nil - - if !service_additional_info.nil? - normal_service_update_interval = service_additional_info["normalUpdateInterval"] - crucial_service_update_interval = service_additional_info["crucialUpdateInterval"] - on_screen_service_update_interval = service_additional_info["onScreenUpdateInterval"] - end - - if !node_additional_info.nil? - normal_update_interval = node_additional_info["normalUpdateInterval"] - crucial_update_interval = node_additional_info["crucialUpdateInterval"] - on_screen_update_interval = node_additional_info["onScreenUpdateInterval"] - threshold = node_additional_info["threshold"] - - if normal_service_update_interval.nil? - normal_service_update_interval = normal_update_interval - end - if crucial_service_update_interval.nil? - crucial_service_update_interval = crucial_update_interval - end - if on_screen_service_update_interval.nil? - on_screen_service_update_interval = on_screen_update_interval - end - - text = "static let healthCheckParameters = CoinHealthCheckParameters( - normalUpdateInterval: #{normal_update_interval / 1000}, - crucialUpdateInterval: #{crucial_update_interval / 1000}, - onScreenUpdateInterval: #{on_screen_update_interval / 1000}, - threshold: #{threshold}, - normalServiceUpdateInterval: #{normal_service_update_interval / 1000}, - crucialServiceUpdateInterval: #{crucial_service_update_interval / 1000}, - onScreenServiceUpdateInterval: #{on_screen_service_update_interval / 1000} - )" - return text - end - - return nil - end - - # Update a swift file - def writeToSwiftFile(name, json) - - # Read data from json - - fullName = json["name"] - symbol = json["symbol"] - decimals = json["decimals"] - explorerTx = json["explorerTx"] - explorerAddress = json["explorerAddress"] - cryptoTransferDecimals = json["cryptoTransferDecimals"] - nodes = "" - nodesArray = json["nodes"]["list"] - if nodesArray != nil - nodesArray.each do |node| - url = node["url"] - altUrl = node["alt_ip"] - if altUrl == nil - nodes += "Node.makeDefaultNode(url: URL(string: \"#{url}\")!),\n" - else - nodes += "Node.makeDefaultNode(url: URL(string: \"#{url}\")!, altUrl: URL(string: \"#{altUrl}\")),\n" - end - end - end - - serviceNodes = "" - servicesInfo = json["services"] - if servicesInfo != nil - services = servicesInfo["list"] - if services != nil - serviceNodesArray = (symbol == "ADM") ? services["infoService"] : services["#{symbol.downcase}Service"] - if serviceNodesArray != nil - serviceNodesArray.each do |node| - url = node["url"] - serviceNodes += "Node.makeDefaultNode(url: URL(string: \"#{url}\")!),\n" - end - end - end - end - - fixedFee = json["fixedFee"] - if fixedFee == nil - fixedFee = json["defaultFee"] - end - if fixedFee == nil - fixedFee = 0.0 - end - - consistencyMaxTime = json["txConsistencyMaxTime"] - if consistencyMaxTime == nil - consistencyMaxTime = 0 - else - consistencyMaxTime = consistencyMaxTime / 1000 - end - minBalance = json["minBalance"] - if minBalance == nil - minBalance = 0 - end - minAmount = json["minTransferAmount"] - if minAmount == nil - minAmount = 0 - end - qqPrefix = json["qqPrefix"] - - defaultVisibility = json["defaultVisibility"] - if defaultVisibility == nil - defaultVisibility = false - end - - defaultOrdinalLevel = json["defaultOrdinalLevel"] - - nodesInfo = json["nodes"] - minNodeVersion = "nil" - - if nodesInfo != nil - minNodeVersion = nodesInfo["minVersion"] - end - - if minNodeVersion == nil - minNodeVersion = "nil" - else - minNodeVersion = "\"#{minNodeVersion}\"" - end - - # txFetchInfo - txFetchInfo = json["txFetchInfo"] - - newPendingInterval = nil - oldPendingInterval = nil - registeredInterval = nil - newPendingAttempts = nil - oldPendingAttempts = nil - - if !txFetchInfo.nil? - newPendingInterval = txFetchInfo["newPendingInterval"] - oldPendingInterval = txFetchInfo["oldPendingInterval"] - registeredInterval = txFetchInfo["registeredInterval"] - newPendingAttempts = txFetchInfo["newPendingAttempts"] - oldPendingAttempts = txFetchInfo["oldPendingAttempts"] - end - - # Gas for eth - reliabilityGasPricePercent = json["reliabilityGasPricePercent"] - reliabilityGasLimitPercent = json["reliabilityGasLimitPercent"] - defaultGasPriceGwei = json["defaultGasPriceGwei"] - defaultGasLimit = json["defaultGasLimit"] - warningGasPriceGwei = json["warningGasPriceGwei"] - - health_check_params = get_health_check_params_from(json) - - emptyText = "" - - # Create swift file - - text = "import Foundation -import BigInt -import CommonKit - -extension #{symbol.capitalize}WalletService { - // MARK: - Constants - static let fixedFee: Decimal = #{fixedFee} - static let currencySymbol = \"#{symbol}\" - static let currencyExponent: Int = -#{decimals} - static let qqPrefix: String = \"#{qqPrefix}\" - - #{health_check_params ? - health_check_params : - emptyText - } - -#{newPendingInterval ? - createSwiftVariable("newPendingInterval", newPendingInterval, "Int", true) : - emptyText - } - -#{oldPendingInterval ? - createSwiftVariable("oldPendingInterval", oldPendingInterval, "Int", true) : - emptyText - } - -#{registeredInterval ? - createSwiftVariable("registeredInterval", registeredInterval, "Int", true) : - emptyText - } - -#{newPendingAttempts ? - createSwiftVariable("newPendingAttempts", newPendingAttempts, "Int", true) : - emptyText - } - -#{oldPendingAttempts ? - createSwiftVariable("oldPendingAttempts", oldPendingAttempts, "Int", true) : - emptyText - } - -#{reliabilityGasPricePercent ? - createSwiftVariable("reliabilityGasPricePercent", reliabilityGasPricePercent, "BigUInt", false) : - emptyText - } - -#{reliabilityGasLimitPercent ? - createSwiftVariable("reliabilityGasLimitPercent", reliabilityGasLimitPercent, "BigUInt", false) : - emptyText - } - -#{defaultGasPriceGwei ? - createSwiftVariable("defaultGasPriceGwei", defaultGasPriceGwei, "BigUInt", false) : - emptyText - } - -#{defaultGasLimit ? - createSwiftVariable("defaultGasLimit", defaultGasLimit, "BigUInt", false) : - emptyText - } - -#{warningGasPriceGwei ? - createSwiftVariable("warningGasPriceGwei", warningGasPriceGwei, "BigUInt", false) : - emptyText - } - - var tokenName: String { - \"#{fullName}\" - } - - var consistencyMaxTime: Double { - #{consistencyMaxTime} - } - - var minBalance: Decimal { - #{minBalance} - } - - var minAmount: Decimal { - #{minAmount} - } - - var defaultVisibility: Bool { - #{defaultVisibility} - } - - var defaultOrdinalLevel: Int? { - #{defaultOrdinalLevel} - } - - static var minNodeVersion: String? { - #{minNodeVersion} - } - - var transferDecimals: Int { - #{cryptoTransferDecimals} - } - - static let explorerTx = \"#{explorerTx.sub! '${ID}', ''}\" - static let explorerAddress = \"#{explorerAddress.sub! '${ID}', ''}\" - - static var nodes: [Node] { - [ - #{nodes} - ] - } - - static var serviceNodes: [Node] { - [ - #{serviceNodes} - ] - } -} -" - # remove empty lines - text = text.gsub!(/\n+/, "\n") - - # If is ADM write to share file - if symbol == "ADM" - textResources = "import Foundation - -public extension AdamantResources { - // MARK: Nodes - static var nodes: [Node] { - [ - #{nodes} - ] - } -}" - File.open(Dir.pwd + "/CommonKit/Sources/CommonKit/AdamantDynamicResources.swift", 'w') { |file| file.write(textResources) } - File.open(Dir.pwd + "/Adamant/Modules/Wallets/#{name}/#{symbol}WalletService+DynamicConstants.swift", 'w') { |file| file.write(text) } - else - File.open(Dir.pwd + "/Adamant/Modules/Wallets/#{name}/#{symbol}WalletService+DynamicConstants.swift", 'w') { |file| file.write(text) } - end - end - - # Read JSON from a file - def readJson(folder) - file = open(folder + "/info.json") - walletName = folder.split('/').last - json = file.read - parsed = JSON.parse(json) - createCoin = parsed["createCoin"] - if createCoin == true - writeToSwiftFile(walletName.capitalize, parsed) - end - end - - # Go over all wallets - def startUnpack(branch) - wallets = Dir[Dir.pwd + "/scripts/wallets/adamant-wallets-#{branch}/assets/general/*"] - wallets.each do |wallet| - readJson(wallet) - end - end - -end - -Coins.new.startUnpack("dev") #master #dev diff --git a/CommonKit/Scripts/UpdateWalletsScript.sh b/CommonKit/Scripts/UpdateWalletsScript.sh deleted file mode 100755 index 10ebe5288..000000000 --- a/CommonKit/Scripts/UpdateWalletsScript.sh +++ /dev/null @@ -1,382 +0,0 @@ -ROOT="$PWD" -BRANCH_NAME="dev" #master #dev -SCRIPTS_DIR="$ROOT/scripts" -WALLETS_DIR="$ROOT/scripts/wallets" -WALLETS_NAME_DIR="$ROOT/scripts/wallets/adamant-wallets-$BRANCH_NAME/assets/general" -WALLETS_TOKENS_DIR="$ROOT/scripts/wallets/adamant-wallets-$BRANCH_NAME/assets/blockchains" - -# Download -function download () -{ - mkdir -p "$WALLETS_DIR" - cd "$WALLETS_DIR" - curl -fSsOL https://github.com/Adamant-im/adamant-wallets/archive/refs/heads/$BRANCH_NAME.zip - tar xzf $BRANCH_NAME.zip -} - -# create Contents for the image -function create_contents -{ - Target=$1 - IMAGE_NAME=$2 - With_Dark=$3 - - if [ $With_Dark = true ] - then - cat > ${Target}/Contents.json << __EOF__ -{ - "images" : [ - { - "filename" : "${IMAGE_NAME}.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "${IMAGE_NAME}_dark.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "${IMAGE_NAME}@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "${IMAGE_NAME}_dark@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "${IMAGE_NAME}@3x.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "${IMAGE_NAME}_dark@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} -__EOF__ - else - cat > ${Target}/Contents.json << __EOF__ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "${IMAGE_NAME}.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "${IMAGE_NAME}@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "${IMAGE_NAME}@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} -__EOF__ - fi -} - -# Move image to asset -function moveImage () -{ - FROM_DIR=$1 - WALLET_NAME=$2 - Target=$3 - IMAGE_NAME_FROM=$4 - IMAGE_NAME_TO=$5 - - cp $FROM_DIR/$WALLET_NAME/Images/${IMAGE_NAME_FROM}.png ${Target}/${IMAGE_NAME_TO}.png - cp $FROM_DIR/$WALLET_NAME/Images/${IMAGE_NAME_FROM}@2x.png ${Target}/${IMAGE_NAME_TO}@2x.png - cp $FROM_DIR/$WALLET_NAME/Images/${IMAGE_NAME_FROM}@3x.png ${Target}/${IMAGE_NAME_TO}@3x.png - - # check dark icons - if [ -e $FROM_DIR/$WALLET_NAME/Images/${IMAGE_NAME_FROM}_dark.png ] - then - cp $FROM_DIR/$WALLET_NAME/Images/${IMAGE_NAME_FROM}_dark.png ${Target}/${IMAGE_NAME_TO}_dark.png - cp $FROM_DIR/$WALLET_NAME/Images/${IMAGE_NAME_FROM}_dark@2x.png ${Target}/${IMAGE_NAME_TO}_dark@2x.png - cp $FROM_DIR/$WALLET_NAME/Images/${IMAGE_NAME_FROM}_dark@3x.png ${Target}/${IMAGE_NAME_TO}_dark@3x.png - create_contents ${Target} ${IMAGE_NAME_TO} true - else - create_contents ${Target} ${IMAGE_NAME_TO} false - fi -} - -# unpack icons -function unpackIcons () -{ - cd "$WALLETS_NAME_DIR" - for dir in $WALLETS_NAME_DIR/*/; do - WALLET_NAME=$(basename $dir) - - # if exist {name} - if [ -e $WALLETS_NAME_DIR/$WALLET_NAME/Images/${WALLET_NAME}_wallet.png ] - then - # copy @3x image to Notification Service Extension - Target_Notification_Content=$ROOT/NotificationServiceExtension/WalletImages - cp $WALLETS_NAME_DIR/$WALLET_NAME/Images/${WALLET_NAME}_wallet@3x.png ${Target_Notification_Content}/${WALLET_NAME}_notificationContent.png - - # move wallet images to assets - Target_Wallet_Image=$ROOT/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/${WALLET_NAME}_wallet.imageset - mkdir -p ${Target_Wallet_Image} - moveImage $WALLETS_NAME_DIR $WALLET_NAME ${Target_Wallet_Image} ${WALLET_NAME}_wallet ${WALLET_NAME}_wallet - fi - - # move notification images to assets - if [ -e $WALLETS_NAME_DIR/$WALLET_NAME/Images/${WALLET_NAME}_wallet.png ] - then - Target_Notification_Image=$ROOT/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/${WALLET_NAME}_notification.imageset - mkdir -p ${Target_Notification_Image} - if [ -e $WALLETS_NAME_DIR/$WALLET_NAME/Images/${WALLET_NAME}_notification.png ] - then - moveImage $WALLETS_NAME_DIR $WALLET_NAME ${Target_Notification_Image} ${WALLET_NAME}_notification ${WALLET_NAME}_notification - else - moveImage $WALLETS_NAME_DIR $WALLET_NAME ${Target_Notification_Image} ${WALLET_NAME}_wallet ${WALLET_NAME}_notification - fi - fi - - # move row images to assets - if [ -e $WALLETS_NAME_DIR/$WALLET_NAME/Images/${WALLET_NAME}_wallet.png ] - then - Target_Row_Image=$ROOT/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/${WALLET_NAME}_wallet_row.imageset - mkdir -p ${Target_Row_Image} - if [ -e $WALLETS_NAME_DIR/$WALLET_NAME/Images/${WALLET_NAME}_wallet_row.png ] - then - moveImage $WALLETS_NAME_DIR $WALLET_NAME ${Target_Row_Image} ${WALLET_NAME}_wallet_row ${WALLET_NAME}_wallet_row - else - moveImage $WALLETS_NAME_DIR $WALLET_NAME ${Target_Row_Image} ${WALLET_NAME}_wallet ${WALLET_NAME}_wallet_row - fi - fi - done -} - -# append token to blockchain swift file -function appendTokenToBlockchainFile () -{ - BLOCKCHAIN_NAME=$1 - WALLET_NAME=$2 - WALLET_SYMBOL=$3 - WALLET_CONTRACT=$4 - WALLET_DECIMALS=$5 - DEFAULT_VISABILITY_RAW=$6 - DEFAULT_ORDINAL_LEVEL_RAW=$7 - BLOCKCHAIN_TYPE=$8 - RELIABILITY_GAS_PRICE_PERCENT=$9 - RELIABILITY_GAS_LIMIT_PERCENT=${10} - DEFAULT_GAS_PRICE_GWEI=${11} - DEFAULT_GAS_LIMIT=${12} - WARNING_GAS_PRICE_GWEI=${13} - TRANSFERS_DECIMALS=${14} - - DEFAULT_VISABILITY="${DEFAULT_VISABILITY_RAW:=false}" - DEFAULT_ORDINAL_LEVEL="${DEFAULT_ORDINAL_LEVEL_RAW:=nil}" - -# to-do defaultVisibility & defaultOrdinalLevel from general info - TARGET=$ROOT/CommonKit/Sources/CommonKit/Models - cat >> ${TARGET}/${BLOCKCHAIN_NAME}TokensList.swift << __EOF__ - ${BLOCKCHAIN_TYPE}Token(symbol: "$WALLET_SYMBOL", - name: "$WALLET_NAME", - contractAddress: "$WALLET_CONTRACT", - decimals: $WALLET_DECIMALS, - naturalUnits: $WALLET_DECIMALS, - defaultVisibility: $DEFAULT_VISABILITY, - defaultOrdinalLevel: $DEFAULT_ORDINAL_LEVEL, - reliabilityGasPricePercent: $RELIABILITY_GAS_PRICE_PERCENT, - reliabilityGasLimitPercent: $RELIABILITY_GAS_LIMIT_PERCENT, - defaultGasPriceGwei: $DEFAULT_GAS_PRICE_GWEI, - defaultGasLimit: $DEFAULT_GAS_LIMIT, - warningGasPriceGwei: $WARNING_GAS_PRICE_GWEI, - transferDecimals: $TRANSFERS_DECIMALS), -__EOF__ -} - -# Write start file -function writeStartFile () -{ - TARGET=$1 - BLOCKCHAIN_NAME=$2 - BLOCKCHAIN_TYPE=$3 - - cat > ${TARGET}/${BLOCKCHAIN_NAME}TokensList.swift << __EOF__ - import Foundation - - public extension ${BLOCKCHAIN_TYPE}Token { - static let supportedTokens: [${BLOCKCHAIN_TYPE}Token] = [ - -__EOF__ -} - -# Write end file -function writeEndFile () -{ - TARGET=$1 - BLOCKCHAIN_NAME=$2 - - cat >> ${TARGET}/${BLOCKCHAIN_NAME}TokensList.swift << __EOF__ - ] - -} -__EOF__ -} - -# read value from JSON file -function readIntValueFromJson () -{ - KEY=$1 - JSON=$2 - - VALUE=$(perl -ne 'if (/"'"$KEY"'": (.*)/) { print $1 . "\n" }' $JSON) - echo "$VALUE" | sed -r 's/[,]+//g' -} - -# get fee value -# logic: -# get value from general/$token/info.json -# if value is empty then get value from general/$blockchain/info.json -# override value from blockchains/$blockchain/info.json -function getFeeValue () -{ - KEY=$1 - WALLET_GENERAL_JSON=$2 - WALLET_GENERAL_BLOCKCHAIN_JSON=$3 - WALLET_BLOCKCHAIN_JSON=$4 - - VALUE=$(readIntValueFromJson "$KEY" $WALLET_GENERAL_JSON) - VALUE_BLOCKCHAIN=$(readIntValueFromJson "$KEY" $WALLET_BLOCKCHAIN_JSON) - - if [ -z "$VALUE" ] - then - VALUE=$(readIntValueFromJson "$KEY" $WALLET_GENERAL_BLOCKCHAIN_JSON) - fi - - if [ ! -z "$VALUE_BLOCKCHAIN" ] - then - VALUE=$VALUE_BLOCKCHAIN - fi - - echo "$VALUE" -} - -# logic: -# get value from general/$token/info.json -# override value from blockchains/$token/info.json -function getOverrideValue () -{ - KEY=$1 - WALLET_GENERAL_JSON=$2 - WALLET_BLOCKCHAIN_JSON=$3 - - VALUE=$(readIntValueFromJson "$KEY" $WALLET_GENERAL_JSON) - VALUE_BLOCKCHAIN=$(readIntValueFromJson "$KEY" $WALLET_BLOCKCHAIN_JSON) - - if [ ! -z "$VALUE_BLOCKCHAIN" ] - then - VALUE=$VALUE_BLOCKCHAIN - fi - - echo "$VALUE" -} - -# set tokens -function setTokens () -{ - # go over all blockchains - for dir in $WALLETS_TOKENS_DIR/*/; do - BLOCKCHAIN_NAME=$(basename $dir) - TARGET=$ROOT/CommonKit/Sources/CommonKit/Models - BLOCKCHAIN_JSON=$WALLETS_TOKENS_DIR/$BLOCKCHAIN_NAME/info.json - BLOCKCHAIN_TYPE=$(perl -ne 'if (/"type": "(.*)"/) { print $1 . "\n" }' $BLOCKCHAIN_JSON) - - # Write start file - writeStartFile "$TARGET" "$BLOCKCHAIN_NAME" "$BLOCKCHAIN_TYPE" - - # go over all tokens in blockchains - for dir in $WALLETS_TOKENS_DIR/$BLOCKCHAIN_NAME/*/; do - # get data from blockchains dir - WALLET_NAME=$(basename $dir) - WALLET_JSON=$WALLETS_TOKENS_DIR/$BLOCKCHAIN_NAME/$WALLET_NAME/info.json - NAME=$(perl -ne 'if (/"name": "(.*)"/) { print $1 . "\n" }' $WALLET_JSON) - SYMBOL=$(perl -ne 'if (/"symbol": "(.*)"/) { print $1 . "\n" }' $WALLET_JSON) - CONTRACT=$(perl -ne 'if (/"contractId": "(.*)"/) { print $1 . "\n" }' $WALLET_JSON) - DECIMALS=$(perl -ne 'if (/"decimals": (.*)/) { print $1 . "\n" }' $WALLET_JSON) - - # get data from general dir - WALLET_GENERAL_JSON=$WALLETS_NAME_DIR/$WALLET_NAME/info.json - WALLET_GENERAL_BLOCKCHAIN_JSON=$WALLETS_NAME_DIR/$BLOCKCHAIN_NAME/info.json - WALLET_BLOCKCHAIN_JSON=$WALLETS_TOKENS_DIR/$BLOCKCHAIN_NAME/info.json - - DEFAULT_VISABILITY=$(getOverrideValue "defaultVisibility" $WALLET_GENERAL_JSON $WALLET_JSON) - DEFAULT_ORDINAL_LEVEL=$(getOverrideValue "defaultOrdinalLevel" $WALLET_GENERAL_JSON $WALLET_JSON) - - RELIABILITY_GAS_PRICE_PERCENT=$(getFeeValue "reliabilityGasPricePercent" $WALLET_GENERAL_JSON $WALLET_GENERAL_BLOCKCHAIN_JSON $WALLET_BLOCKCHAIN_JSON) - RELIABILITY_GAS_LIMIT_PERCENT=$(getFeeValue "reliabilityGasLimitPercent" $WALLET_GENERAL_JSON $WALLET_GENERAL_BLOCKCHAIN_JSON $WALLET_BLOCKCHAIN_JSON) - DEFAULT_GAS_PRICE_GWEI=$(getFeeValue "defaultGasPriceGwei" $WALLET_GENERAL_JSON $WALLET_GENERAL_BLOCKCHAIN_JSON $WALLET_BLOCKCHAIN_JSON) - DEFAULT_GAS_LIMIT=$(getFeeValue "defaultGasLimit" $WALLET_GENERAL_JSON $WALLET_GENERAL_BLOCKCHAIN_JSON $WALLET_BLOCKCHAIN_JSON) - WARNING_GAS_PRICE_GWEI=$(getFeeValue "warningGasPriceGwei" $WALLET_GENERAL_JSON $WALLET_GENERAL_BLOCKCHAIN_JSON $WALLET_BLOCKCHAIN_JSON) - - TRANSFERS_DECIMALS=$(getOverrideValue "cryptoTransferDecimals" $WALLET_GENERAL_JSON $WALLET_JSON) - - # append token to blockchain file - appendTokenToBlockchainFile "$BLOCKCHAIN_NAME" "$NAME" "$SYMBOL" "$CONTRACT" "$DECIMALS" "$DEFAULT_VISABILITY" "$DEFAULT_ORDINAL_LEVEL" "$BLOCKCHAIN_TYPE" "$RELIABILITY_GAS_PRICE_PERCENT" "$RELIABILITY_GAS_LIMIT_PERCENT" "$DEFAULT_GAS_PRICE_GWEI" "$DEFAULT_GAS_LIMIT" "$WARNING_GAS_PRICE_GWEI" "$TRANSFERS_DECIMALS" - done - - # Write end file - writeEndFile "$TARGET" "$BLOCKCHAIN_NAME" - - done -} - -# unpack data for coins -function unpackCoins () -{ - ./$ROOT/CommonKit/Scripts/CoinsScript.rb xcode -} - -# remove temp directory -function remove_script_directory () -{ - rm -r $SCRIPTS_DIR -} - -download - -unpackIcons - -setTokens - -# unpackCoins - -# remove_script_directory diff --git a/CommonKit/Sources/CommonKit/AdamantDynamicResources.swift b/CommonKit/Sources/CommonKit/AdamantDynamicResources.swift index 3bbf50113..280bf433c 100644 --- a/CommonKit/Sources/CommonKit/AdamantDynamicResources.swift +++ b/CommonKit/Sources/CommonKit/AdamantDynamicResources.swift @@ -1,51 +1,26 @@ +import AdamantWalletsKit import Foundation -public extension AdamantResources { +extension AdamantResources { // MARK: Nodes - static var nodes: [Node] { - [ - Node.makeDefaultNode(url: URL(string: "https://clown.adamant.im")!), - Node.makeDefaultNode(url: URL(string: "https://lake.adamant.im")!), - Node.makeDefaultNode( - url: URL(string: "https://endless.adamant.im")!, - altUrl: URL(string: "http://149.102.157.15:36666") - ), - Node.makeDefaultNode(url: URL(string: "https://bid.adamant.im")!), - Node.makeDefaultNode(url: URL(string: "https://unusual.adamant.im")!), - Node.makeDefaultNode( - url: URL(string: "https://debate.adamant.im")!, - altUrl: URL(string: "http://95.216.161.113:36666") - ), - Node.makeDefaultNode(url: URL(string: "http://78.47.205.206:36666")!), - Node.makeDefaultNode(url: URL(string: "http://5.161.53.74:36666")!), - Node.makeDefaultNode(url: URL(string: "http://184.94.215.92:45555")!), - Node.makeDefaultNode( - url: URL(string: "https://node1.adamant.business")!, - altUrl: URL(string: "http://194.233.75.29:45555") - ), - Node.makeDefaultNode(url: URL(string: "https://node2.blockchain2fa.io")!), - Node.makeDefaultNode( - url: URL(string: "https://phecda.adm.im")!, - altUrl: URL(string: "http://46.250.234.248:36666") - ), - Node.makeDefaultNode(url: URL(string: "https://tegmine.adm.im")!), - Node.makeDefaultNode( - url: URL(string: "https://tauri.adm.im")!, - altUrl: URL(string: "http://154.26.159.245:36666") - ), - Node.makeDefaultNode(url: URL(string: "https://dschubba.adm.im")!), - Node.makeDefaultNode( - url: URL(string: "https://tauri.bbry.org")!, - altUrl: URL(string: "http://54.197.36.175:36666") - ), - Node.makeDefaultNode( - url: URL(string: "https://endless.bbry.org")!, - altUrl: URL(string: "http://54.197.36.175:46666") - ), + public static var nodes: [Node] { + guard + let admWallet = CoinInfoProvider.storage?["ADM"], + let walletNodes = admWallet.nodes?.toNodes() + else { + print("Error: Unable to fetch wallet nodes for ADM.") + return [] + } + return walletNodes + } +} +extension CoinInfoDTO.Nodes { + func toNodes() -> [Node] { + list.map { walletNode in Node.makeDefaultNode( - url: URL(string: "https://debate.bbry.org")!, - altUrl: URL(string: "http://54.197.36.175:56666") + url: URL(string: walletNode.url)!, + altUrl: walletNode.altIP.flatMap { URL(string: $0) } ) - ] + } } } diff --git a/CommonKit/Sources/CommonKit/AdamantNotificationKeys.swift b/CommonKit/Sources/CommonKit/AdamantNotificationKeys.swift index a3d772f3b..e0d83f73f 100644 --- a/CommonKit/Sources/CommonKit/AdamantNotificationKeys.swift +++ b/CommonKit/Sources/CommonKit/AdamantNotificationKeys.swift @@ -16,20 +16,20 @@ public enum AdamantNotificationCategories { public enum AdamantNotificationUserInfoKeys { /// Transaction Id. Transaction that fired the push public static let transactionId = "txn-id" - + /// Address, registered for pushes public static let pushRecipient = "push-recipient" - + /// Partner(sender) display name public static let partnerDisplayName = "partner.displayName" - + // Chache flag, that display name were checked, and partner has none - no need to ckeck again public static let partnerNoDislpayNameKey = "partner.noDisplayName" public static let partnerNoDisplayNameValue = "true" - + /// Downloaded by NotificationServiceExtension transaction, serialized in JSON format public static let transaction = "cache.transaction" - + /// Decoded message, if push was handled locally by NotificationServiceExtension /// Use this to save time on downloading transaction, loading Core and decoding message public static let decodedMessage = "cache.decoded-message" diff --git a/CommonKit/Sources/CommonKit/AdamantResources.swift b/CommonKit/Sources/CommonKit/AdamantResources.swift index 749572179..492ce2873 100644 --- a/CommonKit/Sources/CommonKit/AdamantResources.swift +++ b/CommonKit/Sources/CommonKit/AdamantResources.swift @@ -8,42 +8,42 @@ import Foundation -public enum AdamantResources { +public enum AdamantResources { // MARK: ADAMANT Addresses public static let supportEmail = "business@adamant.im" public static let ansReadmeUrl = "https://github.com/Adamant-im/adamant-notificationService" - + // MARK: Contacts public enum contacts { public static let adamantWelcomeWallet = "U00000000000000000001" - + public static let adamantBountyWallet = "U15423595369615486571" public static let adamantBountyWalletPK = "cdab95b082b9774bd975677c868261618c7ce7bea97d02e0f56d483e30c077b6" - + public static let adamantNewBountyWallet = "U1835325601873095435" public static let adamantNewBountyWalletPK = "17c03b201dc2c26f5dbb712958132c90382ab6385d674b79b8718ba1a7eb5905" - + public static let adamantIco = "U7047165086065693428" public static let adamantIcoPK = "1e214309cc659646ecf1d90fa37be23fe76854a76e3b4da9e4d6b65a718baf8b" - + public static let adamantSupport = "U14077334060470162548" public static let adamantSupportPK = "824991512e52aa69885df8c79d64cddee9781737d10f36636bf2cf589b7e166a" - + public static let adamantExchange = "U5149447931090026688" public static let adamantExchangePK = "c61b50a5c72a1ee52a3060014478a5693ec69de15594a985a2b41d6f084caa9d" - + public static let betOnBitcoin = "U17840858470710371662" public static let betOnBitcoinPK = "ed1a7d9a8b0cd1485ae92fb78cebd45f852a24af1c983039904f765a1d581f0e" - + public static let ansAddress = "U10629337621822775991" public static let ansPublicKey = "188b24bd116a556ac8ba905bbbdaa16e237dfb14269f5a4f9a26be77537d977c" - + public static let donateWallet = "U380651761819723095" public static let donateWalletPK = "3af27b283de3ce76bdcb0d4a341208b6bc1a375c46610dfa11ca20a106ed43a8" - + public static let adelinaWallet = "U11138426591213238985" public static let adelinaWalletPK = "8e06eba03ebe4668148647fc00a64b3fae59a1911ce1fd1059baba39ceb705a4" - + public static let pwaBountyBot = "U1644771796259136854" public static let pwaBountyBotPK = "7a5c55dec7a085f1c795a126b3f74ebdccf36b7abfa2f85145443e58fcdff80c" } diff --git a/CommonKit/Sources/CommonKit/Assets/Localization/de.lproj/Localizable.strings b/CommonKit/Sources/CommonKit/Assets/Localization/de.lproj/Localizable.strings index 98ad666b7..7e70d1f25 100644 --- a/CommonKit/Sources/CommonKit/Assets/Localization/de.lproj/Localizable.strings +++ b/CommonKit/Sources/CommonKit/Assets/Localization/de.lproj/Localizable.strings @@ -461,7 +461,7 @@ "ChatScene.FreeTokensAlert.FreeTokens" = "🎁 Free tokens"; /* Chat: 'Free Tokens' message */ -"ChatScene.FreeTokensAlert.Message" = "ADAMANT is a unique Blockchain messenger that is independent from government, corporations and even its developers. It is possible due to a decentralized network, fully open source and run by users. That's why every action, including messaging or saving new contacts, has a network fee of 0.001 ADM. To start messaging now, get your free welcoming tokens."; +"ChatScene.FreeTokensAlert.Message" = "ADAMANT ist ein dezentraler Blockchain-Messenger. Daher fallen für jede Aktion, einschließlich Nachrichtenversand oder Speichern neuer Kontakte, Netzwerkgebühren in Höhe von 0,001 ADM an."; /* Chat: warning message for opening email link without mail app configurated on device */ "ChatScene.Warning.NoMailApp" = "Email kann nicht gesendet werden. Überprüfen Sie die E-Mail-App-Konfigurationen."; @@ -766,6 +766,8 @@ /* NodesEditor: Failed to build URL alert */ "NodesEditor.FailedToBuildURL" = "Invalid host"; +"chat.NewMessages" = "Neue Nachrichten"; + /* Storage: Save Encrypted */ "Storage.SaveEncrypted.Title" = "Die Datei verschlüsselt aufbewahren"; @@ -1377,3 +1379,17 @@ /* Chat unknown */ "Chat.unknown.title" = "Unbekannt"; + +/* No active nodes */ +"Chat.Alert.Title.NoActiveNodes" = "Keine aktiven ADM-Knoten"; +"Chat.Alert.NoActiveNodes" = "Es können keine neuen Nachrichten angefordert werden - Keine aktiven ADM-Blockchain-Knoten. Da Sie einige von ihnen deaktiviert haben, sollten Sie die Knotenliste überprüfen."; + +/* Timestamp in the future */ +"Chat.Alert.Title.TimestampIsInTheFuture" = "Gerätezeit prüfen"; +"Chat.Alert.TimestampIsInTheFuture" = "Es können keine neuen Nachrichten angefordert werden - keine aktiven ADM-Blockchain-Knoten. Da Sie einige von ihnen deaktiviert haben, sollten Sie die Knotenliste überprüfen."; +"Chat.Alert.TimeSettings" = "Zeiteinstellung"; + +/* Review Nodes List */ +"Chat.Alert.ReviewNodesList" = "ADM-Knotenliste überprüfen"; + +"Chat.Timestamp.InFuture.Error" = "Ein Netzwerkknoten hat die Nachricht abgelehnt, weil die Zeit auf Ihrem Gerät vorgeht.\nÜberprüfen Sie die Uhrzeit des Geräts oder versuchen Sie erneut, eine Nachricht zu senden."; diff --git a/CommonKit/Sources/CommonKit/Assets/Localization/en.lproj/Localizable.strings b/CommonKit/Sources/CommonKit/Assets/Localization/en.lproj/Localizable.strings index 0d17be684..01e99f3db 100644 --- a/CommonKit/Sources/CommonKit/Assets/Localization/en.lproj/Localizable.strings +++ b/CommonKit/Sources/CommonKit/Assets/Localization/en.lproj/Localizable.strings @@ -449,10 +449,17 @@ "ChatScene.Actions.NamePlaceholder" = "Name"; /* Chat: 'Free Tokens' button */ -"ChatScene.FreeTokensAlert.FreeTokens" = "🎁 Free tokens"; +"ChatScene.FreeTokensAlert.FreeTokens" = "🎁 Welcome coins"; +"ChatScene.FreeTokensAlert.Title.Chat" = "To start messaging now, claim free welcome coins or top up your ADM balance in another way"; + +"ChatScene.FreeTokensAlert.Title.Book" = "To save contacts, claim free welcome coins or top up your ADM balance in another way"; + +"ChatScene.FreeTokensAlert.Title.Notification" = "To enable push notifications, claim free welcome coins or top up your ADM balance in another way"; + +"ChatScene.FreeTokensAlert.BuyADM" = "Buy ADM"; /* Chat: 'Free Tokens' message */ -"ChatScene.FreeTokensAlert.Message" = "ADAMANT is a unique Blockchain messenger that is independent from government, corporations and even its developers. It is possible due to a decentralized network, fully open source and run by users. That's why every action, including messaging or saving new contacts, has a network fee of 0.001 ADM. To start messaging now, get your free welcoming tokens."; +"ChatScene.FreeTokensAlert.Message" = "ADAMANT is a decentralized blockchain messenger. Therefore, every action, including messaging or saving new contacts, incurs a network fee of 0.001 ADM."; /* Chat: warning message for opening email link without mail app configurated on device */ "ChatScene.Warning.NoMailApp" = "Can't send email. Check Mail app configurations."; @@ -751,6 +758,8 @@ /* NodesEditor: Failed to build URL alert */ "NodesEditor.FailedToBuildURL" = "Invalid host"; +"chat.NewMessages" = "New Messages"; + /* Storage: Save Encrypted */ "Storage.SaveEncrypted.Title" = "Store files encrypted"; @@ -1346,3 +1355,17 @@ /* Chat unknown */ "Chat.unknown.title" = "Unknown"; + +/* No active nodes */ +"Chat.Alert.Title.NoActiveNodes" = "No active ADM nodes"; +"Chat.Alert.NoActiveNodes" = "Unable to request new messages—No active ADM blockchain nodes. As you’ve deactivated some of them, consider reviewing the node list."; + +/* Timestamp in the future */ +"Chat.Alert.Title.TimestampIsInTheFuture" = "Check device time"; +"Chat.Alert.TimestampIsInTheFuture" = "A network node rejected the message because the time on your device is ahead. Check the device's time or try sending a message again."; +"Chat.Alert.TimeSettings" = "Time setting"; + +/* Review Nodes List */ +"Chat.Alert.ReviewNodesList" = "Review ADM node list"; + +"Chat.Timestamp.InFuture.Error" = "A network node rejected the message because the time on your device is ahead.\nCheck the device's time or try sending a message again."; diff --git a/CommonKit/Sources/CommonKit/Assets/Localization/ru.lproj/Localizable.strings b/CommonKit/Sources/CommonKit/Assets/Localization/ru.lproj/Localizable.strings index a50a3f859..055ab24ca 100644 --- a/CommonKit/Sources/CommonKit/Assets/Localization/ru.lproj/Localizable.strings +++ b/CommonKit/Sources/CommonKit/Assets/Localization/ru.lproj/Localizable.strings @@ -449,10 +449,18 @@ "ChatScene.Actions.NamePlaceholder" = "Имя"; /* Chat: 'Free Tokens' button */ -"ChatScene.FreeTokensAlert.FreeTokens" = "🎁 Бесплатные токены"; +"ChatScene.FreeTokensAlert.FreeTokens" = "🎁 Бесплатные монеты"; + +"ChatScene.FreeTokensAlert.Title.Chat" = "Чтобы начать общение, получите бесплатные монеты или пополните баланс ADM другим способом"; + +"ChatScene.FreeTokensAlert.Title.Book" = "Чтобы сохранить контакты, получите бесплатные монеты или пополните баланс ADM другим способом"; + +"ChatScene.FreeTokensAlert.Title.Notification" = "Чтобы включить пуш-уведомления, получите бесплатные монеты или пополните баланс ADM другим способом"; + +"ChatScene.FreeTokensAlert.BuyADM" = "Купить ADM"; /* Chat: 'Free Tokens' message */ -"ChatScene.FreeTokensAlert.Message" = "АДАМАНТ — мессенджер, полностью работающий на блокчейне, независимый от государств, корпораций и даже разработчиков. Это возможно благодаря децентрализованной сетевой инфраструктуре, с полностью открытым исходным кодом и поддерживаемой пользователями. Поэтому каждое действие, включая отправку сообщения или сохранение адресной книги, имеет комиссию сети 0.001 ADM. Чтобы начать общение, получите бесплатные токены."; +"ChatScene.FreeTokensAlert.Message" = "АДАМАНТ — децентрализованный мессенджер на блокчейне. Поэтому каждое действие, включая отправку сообщения или сохранение адресной книги, имеет комиссию сети 0.001 ADM."; /* Chat: warning message for opening email link without mail app configurated on device */ "ChatScene.Warning.NoMailApp" = "Не удалось открыть почтовое приложение. Проверьте настройки."; @@ -751,6 +759,8 @@ /* NodesEditor: Failed to build URL alert */ "NodesEditor.FailedToBuildURL" = "Некорректный адрес хоста"; +"chat.NewMessages" = "Новые сообщения"; + /* Storage: Save Encrypted */ "Storage.SaveEncrypted.Title" = "Хранить файлы зашифрованными"; @@ -1346,3 +1356,17 @@ /* Chat unknown */ "Chat.unknown.title" = "Неизвестный"; + +/* No active nodes */ +"Chat.Alert.Title.NoActiveNodes" = "Нет активных узлов ADM"; +"Chat.Alert.NoActiveNodes" = "Не удается получить новые сообщения — нет активных узлов блокчейна ADM. Поскольку вы отключили некоторые из них, посмотрите список узлов еще раз."; + +/* Timestamp in the future */ +"Chat.Alert.Title.TimestampIsInTheFuture" = "Проверьте время на устройстве"; +"Chat.Alert.TimestampIsInTheFuture" = "Узел сети отклонил сообщение, потому что время на вашем устройстве спешит. Проверьте время на устройстве или попробуйте отправить сообщение снова."; +"Chat.Alert.TimeSettings" = "Настроить время"; + +/* Review Nodes List */ +"Chat.Alert.ReviewNodesList" = "К списку узлов ADM"; + +"Chat.Timestamp.InFuture.Error" = "Узел сети отклонил сообщение, потому что время на вашем устройстве спешит.\nПроверьте время на устройстве или попробуйте отправить сообщение снова."; diff --git a/CommonKit/Sources/CommonKit/Assets/Localization/zh.lproj/Localizable.strings b/CommonKit/Sources/CommonKit/Assets/Localization/zh.lproj/Localizable.strings index 105a23fa6..888a6fc5b 100644 --- a/CommonKit/Sources/CommonKit/Assets/Localization/zh.lproj/Localizable.strings +++ b/CommonKit/Sources/CommonKit/Assets/Localization/zh.lproj/Localizable.strings @@ -452,7 +452,7 @@ "ChatScene.FreeTokensAlert.FreeTokens" = "🎁 免费代币"; /* Chat: 'Free Tokens' message */ -"ChatScene.FreeTokensAlert.Message" = "ADAMANT是一个独特的区块链信使,独立于政府、企业甚至其开发者。这是可能的,因为它是一个去中心化的网络,完全开源,由用户运行。这就是为什么每一个动作,包括发消息或保存新联系人,都要收取0.001 ADM.的网络费。要立即开始发消息,请获得免费的欢迎令牌。"; +"ChatScene.FreeTokensAlert.Message" = "ADAMANT 是一个去中心化的区块链消息应用。因此,每次操作,包括发送消息或保存新联系人,都会产生 0.001 ADM 的网络费用。"; /* Chat: warning message for opening email link without mail app configurated on device */ "ChatScene.Warning.NoMailApp" = "无法发送电子邮件。请检查邮件应用程序配置。"; @@ -709,6 +709,8 @@ /* NodeList: Inform that default nodes was loaded, if user deleted all nodes */ "NodeList.DefaultNodesLoaded" = "已加载默认节点列表"; +"chat.NewMessages" = "新消息"; + /* Storage: Save Encrypted */ "Storage.SaveEncrypted.Title" = "保持文件加密"; @@ -1344,3 +1346,17 @@ /* Chat unknown */ "Chat.unknown.title" = "未知"; + +/* No active nodes */ +"Chat.Alert.Title.NoActiveNodes" = "無活動 ADM 節點"; +"Chat.Alert.NoActiveNodes" = "无法请求新消息 - 没有活动的 ADM 区块链节点。由于您已停用部分节点,请考虑查看节点列表。"; + +/* Timestamp in the future */ +"Chat.Alert.Title.TimestampIsInTheFuture" = "檢查裝置時間"; +"Chat.Alert.TimestampIsInTheFuture" = "無法請求新訊息-沒有活動的 ADM 區塊鏈節點。由於您已停用部分節點,請考慮檢視節點清單。"; +"Chat.Alert.TimeSettings" = "時間設定"; + +/* Review Nodes List */ +"Chat.Alert.ReviewNodesList" = "查看 ADM 节点列表"; + +"Chat.Timestamp.InFuture.Error" = "由于您设备上的时间超前,网络节点拒绝了信息。\n请检查设备的时间或再次尝试发送信息。"; diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/Contents.json index 092b5c8bf..3629920fb 100644 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/Contents.json +++ b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/Contents.json @@ -5,15 +5,48 @@ "idiom" : "universal", "scale" : "1x" }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "clipboard_dark.png", + "idiom" : "universal", + "scale" : "1x" + }, { "filename" : "clipboard@2x.png", "idiom" : "universal", "scale" : "2x" }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "clipboard_dark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, { "filename" : "clipboard@3x.png", "idiom" : "universal", "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "clipboard_dark@3x.png", + "idiom" : "universal", + "scale" : "3x" } ], "info" : { diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/clipboard_dark.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/clipboard_dark.png new file mode 100644 index 000000000..d7c691e36 Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/clipboard_dark.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/clipboard_dark@2x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/clipboard_dark@2x.png new file mode 100644 index 000000000..c524c9222 Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/clipboard_dark@2x.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/clipboard_dark@3x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/clipboard_dark@3x.png new file mode 100644 index 000000000..179383e1f Binary files /dev/null and b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Buttons/clipboard.imageset/clipboard_dark@3x.png differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_notification.imageset/Contents.json deleted file mode 100644 index a03003e9e..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "adamant_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "adamant_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "adamant_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_wallet.imageset/Contents.json deleted file mode 100644 index 1c554fc76..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "adamant_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "adamant_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "adamant_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_wallet_row.imageset/Contents.json deleted file mode 100644 index adf828b19..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/adamant_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "adamant_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "adamant_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "adamant_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_notification.imageset/Contents.json deleted file mode 100644 index f0eb3d944..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "bitcoin_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "bitcoin_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "bitcoin_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_wallet.imageset/Contents.json deleted file mode 100644 index 7c5263564..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "bitcoin_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "bitcoin_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "bitcoin_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_wallet_row.imageset/Contents.json deleted file mode 100644 index bbe830344..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bitcoin_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "bitcoin_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "bitcoin_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "bitcoin_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_notification.imageset/Contents.json deleted file mode 100644 index 50fc88b92..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "bnb_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "bnb_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "bnb_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_wallet.imageset/Contents.json deleted file mode 100644 index 4a0ae2e1f..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "bnb_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "bnb_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "bnb_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_wallet_row.imageset/Contents.json deleted file mode 100644 index 9d366d968..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bnb_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "bnb_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "bnb_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "bnb_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_notification.imageset/Contents.json deleted file mode 100644 index c2ab366da..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "busd_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "busd_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "busd_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_wallet.imageset/Contents.json deleted file mode 100644 index b8544dbb4..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "busd_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "busd_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "busd_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_wallet_row.imageset/Contents.json deleted file mode 100644 index 942ef92f5..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/busd_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "busd_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "busd_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "busd_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_notification.imageset/Contents.json deleted file mode 100644 index 055207333..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "bzz_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "bzz_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "bzz_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_wallet.imageset/Contents.json deleted file mode 100644 index 7e0d5dc00..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "bzz_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "bzz_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "bzz_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_wallet_row.imageset/Contents.json deleted file mode 100644 index eb1c615ff..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/bzz_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "bzz_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "bzz_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "bzz_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_notification.imageset/Contents.json deleted file mode 100644 index 7b13ed8a0..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "dai_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "dai_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "dai_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_wallet.imageset/Contents.json deleted file mode 100644 index 2d53f3e7d..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "dai_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "dai_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "dai_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_wallet_row.imageset/Contents.json deleted file mode 100644 index 14d5f5050..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dai_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "dai_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "dai_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "dai_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_notification.imageset/Contents.json deleted file mode 100644 index 1c767684d..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "dash_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "dash_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "dash_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_wallet.imageset/Contents.json deleted file mode 100644 index 5a49cb37f..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "dash_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "dash_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "dash_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_wallet_row.imageset/Contents.json deleted file mode 100644 index 46d449329..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/dash_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "dash_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "dash_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "dash_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_notification.imageset/Contents.json deleted file mode 100644 index cfac2cdb6..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "doge_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "doge_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "doge_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_wallet.imageset/Contents.json deleted file mode 100644 index 2ce82ba02..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "doge_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "doge_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "doge_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_wallet_row.imageset/Contents.json deleted file mode 100644 index e0e289f81..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/doge_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "doge_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "doge_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "doge_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_notification.imageset/Contents.json deleted file mode 100644 index a6da0dc66..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "ens_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "ens_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "ens_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_wallet.imageset/Contents.json deleted file mode 100644 index bcf36f000..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "ens_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "ens_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "ens_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_wallet_row.imageset/Contents.json deleted file mode 100644 index deac559ae..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ens_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "ens_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "ens_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "ens_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_notification.imageset/Contents.json deleted file mode 100644 index 6e8bb51a7..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "ethereum_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "ethereum_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "ethereum_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_wallet.imageset/Contents.json deleted file mode 100644 index a06a475b2..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "ethereum_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "ethereum_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "ethereum_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_wallet_row.imageset/Contents.json deleted file mode 100644 index 804587fe5..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ethereum_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "ethereum_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "ethereum_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "ethereum_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_notification.imageset/Contents.json deleted file mode 100644 index e461e27fa..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "floki_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "floki_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "floki_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_wallet.imageset/Contents.json deleted file mode 100644 index 4aacf3204..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "floki_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "floki_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "floki_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_wallet_row.imageset/Contents.json deleted file mode 100644 index c0bc983aa..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/floki_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "floki_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "floki_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "floki_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_notification.imageset/Contents.json deleted file mode 100644 index be0f6fb5b..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "flux_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "flux_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "flux_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_wallet.imageset/Contents.json deleted file mode 100644 index 205f3d260..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "flux_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "flux_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "flux_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_wallet_row.imageset/Contents.json deleted file mode 100644 index 04453b061..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/flux_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "flux_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "flux_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "flux_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_notification.imageset/Contents.json deleted file mode 100644 index 7814a9537..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "gt_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "gt_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "gt_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_wallet.imageset/Contents.json deleted file mode 100644 index 3bbcc489e..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "gt_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "gt_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "gt_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_wallet_row.imageset/Contents.json deleted file mode 100644 index 35e3993f8..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/gt_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "gt_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "gt_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "gt_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_notification.imageset/Contents.json deleted file mode 100644 index cbed9380e..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "hot_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "hot_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "hot_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_wallet.imageset/Contents.json deleted file mode 100644 index e6bb29d21..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "hot_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "hot_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "hot_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_wallet_row.imageset/Contents.json deleted file mode 100644 index ceaabfc29..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/hot_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "hot_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "hot_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "hot_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_notification.imageset/Contents.json deleted file mode 100644 index e8e6964c3..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "inj_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "inj_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "inj_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_wallet.imageset/Contents.json deleted file mode 100644 index 2d0da33ef..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "inj_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "inj_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "inj_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_wallet_row.imageset/Contents.json deleted file mode 100644 index 39e2da4de..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/inj_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "inj_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "inj_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "inj_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_notification.imageset/Contents.json deleted file mode 100644 index ad8ca4a0a..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "klayr_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "klayr_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "klayr_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_wallet.imageset/Contents.json deleted file mode 100644 index d29b8f225..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "klayr_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "klayr_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "klayr_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_wallet_row.imageset/Contents.json deleted file mode 100644 index 7fcd1e574..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/klayr_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "klayr_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "klayr_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "klayr_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_notification.imageset/Contents.json deleted file mode 100644 index 9d4ead2dd..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "link_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "link_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "link_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_wallet.imageset/Contents.json deleted file mode 100644 index a9bad9f34..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "link_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "link_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "link_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_wallet_row.imageset/Contents.json deleted file mode 100644 index 3fd334a9b..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/link_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "link_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "link_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "link_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_notification.imageset/Contents.json deleted file mode 100644 index 019b3a437..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_notification.imageset/Contents.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "images" : [ - { - "filename" : "lisk_notification.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "lisk_notification_dark.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "lisk_notification@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "lisk_notification_dark@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "lisk_notification@3x.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "lisk_notification_dark@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_notification.imageset/lisk_notification.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_notification.imageset/lisk_notification.png deleted file mode 100644 index 157ca9011..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_notification.imageset/lisk_notification.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_notification.imageset/lisk_notification@2x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_notification.imageset/lisk_notification@2x.png deleted file mode 100644 index cf2528f8e..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_notification.imageset/lisk_notification@2x.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_notification.imageset/lisk_notification@3x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_notification.imageset/lisk_notification@3x.png deleted file mode 100644 index f079d9577..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_notification.imageset/lisk_notification@3x.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_notification.imageset/lisk_notification_dark.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_notification.imageset/lisk_notification_dark.png deleted file mode 100644 index 7dadcf1f7..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_notification.imageset/lisk_notification_dark.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_notification.imageset/lisk_notification_dark@2x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_notification.imageset/lisk_notification_dark@2x.png deleted file mode 100644 index 5a7a43db8..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_notification.imageset/lisk_notification_dark@2x.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_notification.imageset/lisk_notification_dark@3x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_notification.imageset/lisk_notification_dark@3x.png deleted file mode 100644 index 9e0171a9e..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_notification.imageset/lisk_notification_dark@3x.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet.imageset/Contents.json deleted file mode 100644 index d07ab051a..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet.imageset/Contents.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "images" : [ - { - "filename" : "lisk_wallet.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "lisk_wallet_dark.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "lisk_wallet@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "lisk_wallet_dark@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "lisk_wallet@3x.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "lisk_wallet_dark@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet.imageset/lisk_wallet.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet.imageset/lisk_wallet.png deleted file mode 100644 index 157ca9011..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet.imageset/lisk_wallet.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet.imageset/lisk_wallet@2x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet.imageset/lisk_wallet@2x.png deleted file mode 100644 index cf2528f8e..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet.imageset/lisk_wallet@2x.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet.imageset/lisk_wallet@3x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet.imageset/lisk_wallet@3x.png deleted file mode 100644 index f079d9577..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet.imageset/lisk_wallet@3x.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet.imageset/lisk_wallet_dark.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet.imageset/lisk_wallet_dark.png deleted file mode 100644 index 7dadcf1f7..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet.imageset/lisk_wallet_dark.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet.imageset/lisk_wallet_dark@2x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet.imageset/lisk_wallet_dark@2x.png deleted file mode 100644 index 5a7a43db8..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet.imageset/lisk_wallet_dark@2x.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet.imageset/lisk_wallet_dark@3x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet.imageset/lisk_wallet_dark@3x.png deleted file mode 100644 index 9e0171a9e..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet.imageset/lisk_wallet_dark@3x.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet_row.imageset/Contents.json deleted file mode 100644 index bec92f9aa..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "images" : [ - { - "filename" : "lisk_wallet_row.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "lisk_wallet_row_dark.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "lisk_wallet_row@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "lisk_wallet_row_dark@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "lisk_wallet_row@3x.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "lisk_wallet_row_dark@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet_row.imageset/lisk_wallet_row.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet_row.imageset/lisk_wallet_row.png deleted file mode 100644 index 157ca9011..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet_row.imageset/lisk_wallet_row.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet_row.imageset/lisk_wallet_row@2x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet_row.imageset/lisk_wallet_row@2x.png deleted file mode 100644 index cf2528f8e..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet_row.imageset/lisk_wallet_row@2x.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet_row.imageset/lisk_wallet_row@3x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet_row.imageset/lisk_wallet_row@3x.png deleted file mode 100644 index f079d9577..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet_row.imageset/lisk_wallet_row@3x.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet_row.imageset/lisk_wallet_row_dark.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet_row.imageset/lisk_wallet_row_dark.png deleted file mode 100644 index 7dadcf1f7..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet_row.imageset/lisk_wallet_row_dark.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet_row.imageset/lisk_wallet_row_dark@2x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet_row.imageset/lisk_wallet_row_dark@2x.png deleted file mode 100644 index 5a7a43db8..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet_row.imageset/lisk_wallet_row_dark@2x.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet_row.imageset/lisk_wallet_row_dark@3x.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet_row.imageset/lisk_wallet_row_dark@3x.png deleted file mode 100644 index 9e0171a9e..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/lisk_wallet_row.imageset/lisk_wallet_row_dark@3x.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_notification.imageset/Contents.json deleted file mode 100644 index 784d10db8..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "mana_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "mana_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "mana_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_wallet.imageset/Contents.json deleted file mode 100644 index 19f38df6f..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "mana_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "mana_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "mana_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_wallet_row.imageset/Contents.json deleted file mode 100644 index 8e696247f..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/mana_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "mana_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "mana_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "mana_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_notification.imageset/Contents.json deleted file mode 100644 index faa49c684..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "matic_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "matic_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "matic_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_wallet.imageset/Contents.json deleted file mode 100644 index bf6ab4ab7..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "matic_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "matic_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "matic_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_wallet_row.imageset/Contents.json deleted file mode 100644 index 371b760c7..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/matic_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "matic_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "matic_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "matic_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/Contents.json deleted file mode 100644 index 78e8130df..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "filename" : "no-token.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "no-token@2x_white.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "no-token@3x_white.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token.png deleted file mode 100644 index a3f02a85d..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token@2x_white.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token@2x_white.png deleted file mode 100644 index 23a0ca130..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token@2x_white.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token@3x_white.png b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token@3x_white.png deleted file mode 100644 index fffe1319c..000000000 Binary files a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/no-token.imageset/no-token@3x_white.png and /dev/null differ diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_notification.imageset/Contents.json deleted file mode 100644 index eda8bd743..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "paxg_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "paxg_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "paxg_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_wallet.imageset/Contents.json deleted file mode 100644 index b4d491826..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "paxg_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "paxg_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "paxg_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_wallet_row.imageset/Contents.json deleted file mode 100644 index 5b197084a..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/paxg_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "paxg_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "paxg_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "paxg_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_notification.imageset/Contents.json deleted file mode 100644 index 62c578252..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_notification.imageset/Contents.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "images" : [ - { - "filename" : "qnt_notification.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "qnt_notification_dark.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "qnt_notification@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "qnt_notification_dark@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "qnt_notification@3x.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "qnt_notification_dark@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet.imageset/Contents.json deleted file mode 100644 index c0a3220b8..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet.imageset/Contents.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "images" : [ - { - "filename" : "qnt_wallet.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "qnt_wallet_dark.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "qnt_wallet@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "qnt_wallet_dark@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "qnt_wallet@3x.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "qnt_wallet_dark@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet_row.imageset/Contents.json deleted file mode 100644 index d5c5ad166..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/qnt_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "images" : [ - { - "filename" : "qnt_wallet_row.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "qnt_wallet_row_dark.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "qnt_wallet_row@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "qnt_wallet_row_dark@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "qnt_wallet_row@3x.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "qnt_wallet_row_dark@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_notification.imageset/Contents.json deleted file mode 100644 index 982b3b0a9..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_notification.imageset/Contents.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "images" : [ - { - "filename" : "ren_notification.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "ren_notification_dark.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "ren_notification@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "ren_notification_dark@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "ren_notification@3x.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "ren_notification_dark@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet.imageset/Contents.json deleted file mode 100644 index 8bab5e53b..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet.imageset/Contents.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "images" : [ - { - "filename" : "ren_wallet.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "ren_wallet_dark.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "ren_wallet@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "ren_wallet_dark@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "ren_wallet@3x.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "ren_wallet_dark@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet_row.imageset/Contents.json deleted file mode 100644 index 6d98ae71a..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/ren_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "images" : [ - { - "filename" : "ren_wallet_row.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "ren_wallet_row_dark.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "ren_wallet_row@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "ren_wallet_row_dark@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "ren_wallet_row@3x.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "ren_wallet_row_dark@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_notification.imageset/Contents.json deleted file mode 100644 index af58be7f2..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_notification.imageset/Contents.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "images" : [ - { - "filename" : "skl_notification.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "skl_notification_dark.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "skl_notification@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "skl_notification_dark@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "skl_notification@3x.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "skl_notification_dark@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet.imageset/Contents.json deleted file mode 100644 index 0c437ba56..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet.imageset/Contents.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "images" : [ - { - "filename" : "skl_wallet.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "skl_wallet_dark.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "skl_wallet@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "skl_wallet_dark@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "skl_wallet@3x.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "skl_wallet_dark@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet_row.imageset/Contents.json deleted file mode 100644 index 745711ba4..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/skl_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "images" : [ - { - "filename" : "skl_wallet_row.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "skl_wallet_row_dark.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "skl_wallet_row@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "skl_wallet_row_dark@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "skl_wallet_row@3x.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "skl_wallet_row_dark@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_notification.imageset/Contents.json deleted file mode 100644 index a54a8e2dd..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "snt_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "snt_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "snt_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_wallet.imageset/Contents.json deleted file mode 100644 index fcd7db51b..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "snt_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "snt_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "snt_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_wallet_row.imageset/Contents.json deleted file mode 100644 index 383dd597f..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snt_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "snt_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "snt_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "snt_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_notification.imageset/Contents.json deleted file mode 100644 index 11d6cdbd6..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_notification.imageset/Contents.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "images" : [ - { - "filename" : "snx_notification.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "snx_notification_dark.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "snx_notification@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "snx_notification_dark@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "snx_notification@3x.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "snx_notification_dark@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet.imageset/Contents.json deleted file mode 100644 index 0195c26d4..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet.imageset/Contents.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "images" : [ - { - "filename" : "snx_wallet.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "snx_wallet_dark.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "snx_wallet@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "snx_wallet_dark@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "snx_wallet@3x.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "snx_wallet_dark@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet_row.imageset/Contents.json deleted file mode 100644 index b808cf2b2..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/snx_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "images" : [ - { - "filename" : "snx_wallet_row.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "snx_wallet_row_dark.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "snx_wallet_row@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "snx_wallet_row_dark@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "snx_wallet_row@3x.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "snx_wallet_row_dark@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_notification.imageset/Contents.json deleted file mode 100644 index 6c5b00d47..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "storj_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "storj_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "storj_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_wallet.imageset/Contents.json deleted file mode 100644 index f04d7e847..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "storj_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "storj_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "storj_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_wallet_row.imageset/Contents.json deleted file mode 100644 index 018c82fbf..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/storj_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "storj_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "storj_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "storj_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_notification.imageset/Contents.json deleted file mode 100644 index 6789596f3..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "tusd_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "tusd_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "tusd_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_wallet.imageset/Contents.json deleted file mode 100644 index b2b325d51..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "tusd_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "tusd_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "tusd_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_wallet_row.imageset/Contents.json deleted file mode 100644 index c6211ebec..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/tusd_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "tusd_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "tusd_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "tusd_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_notification.imageset/Contents.json deleted file mode 100644 index 387aa518c..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "uni_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "uni_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "uni_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_wallet.imageset/Contents.json deleted file mode 100644 index 25e2583d3..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "uni_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "uni_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "uni_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_wallet_row.imageset/Contents.json deleted file mode 100644 index c5e90f5fe..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/uni_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "uni_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "uni_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "uni_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_notification.imageset/Contents.json deleted file mode 100644 index ef741ddc7..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "usdc_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "usdc_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "usdc_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_wallet.imageset/Contents.json deleted file mode 100644 index 1450c2580..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "usdc_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "usdc_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "usdc_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_wallet_row.imageset/Contents.json deleted file mode 100644 index 289dfd4d2..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdc_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "usdc_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "usdc_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "usdc_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_notification.imageset/Contents.json deleted file mode 100644 index 14541c3e9..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "usdp_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "usdp_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "usdp_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_wallet.imageset/Contents.json deleted file mode 100644 index 2fd8e1a7c..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "usdp_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "usdp_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "usdp_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_wallet_row.imageset/Contents.json deleted file mode 100644 index d6c41da3a..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdp_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "usdp_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "usdp_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "usdp_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_notification.imageset/Contents.json deleted file mode 100644 index 98e7f9a36..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "usds_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "usds_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "usds_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_wallet.imageset/Contents.json deleted file mode 100644 index 366a4e29a..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "usds_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "usds_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "usds_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_wallet_row.imageset/Contents.json deleted file mode 100644 index ab22b10b9..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usds_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "usds_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "usds_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "usds_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_notification.imageset/Contents.json deleted file mode 100644 index 7a0cbb72c..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "usdt_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "usdt_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "usdt_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_wallet.imageset/Contents.json deleted file mode 100644 index 868eb922b..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "usdt_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "usdt_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "usdt_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_wallet_row.imageset/Contents.json deleted file mode 100644 index 47924ace2..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/usdt_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "usdt_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "usdt_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "usdt_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_notification.imageset/Contents.json deleted file mode 100644 index eac049afb..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_notification.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "verse_notification.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "verse_notification@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "verse_notification@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_wallet.imageset/Contents.json deleted file mode 100644 index 4dab739fb..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_wallet.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "verse_wallet.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "verse_wallet@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "verse_wallet@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_wallet_row.imageset/Contents.json deleted file mode 100644 index ddea69321..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/verse_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x", - "filename" : "verse_wallet_row.png" - }, - { - "idiom" : "universal", - "scale" : "2x", - "filename" : "verse_wallet_row@2x.png" - }, - { - "idiom" : "universal", - "scale" : "3x", - "filename" : "verse_wallet_row@3x.png" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_notification.imageset/Contents.json deleted file mode 100644 index 6ab38e942..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_notification.imageset/Contents.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "images" : [ - { - "filename" : "woo_notification.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "woo_notification_dark.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "woo_notification@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "woo_notification_dark@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "woo_notification@3x.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "woo_notification_dark@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet.imageset/Contents.json deleted file mode 100644 index c979d7340..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet.imageset/Contents.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "images" : [ - { - "filename" : "woo_wallet.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "woo_wallet_dark.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "woo_wallet@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "woo_wallet_dark@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "woo_wallet@3x.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "woo_wallet_dark@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet_row.imageset/Contents.json deleted file mode 100644 index 1808bc93f..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/woo_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "images" : [ - { - "filename" : "woo_wallet_row.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "woo_wallet_row_dark.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "woo_wallet_row@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "woo_wallet_row_dark@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "woo_wallet_row@3x.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "woo_wallet_row_dark@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_notification.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_notification.imageset/Contents.json deleted file mode 100644 index 9879e790f..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_notification.imageset/Contents.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "images" : [ - { - "filename" : "xcn_notification.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "xcn_notification_dark.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "xcn_notification@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "xcn_notification_dark@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "xcn_notification@3x.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "xcn_notification_dark@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet.imageset/Contents.json deleted file mode 100644 index c0ab32680..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet.imageset/Contents.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "images" : [ - { - "filename" : "xcn_wallet.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "xcn_wallet_dark.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "xcn_wallet@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "xcn_wallet_dark@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "xcn_wallet@3x.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "xcn_wallet_dark@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet_row.imageset/Contents.json b/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet_row.imageset/Contents.json deleted file mode 100644 index 913f6ae14..000000000 --- a/CommonKit/Sources/CommonKit/Assets/Shared.xcassets/Wallets/xcn_wallet_row.imageset/Contents.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "images" : [ - { - "filename" : "xcn_wallet_row.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "xcn_wallet_row_dark.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "xcn_wallet_row@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "xcn_wallet_row_dark@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "xcn_wallet_row@3x.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "xcn_wallet_row_dark@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/CommonKit/Sources/CommonKit/Core/Crypto.swift b/CommonKit/Sources/CommonKit/Core/Crypto.swift index 7ed8c8192..7df5ace7c 100644 --- a/CommonKit/Sources/CommonKit/Core/Crypto.swift +++ b/CommonKit/Sources/CommonKit/Core/Crypto.swift @@ -6,9 +6,9 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation import Clibsodium import CryptoSwift +import Foundation public typealias Bytes = [UInt8] @@ -23,59 +23,72 @@ public struct Sign: Sendable { public var SignBytes: Int { return Int(crypto_sign_bytes()) } public var PublicKeyBytes: Int { return Int(crypto_sign_publickeybytes()) } public var SecretKeyBytes: Int { return Int(crypto_sign_secretkeybytes()) } - + public func keypair(from seed: Bytes) -> (publicKey: Bytes, privateKey: Bytes)? { var publicKey = Bytes(count: PublicKeyBytes) var privateKey = Bytes(count: SecretKeyBytes) - - guard .SUCCESS == crypto_sign_seed_keypair( - &publicKey, - &privateKey, - seed - ).exitCode else { return nil } - + + guard + .SUCCESS + == crypto_sign_seed_keypair( + &publicKey, + &privateKey, + seed + ).exitCode + else { return nil } + return (publicKey: publicKey, privateKey: privateKey) } - + public func signature(message: Bytes, secretKey: Bytes) -> Bytes? { guard secretKey.count == SecretKeyBytes else { return nil } var signature = [UInt8](count: SignBytes) - - guard .SUCCESS == crypto_sign_detached( - &signature, - nil, - message, UInt64(message.count), - secretKey - ).exitCode else { return nil } - + + guard + .SUCCESS + == crypto_sign_detached( + &signature, + nil, + message, + UInt64(message.count), + secretKey + ).exitCode + else { return nil } + return signature } - + public init() {} } public struct ED2Curve: Sendable { public var KeyBytes: Int { return Int(crypto_scalarmult_curve25519_bytes()) } - + public func publicKey(_ key: Bytes) -> Bytes? { var publicKey = Bytes(count: KeyBytes) - - guard .SUCCESS == crypto_sign_ed25519_pk_to_curve25519( - &publicKey, - key - ).exitCode else { return nil } - + + guard + .SUCCESS + == crypto_sign_ed25519_pk_to_curve25519( + &publicKey, + key + ).exitCode + else { return nil } + return publicKey } - + public func privateKey(_ key: Bytes) -> Bytes? { var privateKey = Bytes(count: KeyBytes) - - guard .SUCCESS == crypto_sign_ed25519_sk_to_curve25519( - &privateKey, - key - ).exitCode else { return nil } - + + guard + .SUCCESS + == crypto_sign_ed25519_sk_to_curve25519( + &privateKey, + key + ).exitCode + else { return nil } + return privateKey } } @@ -85,7 +98,7 @@ public struct Box: NonceGenerator { public var NonceBytes: Int { return Int(crypto_box_noncebytes()) } public var PublicKeyBytes: Int { return Int(crypto_box_publickeybytes()) } public var SecretKeyBytes: Int { return Int(crypto_box_secretkeybytes()) } - + public func seal( message: Bytes, recipientPublicKey: Bytes, @@ -93,43 +106,50 @@ public struct Box: NonceGenerator { ) -> (authenticatedCipherText: Bytes, nonce: Bytes)? { guard recipientPublicKey.count == PublicKeyBytes, senderSecretKey.count == SecretKeyBytes - else { return nil } - + else { return nil } + var authenticatedCipherText = Bytes(count: message.count + MacBytes) let nonce = self.nonce() - - guard .SUCCESS == crypto_box_easy( - &authenticatedCipherText, - message, - CUnsignedLongLong(message.count), - nonce, - recipientPublicKey, - senderSecretKey - ).exitCode else { return nil } - + + guard + .SUCCESS + == crypto_box_easy( + &authenticatedCipherText, + message, + CUnsignedLongLong(message.count), + nonce, + recipientPublicKey, + senderSecretKey + ).exitCode + else { return nil } + return (authenticatedCipherText: authenticatedCipherText, nonce: nonce) } - + public func open(authenticatedCipherText: Bytes, senderPublicKey: Bytes, recipientSecretKey: Bytes, nonce: Bytes) -> Bytes? { guard nonce.count == NonceBytes, authenticatedCipherText.count >= MacBytes, senderPublicKey.count == PublicKeyBytes, recipientSecretKey.count == SecretKeyBytes - else { return nil } - + else { return nil } + var message = Bytes(count: authenticatedCipherText.count - MacBytes) - - guard .SUCCESS == crypto_box_open_easy( - &message, - authenticatedCipherText, UInt64(authenticatedCipherText.count), - nonce, - senderPublicKey, - recipientSecretKey - ).exitCode else { return nil } - + + guard + .SUCCESS + == crypto_box_open_easy( + &message, + authenticatedCipherText, + UInt64(authenticatedCipherText.count), + nonce, + senderPublicKey, + recipientSecretKey + ).exitCode + else { return nil } + return message } - + public init() {} } @@ -145,28 +165,36 @@ public struct SecretBox: NonceGenerator { guard secretKey.count == KeyBytes else { return nil } var authenticatedCipherText = Bytes(count: message.count + MacBytes) let nonce = self.nonce() - - guard .SUCCESS == crypto_secretbox_easy( - &authenticatedCipherText, - message, UInt64(message.count), - nonce, - secretKey - ).exitCode else { return nil } - + + guard + .SUCCESS + == crypto_secretbox_easy( + &authenticatedCipherText, + message, + UInt64(message.count), + nonce, + secretKey + ).exitCode + else { return nil } + return (authenticatedCipherText: authenticatedCipherText, nonce: nonce) } - + public func open(authenticatedCipherText: Bytes, secretKey: Bytes, nonce: Bytes) -> Bytes? { guard authenticatedCipherText.count >= MacBytes else { return nil } var message = Bytes(count: authenticatedCipherText.count - MacBytes) - - guard .SUCCESS == crypto_secretbox_open_easy( - &message, - authenticatedCipherText, UInt64(authenticatedCipherText.count), - nonce, - secretKey - ).exitCode else { return nil } - + + guard + .SUCCESS + == crypto_secretbox_open_easy( + &message, + authenticatedCipherText, + UInt64(authenticatedCipherText.count), + nonce, + secretKey + ).exitCode + else { return nil } + return message } } @@ -175,35 +203,35 @@ public protocol NonceGenerator: Sendable { var NonceBytes: Int { get } } -public extension NonceGenerator { +extension NonceGenerator { /// Generates a random nonce. - func nonce() -> Bytes { + public func nonce() -> Bytes { var nonce = Bytes(count: NonceBytes) randombytes_buf(&nonce, NonceBytes) return nonce } } -public extension Array where Element == UInt8 { - init (count bytes: Int) { +extension Array where Element == UInt8 { + public init(count bytes: Int) { self.init(repeating: 0, count: bytes) } - - var utf8String: String? { + + public var utf8String: String? { return String(data: Data(self), encoding: .utf8) } - - func toData() -> Data { + + public func toData() -> Data { return Data(self) } } -public extension ArraySlice where Element == UInt8 { - var bytes: Bytes { return Bytes(self) } +extension ArraySlice where Element == UInt8 { + public var bytes: Bytes { return Bytes(self) } } -public extension Sequence where Self.Element == UInt8 { - func hexString() -> String { +extension Sequence where Self.Element == UInt8 { + public func hexString() -> String { return map { String(format: "%02hhx", $0) }.joined() } } @@ -211,15 +239,15 @@ public extension Sequence where Self.Element == UInt8 { private enum ExitCode { case SUCCESS case FAILURE - + init(from int: Int32) { switch int { - case 0: self = .SUCCESS + case 0: self = .SUCCESS default: self = .FAILURE } } } -private extension Int32 { - var exitCode: ExitCode { return ExitCode(from: self) } +extension Int32 { + fileprivate var exitCode: ExitCode { return ExitCode(from: self) } } diff --git a/CommonKit/Sources/CommonKit/Core/Mnemonic.swift b/CommonKit/Sources/CommonKit/Core/Mnemonic.swift index 8bb189040..8d5229ee8 100644 --- a/CommonKit/Sources/CommonKit/Core/Mnemonic.swift +++ b/CommonKit/Sources/CommonKit/Core/Mnemonic.swift @@ -6,27 +6,29 @@ // Copyright © 2019 Adamant. All rights reserved. // -import Foundation import CryptoSwift +import Foundation public enum Mnemonic { // MARK: - Passphrase seeds - + public static func seed(passphrase: String, salt: String = "mnemonic") -> [UInt8]? { let password = passphrase.decomposedStringWithCompatibilityMapping let salt = salt.decomposedStringWithCompatibilityMapping - - if let seed = try? PKCS5.PBKDF2(password: password.bytes, salt: salt.bytes, iterations: 2048, keyLength: 64, variant: HMAC.Variant.sha2(.sha512)).calculate() { + + if let seed = try? PKCS5.PBKDF2(password: password.bytes, salt: salt.bytes, iterations: 2048, keyLength: 64, variant: HMAC.Variant.sha2(.sha512)) + .calculate() + { return seed } else { return nil } } - + public static func seed(mnemonic m: [String], passphrase: String = "") -> [UInt8]? { let mnemonic = m.joined(separator: " ") let salt = ("mnemonic" + passphrase) - + return seed(passphrase: mnemonic, salt: salt) } } diff --git a/CommonKit/Sources/CommonKit/Core/NativeAdamantCore.swift b/CommonKit/Sources/CommonKit/Core/NativeAdamantCore.swift index 32e93519c..1fc1b1816 100644 --- a/CommonKit/Sources/CommonKit/Core/NativeAdamantCore.swift +++ b/CommonKit/Sources/CommonKit/Core/NativeAdamantCore.swift @@ -6,8 +6,8 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation import CryptoSwift +import Foundation /* * Native Adamanat Core @@ -16,69 +16,69 @@ import CryptoSwift public final class NativeAdamantCore: AdamantCore { // MARK: - Messages - + public func encodeMessage(_ message: String, recipientPublicKey publicKey: String, privateKey privateKeyHex: String) -> (message: String, nonce: String)? { let message = message.bytes let recipientKey = publicKey.hexBytes() let privateKey = privateKeyHex.hexBytes() - + guard let publicKey = Crypto.ed2Curve.publicKey(recipientKey) else { print("FAIL to create ed2curve publick key from SHA256") return nil } - + guard let secretKey = Crypto.ed2Curve.privateKey(privateKey) else { print("FAIL to create ed2curve secret key from SHA256") return nil } - + guard let encrypted = Crypto.box.seal(message: message, recipientPublicKey: publicKey, senderSecretKey: secretKey) else { print("FAIL to encrypt") return nil } - + let encryptedMessage = encrypted.authenticatedCipherText.hexString() let nonce = encrypted.nonce.hexString() - + return (message: encryptedMessage, nonce: nonce) } - + public func decodeMessage(rawMessage: String, rawNonce: String, senderPublicKey senderKeyHex: String, privateKey privateKeyHex: String) -> String? { let message = rawMessage.hexBytes() let nonce = rawNonce.hexBytes() let senderKey = senderKeyHex.hexBytes() let privateKey = privateKeyHex.hexBytes() - + guard let publicKey = Crypto.ed2Curve.publicKey(senderKey) else { print("FAIL to create ed2curve publick key from SHA256") return nil } - + guard let secretKey = Crypto.ed2Curve.privateKey(privateKey) else { print("FAIL to create ed2curve secret key from SHA256") return nil } - + guard let decrepted = Crypto.box.open(authenticatedCipherText: message, senderPublicKey: publicKey, recipientSecretKey: secretKey, nonce: nonce) else { print("FAIL to decrypt") return nil } - + return decrepted.utf8String } - + public func sign(transaction: SignableTransaction, senderId: String, keypair: Keypair) -> String? { let privateKey = keypair.privateKey.hexBytes() let hash = transaction.bytes.sha256() - + guard let signature = Crypto.sign.signature(message: hash, secretKey: privateKey) else { print("FAIL to sign of transaction") return nil } - + return signature.hexString() } - + public func encodeData( _ data: Data, recipientPublicKey publicKey: String, @@ -87,28 +87,28 @@ public final class NativeAdamantCore: AdamantCore { let message = data.bytes let recipientKey = publicKey.hexBytes() let privateKey = privateKeyHex.hexBytes() - + guard let publicKey = Crypto.ed2Curve.publicKey(recipientKey) else { print("FAIL to create ed2curve publick key from SHA256") return nil } - + guard let secretKey = Crypto.ed2Curve.privateKey(privateKey) else { print("FAIL to create ed2curve secret key from SHA256") return nil } - + guard let encrypted = Crypto.box.seal(message: message, recipientPublicKey: publicKey, senderSecretKey: secretKey) else { print("FAIL to encrypt") return nil } - + let encryptedData = encrypted.authenticatedCipherText.toData() let nonce = encrypted.nonce.hexString() - + return (data: encryptedData, nonce: nonce) } - + public func decodeData( _ data: Data, rawNonce: String, @@ -119,205 +119,221 @@ public final class NativeAdamantCore: AdamantCore { let nonce = rawNonce.hexBytes() let senderKey = senderKeyHex.hexBytes() let privateKey = privateKeyHex.hexBytes() - + guard let publicKey = Crypto.ed2Curve.publicKey(senderKey) else { print("FAIL to create ed2curve publick key from SHA256") return nil } - + guard let secretKey = Crypto.ed2Curve.privateKey(privateKey) else { print("FAIL to create ed2curve secret key from SHA256") return nil } - + guard let decrepted = Crypto.box.open(authenticatedCipherText: message, senderPublicKey: publicKey, recipientSecretKey: secretKey, nonce: nonce) else { print("FAIL to decrypt") return nil } - + return decrepted.toData() } // MARK: - Values - + public func encodeValue(_ value: [String: Any], privateKey privateKeyHex: String) -> (message: String, nonce: String)? { let data = ["payload": value] - - let padded: String = String.random(length: Int(arc4random_uniform(10)), alphabet: "abcdefghijklmnopqrstuvwxyz") + AdamantUtilities.JSONStringify(value: data as AnyObject) + String.random(length: Int(arc4random_uniform(10)), alphabet: "abcdefghijklmnopqrstuvwxyz") - + + let padded: String = + String.random(length: Int(arc4random_uniform(10)), alphabet: "abcdefghijklmnopqrstuvwxyz") + + AdamantUtilities.JSONStringify(value: data as AnyObject) + + String.random(length: Int(arc4random_uniform(10)), alphabet: "abcdefghijklmnopqrstuvwxyz") + let message = padded.bytes let privateKey = privateKeyHex.hexBytes() let hash = privateKey.sha256() - + guard let secretKey = Crypto.ed2Curve.privateKey(hash) else { print("FAIL to create ed2curve secret key from SHA256") return nil } - + guard let encrypted = Crypto.secretBox.seal(message: message, secretKey: secretKey) else { print("FAIL to encrypt") return nil } - + let encryptedMessage = encrypted.authenticatedCipherText.hexString() let nonce = encrypted.nonce.hexString() - + return (message: encryptedMessage, nonce: nonce) } - + public func decodeValue(rawMessage: String, rawNonce: String, privateKey privateKeyHex: String) -> String? { let message = rawMessage.hexBytes() let nonce = rawNonce.hexBytes() let privateKey = privateKeyHex.hexBytes() let hash = privateKey.sha256() - + guard let secretKey = Crypto.ed2Curve.privateKey(hash) else { print("FAIL to create ed2curve secret key from SHA256") return nil } - + guard let decrepted = Crypto.secretBox.open(authenticatedCipherText: message, secretKey: secretKey, nonce: nonce) else { print("FAIL to decrypt") return nil } - + return decrepted.utf8String } - + // MARK: - Passphrases - - public func createKeypairFor(passphrase: String) -> Keypair? { - guard let hash = createRawHashFor(passphrase: passphrase) else { + + public func createKeypairFor(passphrase: String, password: String) -> Keypair? { + guard let hash = createSeedFor(passphrase: passphrase, password: password) else { print("Unable create hash from passphrase") return nil } - + guard let keypair = Crypto.sign.keypair(from: hash) else { print("Unable create Keypair from seed") return nil } - + return Keypair(publicKey: keypair.publicKey.hexString(), privateKey: keypair.privateKey.hexString()) } - - public func createHashFor(passphrase: String) -> String? { - guard let hash = createRawHashFor(passphrase: passphrase) else { - print("Unable create hash from passphrase") - return nil - } - - return hash.hexString() - } - - private func createRawHashFor(passphrase: String) -> [UInt8]? { - guard let seed = Mnemonic.seed(passphrase: passphrase) else { + + public func createSeedFor(passphrase: String, password: String) -> [UInt8]? { + guard let seed = Mnemonic.seed(passphrase: passphrase, salt: "mnemonic\(password)") else { print("FAIL to create Seed from passphrase bytes") return nil } - + return seed.sha256() } - + public init() {} } // MARK: - String -public extension String { - func hexBytes() -> [UInt8] { - return (0.. [UInt8] { + let utf8CString = self.utf8CString + let length = utf8CString.count - 1 // utf8CString includes a null terminator + + var bytes = [UInt8]() + bytes.reserveCapacity(length / 2) + + for i in stride(from: 0, to: length, by: 2) { + if let highValue = hexCharToUInt8(utf8CString[i]), + let lowValue = hexCharToUInt8(utf8CString[i + 1]) + { + bytes.append((highValue << 4) | lowValue) + } else { + bytes.append(0) + } } + + return bytes } - - static func random(length: Int = 32, alphabet: String = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") -> String { + + public static func random(length: Int = 32, alphabet: String = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") -> String { let upperBound = UInt32(alphabet.count) - return String((0.. Character in - let index = alphabet.index(alphabet.startIndex, offsetBy: Int(arc4random_uniform(upperBound))) - return alphabet[index] - }) + return String( + (0.. Character in + let index = alphabet.index(alphabet.startIndex, offsetBy: Int(arc4random_uniform(upperBound))) + return alphabet[index] + } + ) + } +} + +extension String { + fileprivate func hexCharToUInt8(_ char: CChar) -> UInt8? { + switch char { + case 48...57: // '0'-'9' + return UInt8(char - 48) + case 65...70: // 'A'-'F' + return UInt8(char - 55) + case 97...102: // 'a'-'f' + return UInt8(char - 87) + default: + return nil + } } } // MARK: - Bytes -private extension SignableTransaction { - - var bytes: [UInt8] { +extension SignableTransaction { + + fileprivate var bytes: [UInt8] { return - typeBytes + - timestampBytes + - senderPublicKeyBytes + - requesterPublicKeyBytes + - recipientIdBytes + - amountBytes + - assetBytes + - signatureBytes + - signSignatureBytes + typeBytes + timestampBytes + senderPublicKeyBytes + requesterPublicKeyBytes + recipientIdBytes + amountBytes + assetBytes + signatureBytes + + signSignatureBytes } - - var typeBytes: [UInt8] { + + fileprivate var typeBytes: [UInt8] { return [UInt8(type.rawValue)] } - - var timestampBytes: [UInt8] { + + fileprivate var timestampBytes: [UInt8] { return ByteBackpacker.pack(UInt32(timestamp), byteOrder: .littleEndian) } - - var senderPublicKeyBytes: [UInt8] { + + fileprivate var senderPublicKeyBytes: [UInt8] { return senderPublicKey.hexBytes() } - - var requesterPublicKeyBytes: [UInt8] { + + fileprivate var requesterPublicKeyBytes: [UInt8] { return requesterPublicKey?.hexBytes() ?? [] } - - var recipientIdBytes: [UInt8] { + + fileprivate var recipientIdBytes: [UInt8] { guard let value = recipientId?.replacingOccurrences(of: "U", with: ""), - let number = UInt64(value) else { return Bytes(count: 8) } + let number = UInt64(value) + else { return Bytes(count: 8) } return ByteBackpacker.pack(number, byteOrder: .bigEndian) } - - var amountBytes: [UInt8] { + + fileprivate var amountBytes: [UInt8] { let value = (self.amount.shiftedToAdamant() as NSDecimalNumber).uint64Value let bytes = ByteBackpacker.pack(value, byteOrder: .littleEndian) return bytes } - - var signatureBytes: [UInt8] { + + fileprivate var signatureBytes: [UInt8] { return [] } - - var signSignatureBytes: [UInt8] { + + fileprivate var signSignatureBytes: [UInt8] { return [] } - - var assetBytes: [UInt8] { + + fileprivate var assetBytes: [UInt8] { switch type { case .chatMessage: guard let msg = asset.chat?.message, let own = asset.chat?.ownMessage, let type = asset.chat?.type else { return [] } - + return msg.hexBytes() + own.hexBytes() + ByteBackpacker.pack(UInt32(type.rawValue), byteOrder: .littleEndian) - + case .state: guard let key = asset.state?.key, let value = asset.state?.value, let type = asset.state?.type else { return [] } - + return value.bytes + key.bytes + ByteBackpacker.pack(UInt32(type.rawValue), byteOrder: .littleEndian) - + case .vote: guard let votes = asset.votes?.votes - else { return [] } - + else { return [] } + var bytes = [UInt8]() for vote in votes { bytes += vote.bytes } - + return bytes - + default: return [] } diff --git a/CommonKit/Sources/CommonKit/Core/SecuredStore.swift b/CommonKit/Sources/CommonKit/Core/SecuredStore.swift index 91cd2608a..737f4da75 100644 --- a/CommonKit/Sources/CommonKit/Core/SecuredStore.swift +++ b/CommonKit/Sources/CommonKit/Core/SecuredStore.swift @@ -1,5 +1,5 @@ // -// SecuredStore.swift +// SecureStore.swift // Adamant // // Created by Anokhov Pavel on 04.03.2018. @@ -13,15 +13,15 @@ public enum StoreKey {} // MARK: - Notifications -public extension Notification.Name { - enum SecuredStore { +extension Notification.Name { + public enum SecureStore { /// Raised when store is purged - public static let securedStorePurged = Notification.Name("adamant.SecuredStore.purged") + public static let SecureStorePurged = Notification.Name("adamant.SecureStore.purged") } } -public extension StoreKey { - enum notificationsService { +extension StoreKey { + public enum notificationsService { public static let notificationsMode = "notifications.mode" public static let customBadgeNumber = "notifications.number" public static let notificationsSound = "notifications.sound" @@ -30,45 +30,46 @@ public extension StoreKey { public static let inAppVibrate = "notifications.inAppVibrate" public static let inAppToasts = "notifications.inAppToasts" } - - enum visibleWallets { + + public enum visibleWallets { public static let invisibleWallets = "invisible.wallets" public static let indexWallets = "index.wallets" public static let indexWalletsWithInvisible = "index.wallets.include.ivisible" public static let useCustomIndexes = "visible.wallets.useCustomIndexes" public static let useCustomVisibility = "visible.wallets.useCustomVisibility" } - - enum increaseFee { + + public enum increaseFee { public static let increaseFee = "increaseFee" } - - enum crashlytic { + + public enum crashlytic { public static let crashlyticEnabled = "crashlyticEnabled" } - - enum emojis { + + public enum emojis { public static let emojis = "emojis" } - - enum partnerQR { + + public enum partnerQR { public static let includeNameEnabled = "includeNameEnabled" public static let includeURLEnabled = "includeURLEnabled" } - - enum language { + + public enum language { public static let language = "language" public static let languageLocale = "language.locale" } - - enum storage { + + public enum storage { public static let autoDownloadPreview = "autoDownloadPreviewEnabled" public static let autoDownloadFullMedia = "autoDownloadFullMediaEnabled" public static let saveFileEncrypted = "saveFileEncrypted" } } -public protocol SecuredStore: AnyObject, Sendable { +// sourcery: AutoMockable +public protocol SecureStore: AnyObject, Sendable { func get(_ key: String) -> T? func set(_ value: T, for key: String) diff --git a/CommonKit/Sources/CommonKit/Core/UserDefaultsKeys.swift b/CommonKit/Sources/CommonKit/Core/UserDefaultsKeys.swift new file mode 100644 index 000000000..b99edceaf --- /dev/null +++ b/CommonKit/Sources/CommonKit/Core/UserDefaultsKeys.swift @@ -0,0 +1,10 @@ +// +// UserDefaultsKeys.swift +// CommonKit +// +// Created by Sergei Veretennikov on 22.03.2025. +// + +public enum UserDefaultsKey: String { + case needsToShowNoActiveNodesAlert +} diff --git a/CommonKit/Sources/CommonKit/Core/UserDefaultsWrapper.swift b/CommonKit/Sources/CommonKit/Core/UserDefaultsWrapper.swift new file mode 100644 index 000000000..ab6383f6e --- /dev/null +++ b/CommonKit/Sources/CommonKit/Core/UserDefaultsWrapper.swift @@ -0,0 +1,31 @@ +// +// UserDefaultsWrapper.swift +// CommonKit +// +// Created by Sergei Veretennikov on 22.03.2025. +// + +import Foundation + +@propertyWrapper +public struct UserDefaultsStorage { + private let defaults = UserDefaults.standard + private let key: String + + public var wrappedValue: T? { + get { + defaults.object(forKey: key) as? T + } + set { + if newValue == nil { + defaults.removeObject(forKey: key) + } else { + defaults.set(newValue, forKey: key) + } + } + } + + public init(_ key: UserDefaultsKey) { + self.key = key.rawValue + } +} diff --git a/CommonKit/Sources/CommonKit/ExtensionsTools/ExtensionsApi.swift b/CommonKit/Sources/CommonKit/ExtensionsTools/ExtensionsApi.swift index 8b15a82ae..656127aa4 100644 --- a/CommonKit/Sources/CommonKit/ExtensionsTools/ExtensionsApi.swift +++ b/CommonKit/Sources/CommonKit/ExtensionsTools/ExtensionsApi.swift @@ -12,34 +12,34 @@ public final class ExtensionsApi: Sendable { // MARK: Properties private let addressBookKey = "contact_list" private let apiService: AdamantApiServiceProtocol - + // MARK: Cotr public init(apiService: AdamantApiServiceProtocol) { self.apiService = apiService } - + // MARK: - API - + // MARK: Transactions public func getTransaction(by id: UInt64) -> Transaction? { Task.sync { [apiService] in try? await apiService.getTransaction(id: id, withAsset: true).get() } } - + // MARK: Address book - + public func getAddressBook( for address: String, core: NativeAdamantCore, keypair: Keypair - ) -> [String:ContactDescription]? { + ) -> [String: ContactDescription]? { let addressBookString = Task.sync { [apiService, addressBookKey] in try? await apiService.get(key: addressBookKey, sender: address).get() } - + // Working with transaction - + guard let object = addressBookString?.toDictionary(), let message = object["message"] as? String, @@ -47,26 +47,28 @@ public final class ExtensionsApi: Sendable { else { return nil } - + // Decoding guard let decodedMessage = core.decodeValue(rawMessage: message, rawNonce: nonce, privateKey: keypair.privateKey), let rawJson = decodedMessage.matches(for: "\\{.*\\}").first, - let contacts = rawJson.toDictionary()?["payload"] as? [String:Any] else { - return nil + let contacts = rawJson.toDictionary()?["payload"] as? [String: Any] + else { + return nil } - - var result = [String:ContactDescription]() + + var result = [String: ContactDescription]() let decoder = JSONDecoder() - + for (key, value) in contacts { guard let data = try? JSONSerialization.data(withJSONObject: value, options: []), - let description = try? decoder.decode(ContactDescription.self, from: data) else { + let description = try? decoder.decode(ContactDescription.self, from: data) + else { continue } - + result[key] = description } - + if result.count > 0 { return result } else { diff --git a/CommonKit/Sources/CommonKit/ExtensionsTools/ExtensionsApiFactory.swift b/CommonKit/Sources/CommonKit/ExtensionsTools/ExtensionsApiFactory.swift index 0eb644d1e..f24740f7b 100644 --- a/CommonKit/Sources/CommonKit/ExtensionsTools/ExtensionsApiFactory.swift +++ b/CommonKit/Sources/CommonKit/ExtensionsTools/ExtensionsApiFactory.swift @@ -9,37 +9,39 @@ import Combine public struct ExtensionsApiFactory { public let core: AdamantCore - public let securedStore: SecuredStore - - public init(core: AdamantCore, securedStore: SecuredStore) { + public let SecureStore: SecureStore + + public init(core: AdamantCore, SecureStore: SecureStore) { self.core = core - self.securedStore = securedStore + self.SecureStore = SecureStore } - + public func make() -> ExtensionsApi { - .init(apiService: AdamantApiService( - healthCheckWrapper: .init( - service: AdamantApiCore(apiCore: APICore()), - nodesStorage: NodesStorage( - securedStore: securedStore, - nodesMergingService: NodesMergingService(), - defaultNodes: { _ in .init() } - ), - nodesAdditionalParamsStorage: NodesAdditionalParamsStorage( - securedStore: securedStore - ), - isActive: false, - params: .init( - group: .adm, - name: "ADM", - normalUpdateInterval: .infinity, - crucialUpdateInterval: .infinity, - minNodeVersion: nil, - nodeHeightEpsilon: .zero + .init( + apiService: AdamantApiService( + healthCheckWrapper: .init( + service: AdamantApiCore(apiCore: APICore()), + nodesStorage: NodesStorage( + SecureStore: SecureStore, + nodesMergingService: NodesMergingService(), + defaultNodes: { _ in .init() } + ), + nodesAdditionalParamsStorage: NodesAdditionalParamsStorage( + SecureStore: SecureStore + ), + isActive: false, + params: .init( + group: .adm, + name: "ADM", + normalUpdateInterval: .infinity, + crucialUpdateInterval: .infinity, + minNodeVersion: nil, + nodeHeightEpsilon: .zero + ), + connection: Just(true).eraseToAnyPublisher() ), - connection: Just(true).eraseToAnyPublisher() - ), - adamantCore: core - )) + adamantCore: core + ) + ) } } diff --git a/CommonKit/Sources/CommonKit/Helpers/ADM+JsonDecode.swift b/CommonKit/Sources/CommonKit/Helpers/ADM+JsonDecode.swift index cb85f5854..cc215349f 100644 --- a/CommonKit/Sources/CommonKit/Helpers/ADM+JsonDecode.swift +++ b/CommonKit/Sources/CommonKit/Helpers/ADM+JsonDecode.swift @@ -23,8 +23,8 @@ public struct JSONCodingKeys: CodingKey { } } -public extension KeyedDecodingContainer { - func decode(forKey key: K) throws -> Data { +extension KeyedDecodingContainer { + public func decode(forKey key: K) throws -> Data { if let stringValue = try? decode(String.self, forKey: key) { return Data(stringValue.utf8) } else if (try? decode(Dictionary.self, forKey: key)) != nil { @@ -32,35 +32,35 @@ public extension KeyedDecodingContainer { let dictionary = try container.decode([String: Any].self) return try JSONSerialization.data(withJSONObject: dictionary, options: []) } - + return Data() } - - func decode(_ type: Dictionary.Type, forKey key: K) throws -> [String: Any] { + + public func decode(_ type: [String: Any].Type, forKey key: K) throws -> [String: Any] { let container = try self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key) return try container.decode(type) } - func decodeIfPresent(_ type: Dictionary.Type, forKey key: K) throws -> [String: Any]? { + public func decodeIfPresent(_ type: [String: Any].Type, forKey key: K) throws -> [String: Any]? { guard contains(key) else { return nil } return try decode(type, forKey: key) } - func decode(_ type: Array.Type, forKey key: K) throws -> [Any] { + public func decode(_ type: [Any].Type, forKey key: K) throws -> [Any] { var container = try self.nestedUnkeyedContainer(forKey: key) return try container.decode(type) } - func decodeIfPresent(_ type: Array.Type, forKey key: K) throws -> [Any]? { + public func decodeIfPresent(_ type: [Any].Type, forKey key: K) throws -> [Any]? { guard contains(key) else { return nil } return try decode(type, forKey: key) } - func decode(_ type: Dictionary.Type) throws -> [String: Any] { + public func decode(_ type: [String: Any].Type) throws -> [String: Any] { var dictionary: [String: Any] = [:] for key in allKeys { @@ -82,8 +82,8 @@ public extension KeyedDecodingContainer { } } -public extension UnkeyedDecodingContainer { - mutating func decode(_ type: Array.Type) throws -> [Any] { +extension UnkeyedDecodingContainer { + public mutating func decode(_ type: [Any].Type) throws -> [Any] { var array: [Any] = [] while isAtEnd == false { if let value = try? decode(Bool.self) { @@ -101,7 +101,7 @@ public extension UnkeyedDecodingContainer { return array } - mutating func decode(_ type: Dictionary.Type) throws -> [String: Any] { + public mutating func decode(_ type: [String: Any].Type) throws -> [String: Any] { let nestedContainer = try self.nestedContainer(keyedBy: JSONCodingKeys.self) return try nestedContainer.decode(type) } diff --git a/CommonKit/Sources/CommonKit/Helpers/AdamantBalanceFormat.swift b/CommonKit/Sources/CommonKit/Helpers/AdamantBalanceFormat.swift index 4ed3ed052..ee9ac355f 100644 --- a/CommonKit/Sources/CommonKit/Helpers/AdamantBalanceFormat.swift +++ b/CommonKit/Sources/CommonKit/Helpers/AdamantBalanceFormat.swift @@ -17,26 +17,26 @@ public enum AdamantBalanceFormat { // MARK: Styles /// 8 digits after the decimal point case full - + /// 4 digits after the decimal point case compact - + /// 2 digits after the decimal point case short - + /// N digits after the decimal point case custom(Int) - + // MARK: Formatters - + public static func currencyFormatter(for format: AdamantBalanceFormat, currencySymbol symbol: String?) -> NumberFormatter { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.roundingMode = .floor formatter.minimumFractionDigits = 0 - + var positiveFormat: String - + switch format { case .full: positiveFormat = "#.########" case .compact: @@ -45,26 +45,26 @@ public enum AdamantBalanceFormat { case .short: positiveFormat = "#.##" case .custom(let digits): positiveFormat = "#." - for _ in 1...digits { + for _ in 1...digits { positiveFormat.append("#") } } - + if let symbol = symbol { formatter.positiveFormat = "\(positiveFormat) \(symbol)" } else { formatter.positiveFormat = positiveFormat } - + return formatter } - + public static let currencyFormatterFull = currencyFormatter(for: .full, currencySymbol: nil) public static let currencyFormatterCompact = currencyFormatter(for: .compact, currencySymbol: nil) public static let currencyFormatterShort = currencyFormatter(for: .short, currencySymbol: nil) - + // MARK: Methods - + public var defaultFormatter: NumberFormatter { switch self { case .full: return AdamantBalanceFormat.currencyFormatter(for: .full, currencySymbol: nil) @@ -73,7 +73,7 @@ public enum AdamantBalanceFormat { case .custom(let decimals): return AdamantBalanceFormat.currencyFormatter(for: .custom(decimals), currencySymbol: nil) } } - + public func format(_ value: Decimal, withCurrencySymbol symbol: String? = nil) -> String { if let symbol = symbol { return "\(defaultFormatter.string(from: value)!) \(symbol)" @@ -81,7 +81,7 @@ public enum AdamantBalanceFormat { return defaultFormatter.string(from: value)! } } - + public func format(_ value: Double, withCurrencySymbol symbol: String? = nil) -> String { if let symbol = symbol { return "\(defaultFormatter.string(from: NSNumber(floatLiteral: value))!) \(symbol)" @@ -89,9 +89,9 @@ public enum AdamantBalanceFormat { return defaultFormatter.string(from: NSNumber(floatLiteral: value))! } } - + // MARK: Other formatters - + public static let rawNumberDotFormatter: NumberFormatter = { let f = NumberFormatter() f.numberStyle = .decimal @@ -99,10 +99,10 @@ public enum AdamantBalanceFormat { f.decimalSeparator = "." f.usesGroupingSeparator = false f.minimumFractionDigits = 0 - f.maximumFractionDigits = 12 // 18 is too low, 0.007 for example will serialize as 0.007000000000000001 + f.maximumFractionDigits = 12 // 18 is too low, 0.007 for example will serialize as 0.007000000000000001 return f }() - + public static let rawNumberCommaFormatter: NumberFormatter = { let f = NumberFormatter() f.numberStyle = .decimal @@ -110,13 +110,13 @@ public enum AdamantBalanceFormat { f.decimalSeparator = "," f.usesGroupingSeparator = false f.minimumFractionDigits = 0 - f.maximumFractionDigits = 12 // 18 is too low, 0.007 for example will serialize as 0.007000000000000001 + f.maximumFractionDigits = 12 // 18 is too low, 0.007 for example will serialize as 0.007000000000000001 return f }() - + public static func deserializeBalance(from string: String) -> Decimal? { // NumberFormatter.number(from: string).decimalValue loses precision. - + if let number = Decimal(string: string), number != 0.0 { return number } else if let number = Decimal(string: string, locale: Locale.current), number != 0.0 { @@ -132,8 +132,8 @@ public enum AdamantBalanceFormat { } // MARK: - Helper -public extension NumberFormatter { - func string(from decimal: Decimal) -> String? { +extension NumberFormatter { + public func string(from decimal: Decimal) -> String? { return string(from: NSNumber(value: decimal.doubleValue)) } } diff --git a/CommonKit/Sources/CommonKit/Helpers/AdamantContacts.swift b/CommonKit/Sources/CommonKit/Helpers/AdamantContacts.swift index 200611586..844348fa1 100644 --- a/CommonKit/Sources/CommonKit/Helpers/AdamantContacts.swift +++ b/CommonKit/Sources/CommonKit/Helpers/AdamantContacts.swift @@ -19,11 +19,11 @@ public enum AdamantContacts: CaseIterable { case donate case adamantWelcomeWallet case adelina - + public static var systemAddresses: [String] { Self.allCases.map { $0.name } } - + public var name: String { switch self { case .adamantWelcomeWallet: @@ -46,16 +46,16 @@ public enum AdamantContacts: CaseIterable { return .localized("Accounts.AdamantBountyBot", comment: "System accounts: PWA ADM Bounty bot") } } - + public var isSystem: Bool { switch self { - case .adamantExchange, .betOnBitcoin, .adelina: + case .adamantExchange, .betOnBitcoin, .adelina, .donate: return false - case .adamantWelcomeWallet, .adamantSupport, .adamantIco, .adamantBountyWallet, .adamantNewBountyWallet, .donate, .pwaBountyBot: + case .adamantWelcomeWallet, .adamantSupport, .adamantIco, .adamantBountyWallet, .adamantNewBountyWallet, .pwaBountyBot: return true } } - + public var address: String { switch self { case .adamantBountyWallet: return AdamantResources.contacts.adamantBountyWallet @@ -70,7 +70,7 @@ public enum AdamantContacts: CaseIterable { case .pwaBountyBot: return AdamantResources.contacts.pwaBountyBot } } - + public var publicKey: String? { switch self { case .adamantExchange: return AdamantResources.contacts.adamantExchangePK @@ -85,21 +85,21 @@ public enum AdamantContacts: CaseIterable { case .pwaBountyBot: return AdamantResources.contacts.pwaBountyBotPK } } - + public var isReadonly: Bool { switch self { case .adamantBountyWallet, .adamantNewBountyWallet, .adamantIco, .adamantWelcomeWallet: return true case .adamantSupport, .adamantExchange, .betOnBitcoin, .donate, .adelina, .pwaBountyBot: return false } } - + public var isHidden: Bool { switch self { case .adamantBountyWallet, .adamantNewBountyWallet, .pwaBountyBot: return true case .adamantIco, .adamantSupport, .adamantExchange, .betOnBitcoin, .donate, .adamantWelcomeWallet, .adelina: return false } } - + public var avatar: String { switch self { case .adamantExchange, .betOnBitcoin, .donate, .adamantBountyWallet, .adamantNewBountyWallet, .adelina, .pwaBountyBot: @@ -108,7 +108,7 @@ public enum AdamantContacts: CaseIterable { return "avatar_bots" } } - + public var nodeNameKey: String? { switch self { case .adamantBountyWallet, .adamantNewBountyWallet: @@ -129,16 +129,16 @@ public enum AdamantContacts: CaseIterable { } } -public extension AdamantContacts { - init?(nodeNameKey: String) { +extension AdamantContacts { + public init?(nodeNameKey: String) { guard let contact = Self.allCases .first(where: { nodeNameKey == $0.nodeNameKey }) else { return nil } self = contact } - - init?(address: String) { + + public init?(address: String) { guard let contact = Self.allCases .first(where: { address == $0.address }) diff --git a/CommonKit/Sources/CommonKit/Helpers/AdamantCore+Extensions.swift b/CommonKit/Sources/CommonKit/Helpers/AdamantCore+Extensions.swift index 006a642c4..de9a9999f 100644 --- a/CommonKit/Sources/CommonKit/Helpers/AdamantCore+Extensions.swift +++ b/CommonKit/Sources/CommonKit/Helpers/AdamantCore+Extensions.swift @@ -6,11 +6,11 @@ // Copyright © 2022 Adamant. All rights reserved. // -import Foundation import BigInt +import Foundation -public extension AdamantCore { - func makeSignedTransaction( +extension AdamantCore { + public func makeSignedTransaction( transaction: SignableTransaction, senderId: String, keypair: Keypair @@ -18,7 +18,7 @@ public extension AdamantCore { guard let signature = sign(transaction: transaction, senderId: senderId, keypair: keypair) else { return nil } - + return .init( type: transaction.type, timestamp: transaction.timestamp, @@ -31,22 +31,24 @@ public extension AdamantCore { requesterPublicKey: transaction.requesterPublicKey ) } - - func makeSendMessageTransaction( + + /// Create transaction of the message for sending to blockchain + public func makeSendMessageTransaction( senderId: String, recipientId: String, keypair: Keypair, message: String, type: ChatType, nonce: String, - amount: Decimal? + amount: Decimal?, + date: Date ) throws -> UnregisteredTransaction { let normalizedTransaction = NormalizedTransaction( type: .chatMessage, amount: amount ?? .zero, senderPublicKey: keypair.publicKey, requesterPublicKey: nil, - date: Date(), + date: date, recipientId: recipientId, asset: TransactionAsset( chat: ChatAsset(message: message, ownMessage: nonce, type: type), @@ -54,166 +56,173 @@ public extension AdamantCore { votes: nil ) ) - - guard let transaction = makeSignedTransaction( - transaction: normalizedTransaction, - senderId: senderId, - keypair: keypair - ) else { + + guard + let transaction = makeSignedTransaction( + transaction: normalizedTransaction, + senderId: senderId, + keypair: keypair + ) + else { throw InternalAPIError.signTransactionFailed } - + return transaction } - - func createTransferTransaction( + + /// Create transaction of the transfer for sending to blockchain + public func createTransferTransaction( senderId: String, recipientId: String, keypair: Keypair, - amount: Decimal? + amount: Decimal?, + date: Date ) -> UnregisteredTransaction? { let normalizedTransaction = NormalizedTransaction( type: .send, amount: amount ?? .zero, senderPublicKey: keypair.publicKey, requesterPublicKey: nil, - date: .now, + date: date, recipientId: recipientId, asset: .init() ) - - guard let transaction = makeSignedTransaction( - transaction: normalizedTransaction, - senderId: senderId, - keypair: keypair - ) else { + + guard + let transaction = makeSignedTransaction( + transaction: normalizedTransaction, + senderId: senderId, + keypair: keypair + ) + else { return nil } - + return transaction } } // MARK: - Bytes -public extension UnregisteredTransaction { - func generateId() -> String? { +extension UnregisteredTransaction { + public func generateId() -> String? { let hash = bytes.sha256() - + guard hash.count > 7 else { return nil } - + var temp: [UInt8] = [] - + for i in 0..<8 { temp.insert(hash[7 - i], at: i) } - + guard let value = bigIntFromBuffer(temp, size: 1) else { return nil } - + return String(value) } } -private extension UnregisteredTransaction { - func bigIntFromBuffer(_ buffer: [UInt8], size: Int) -> BigInt? { +extension UnregisteredTransaction { + fileprivate func bigIntFromBuffer(_ buffer: [UInt8], size: Int) -> BigInt? { if buffer.isEmpty || size <= 0 { return nil } - + var chunks: [[UInt8]] = [] - + for i in stride(from: 0, to: buffer.count, by: size) { let chunk = buffer[i] chunks.append([chunk]) } - + let hexStrings = chunks.map { chunk in return chunk.map { byte in let hex = String(byte, radix: 16) return hex.count == 1 ? "0" + hex : hex }.joined() } - + let hex = hexStrings.joined() - + return BigInt(hex, radix: 16) } - - var bytes: [UInt8] { + + fileprivate var bytes: [UInt8] { return typeBytes - + timestampBytes - + senderPublicKeyBytes - + requesterPublicKeyBytes - + recipientIdBytes - + amountBytes - + assetBytes - + signatureBytes - + signSignatureBytes + + timestampBytes + + senderPublicKeyBytes + + requesterPublicKeyBytes + + recipientIdBytes + + amountBytes + + assetBytes + + signatureBytes + + signSignatureBytes } - - var typeBytes: [UInt8] { + + fileprivate var typeBytes: [UInt8] { [UInt8(type.rawValue)] } - - var timestampBytes: [UInt8] { + + fileprivate var timestampBytes: [UInt8] { ByteBackpacker.pack(UInt32(timestamp), byteOrder: .littleEndian) } - - var senderPublicKeyBytes: [UInt8] { + + fileprivate var senderPublicKeyBytes: [UInt8] { senderPublicKey.hexBytes() } - - var requesterPublicKeyBytes: [UInt8] { + + fileprivate var requesterPublicKeyBytes: [UInt8] { requesterPublicKey?.hexBytes() ?? [] } - - var recipientIdBytes: [UInt8] { + + fileprivate var recipientIdBytes: [UInt8] { guard let value = recipientId?.replacingOccurrences(of: "U", with: ""), - let number = UInt64(value) else { return Bytes(count: 8) } + let number = UInt64(value) + else { return Bytes(count: 8) } return ByteBackpacker.pack(number, byteOrder: .bigEndian) } - - var amountBytes: [UInt8] { + + fileprivate var amountBytes: [UInt8] { let value = (self.amount.shiftedToAdamant() as NSDecimalNumber).uint64Value let bytes = ByteBackpacker.pack(value, byteOrder: .littleEndian) return bytes } - - var signatureBytes: [UInt8] { + + fileprivate var signatureBytes: [UInt8] { signature.hexBytes() } - - var signSignatureBytes: [UInt8] { + + fileprivate var signSignatureBytes: [UInt8] { [] } - - var assetBytes: [UInt8] { + + fileprivate var assetBytes: [UInt8] { switch type { case .chatMessage: guard let msg = asset.chat?.message, let own = asset.chat?.ownMessage, let type = asset.chat?.type else { return [] } - + return msg.hexBytes() + own.hexBytes() + ByteBackpacker.pack(UInt32(type.rawValue), byteOrder: .littleEndian) - + case .state: guard let key = asset.state?.key, let value = asset.state?.value, let type = asset.state?.type else { return [] } - + return value.bytes + key.bytes + ByteBackpacker.pack(UInt32(type.rawValue), byteOrder: .littleEndian) - + case .vote: guard let votes = asset.votes?.votes else { return [] } - + var bytes = [UInt8]() for vote in votes { bytes += vote.bytes } - + return bytes - + default: return [] } diff --git a/CommonKit/Sources/CommonKit/Helpers/AdamantSecureStorage.swift b/CommonKit/Sources/CommonKit/Helpers/AdamantSecureStorage.swift index c622f3a7c..55e2ae120 100644 --- a/CommonKit/Sources/CommonKit/Helpers/AdamantSecureStorage.swift +++ b/CommonKit/Sources/CommonKit/Helpers/AdamantSecureStorage.swift @@ -9,17 +9,17 @@ import Foundation public struct AdamantSecureStorage: SecureStorageProtocol { private let tag = "com.adamant.keys.id".data(using: .utf8)! - - public init() { } - + + public init() {} + public func getPrivateKey() -> SecKey? { loadPrivateKey() ?? createAndStorePrivateKey() } - + public func getPublicKey(privateKey: SecKey) -> SecKey? { SecKeyCopyPublicKey(privateKey) } - + public func encrypt(data: Data, publicKey: SecKey) -> Data? { SecKeyCreateEncryptedData( publicKey, @@ -28,7 +28,7 @@ public struct AdamantSecureStorage: SecureStorageProtocol { nil ).map { $0 as Data } } - + public func decrypt(data: Data, privateKey: SecKey) -> Data? { SecKeyCreateDecryptedData( privateKey, @@ -39,31 +39,33 @@ public struct AdamantSecureStorage: SecureStorageProtocol { } } -private extension AdamantSecureStorage { - func loadPrivateKey() -> SecKey? { +extension AdamantSecureStorage { + fileprivate func loadPrivateKey() -> SecKey? { let query: [String: Any] = [ kSecClass as String: kSecClassKey, kSecAttrApplicationTag as String: tag, kSecAttrKeyType as String: kSecAttrKeyTypeEC, kSecReturnRef as String: true ] - + var item: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &item) - + return status == errSecSuccess - ? (item as! SecKey) - : nil + ? (item as! SecKey) + : nil } - - func createAndStorePrivateKey() -> SecKey? { - guard let access = SecAccessControlCreateWithFlags( - kCFAllocatorDefault, - kSecAttrAccessibleAfterFirstUnlock, - .privateKeyUsage, - nil - ) else { return nil } - + + fileprivate func createAndStorePrivateKey() -> SecKey? { + guard + let access = SecAccessControlCreateWithFlags( + kCFAllocatorDefault, + kSecAttrAccessibleAfterFirstUnlock, + .privateKeyUsage, + nil + ) + else { return nil } + let attributes: [String: Any] = [ kSecAttrKeyType as String: kSecAttrKeyTypeEC, kSecAttrKeySizeInBits as String: 256, @@ -74,7 +76,7 @@ private extension AdamantSecureStorage { kSecAttrAccessControl as String: access ] ] - + return SecKeyCreateRandomKey(attributes as CFDictionary, nil) } } diff --git a/CommonKit/Sources/CommonKit/Helpers/AdamantUtilities.swift b/CommonKit/Sources/CommonKit/Helpers/AdamantUtilities.swift index 4189953b0..d8d242579 100644 --- a/CommonKit/Sources/CommonKit/Helpers/AdamantUtilities.swift +++ b/CommonKit/Sources/CommonKit/Helpers/AdamantUtilities.swift @@ -11,30 +11,37 @@ import os public enum AdamantUtilities { public enum Git {} - + public static let admCurrencyExponent: Int = -8 - + // MARK: - Dates - + public static func encodeAdamant(date: Date) -> TimeInterval { return date.timeIntervalSince1970 - magicAdamantTimeInterval } - + public static func decodeAdamant(timestamp: TimeInterval) -> Date { return Date(timeIntervalSince1970: timestamp + magicAdamantTimeInterval) } - - private static let magicAdamantTimeInterval: TimeInterval = { + + public static let magicAdamantTimeInterval: TimeInterval = { // JS handles moth as 0-based number, swift handles month as 1-based number. - let components = DateComponents(calendar: Calendar(identifier: .gregorian), timeZone: TimeZone(abbreviation: "UTC"), year: 2017, month: 9, day: 2, hour: 17) + let components = DateComponents( + calendar: Calendar(identifier: .gregorian), + timeZone: TimeZone(abbreviation: "UTC"), + year: 2017, + month: 9, + day: 2, + hour: 17 + ) return components.date!.timeIntervalSince1970 }() - + // MARK: - JSON - + public static func JSONStringify(value: AnyObject, prettyPrinted: Bool = false) -> String { let options = prettyPrinted ? JSONSerialization.WritingOptions.prettyPrinted : [] - + if JSONSerialization.isValidJSONObject(value) { if let data = try? JSONSerialization.data(withJSONObject: value, options: options) { if let string = String(data: data, encoding: .utf8) { @@ -42,10 +49,10 @@ public enum AdamantUtilities { } } } - + return "" } - + /// Address generation algorithm: /// https://github.com/Adamant-im/adamant/wiki/Generating-ADAMANT-account-and-key-pair#3-a-users-adm-wallet-address-is-generated-from-the-publickeys-sha-256-hash public static func generateAddress(publicKey: String) -> String { @@ -54,15 +61,15 @@ public enum AdamantUtilities { let number = data.withUnsafeBytes { $0.load(as: UInt64.self) } return "U\(number)" } - + public static func consoleLog(_ args: String..., separator: String = " ") { let message = args.joined(separator: separator) os_log("adamant-console-log %{public}@", message) } } -public extension AdamantUtilities.Git { - static let commitHash = Bundle.module.url( +extension AdamantUtilities.Git { + public static let commitHash = Bundle.module.url( forResource: "GitData", withExtension: "plist" ).flatMap { NSDictionary(contentsOf: $0)?.value(forKey: "CommitHash") as? String } diff --git a/CommonKit/Sources/CommonKit/Helpers/ApiServiceError+AFError.swift b/CommonKit/Sources/CommonKit/Helpers/ApiServiceError+AFError.swift index 8d6c1a6ac..d6ca48be0 100644 --- a/CommonKit/Sources/CommonKit/Helpers/ApiServiceError+AFError.swift +++ b/CommonKit/Sources/CommonKit/Helpers/ApiServiceError+AFError.swift @@ -7,10 +7,10 @@ import Alamofire -public extension ApiServiceError { - init(error: Error) { +extension ApiServiceError { + public init(error: Error) { let afError = error as? AFError - + switch afError { case .explicitlyCancelled: self = .requestCancelled diff --git a/CommonKit/Sources/CommonKit/Helpers/Array+adamant.swift b/CommonKit/Sources/CommonKit/Helpers/Array+adamant.swift index df45b552d..c0a3ee817 100644 --- a/CommonKit/Sources/CommonKit/Helpers/Array+adamant.swift +++ b/CommonKit/Sources/CommonKit/Helpers/Array+adamant.swift @@ -7,10 +7,10 @@ import Foundation -public extension Array { - func chunked(into size: Int) -> [[Element]] { +extension Array { + public func chunked(into size: Int) -> [[Element]] { return stride(from: 0, to: count, by: size).map { - Array(self[$0 ..< Swift.min($0 + size, count)]) + Array(self[$0..: @unchecked Sendable { private var _value: Value private let lock = NSLock() - + public var projectedValue: Atomic { self } - + public var wrappedValue: Value { get { value } set { value = newValue } } - + public var value: Value { get { lock.lock() @@ -39,11 +39,11 @@ public final class Atomic: @unchecked Sendable { _value = newValue } } - + public init(_ value: Value) { _value = value } - + public convenience init(wrappedValue: Value) { self.init(wrappedValue) } @@ -56,7 +56,7 @@ public final class Atomic: @unchecked Sendable { defer { lock.unlock() } return mutation(&_value) } - + @discardableResult public func isolated(_ processing: (Value) -> T) -> T { lock.lock() diff --git a/CommonKit/Sources/CommonKit/Helpers/ByteBackpacker.swift b/CommonKit/Sources/CommonKit/Helpers/ByteBackpacker.swift index 9db4c5a92..284896959 100644 --- a/CommonKit/Sources/CommonKit/Helpers/ByteBackpacker.swift +++ b/CommonKit/Sources/CommonKit/Helpers/ByteBackpacker.swift @@ -12,15 +12,15 @@ public typealias Byte = UInt8 public enum ByteOrder: Sendable { case bigEndian case littleEndian - + /// Machine specific byte order public static let nativeByteOrder: ByteOrder = (Int(CFByteOrderGetCurrent()) == Int(CFByteOrderLittleEndian.rawValue)) ? .littleEndian : .bigEndian } open class ByteBackpacker { - + private static let referenceTypeErrorString = "TypeError: Reference Types are not supported." - + /// Unpack a byte array into type `T` /// /// - Parameters: @@ -30,7 +30,7 @@ open class ByteBackpacker { open class func unpack(_ valueByteArray: [Byte], byteOrder: ByteOrder = .nativeByteOrder) -> T { return ByteBackpacker.unpack(valueByteArray, toType: T.self, byteOrder: byteOrder) } - + /// Unpack a byte array into type `T` for type inference /// /// - Parameters: @@ -47,32 +47,32 @@ open class ByteBackpacker { } } } - + /// Pack method convinience method /// /// - Parameters: /// - value: value to pack of type `T` /// - byteOrder: Byte order (wither little or big endian) /// - Returns: Byte array - open class func pack( _ value: T, byteOrder: ByteOrder = .nativeByteOrder) -> [Byte] { + open class func pack(_ value: T, byteOrder: ByteOrder = .nativeByteOrder) -> [Byte] { assert(!(T.self is AnyClass), ByteBackpacker.referenceTypeErrorString) - var value = value // inout works only for var not let types + var value = value // inout works only for var not let types let valueByteArray = withUnsafePointer(to: &value) { - Array(UnsafeBufferPointer(start: $0.withMemoryRebound(to: Byte.self, capacity: 1) {$0}, count: MemoryLayout.size)) + Array(UnsafeBufferPointer(start: $0.withMemoryRebound(to: Byte.self, capacity: 1) { $0 }, count: MemoryLayout.size)) } return (byteOrder == ByteOrder.nativeByteOrder) ? valueByteArray : valueByteArray.reversed() } } -public extension Data { - +extension Data { + /// Extension for exporting Data (NSData) to byte array directly /// /// - Returns: Byte array - func toByteArray() -> [Byte] { + public func toByteArray() -> [Byte] { let count = self.count / MemoryLayout.size var array = [Byte](repeating: 0, count: count) - copyBytes(to: &array, count:count * MemoryLayout.size) + copyBytes(to: &array, count: count * MemoryLayout.size) return array } } diff --git a/CommonKit/Sources/CommonKit/Helpers/CollectionUtilites.swift b/CommonKit/Sources/CommonKit/Helpers/CollectionUtilites.swift index 5c4784243..b32fed798 100644 --- a/CommonKit/Sources/CommonKit/Helpers/CollectionUtilites.swift +++ b/CommonKit/Sources/CommonKit/Helpers/CollectionUtilites.swift @@ -6,17 +6,17 @@ // Copyright © 2022 Adamant. All rights reserved. // -public extension Collection { +extension Collection { /// Returns the element at the specified index if it is within bounds, otherwise nil. - subscript(safe index: Index) -> Element? { + public subscript(safe index: Index) -> Element? { indices.contains(index) ? self[index] : nil } } -public extension Collection where Element: AnyObject { - func hasTheSameReferences(as collection: Self) -> Bool { +extension Collection where Element: AnyObject { + public func hasTheSameReferences(as collection: Self) -> Bool { count == collection.count && !zip(self, collection) .map({ $0 === $1 }) diff --git a/CommonKit/Sources/CommonKit/Helpers/Date+TimestampMs.swift b/CommonKit/Sources/CommonKit/Helpers/Date+TimestampMs.swift new file mode 100644 index 000000000..7755f201f --- /dev/null +++ b/CommonKit/Sources/CommonKit/Helpers/Date+TimestampMs.swift @@ -0,0 +1,15 @@ +// +// Date+Milliseconds.swift +// Adamant +// +// Created by Sergei Veretennikov on 24.02.2025. +// Copyright © 2025 Adamant. All rights reserved. +// + +import Foundation + +extension NSDate { + public var timeIntervalMillisecondsSince1970: Int64 { + Int64(timeIntervalSince1970 * 1000) + } +} diff --git a/CommonKit/Sources/CommonKit/Helpers/Date+adamant.swift b/CommonKit/Sources/CommonKit/Helpers/Date+adamant.swift index 69fba5488..2fb8f7201 100644 --- a/CommonKit/Sources/CommonKit/Helpers/Date+adamant.swift +++ b/CommonKit/Sources/CommonKit/Helpers/Date+adamant.swift @@ -6,19 +6,19 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation import DateToolsSwift +import Foundation -public extension Date { +extension Date { // MARK: - Constants - static let adamantNullDate = Date(timeIntervalSince1970: .zero) - + public static let adamantNullDate = Date(timeIntervalSince1970: .zero) + // MARK: - Humanized dates - + /// Returns readable date with time. - func humanizedDateTime(withWeekday: Bool = true) -> String { + public func humanizedDateTime(withWeekday: Bool = true) -> String { let formatter = defaultFormatter - + if year == Date().year { let dateString: String if isToday { @@ -30,34 +30,34 @@ public extension Date { it will display something like '6 hours ago' */ dateString = String.localized("Chats.Date.Yesterday") - } else if withWeekday && weeksAgo < 1 { // This week, show weekday, month and date + } else if withWeekday && weeksAgo < 1 { // This week, show weekday, month and date dateString = Date.formatterWeekDayMonth.string(from: self) - } else { // This year, long ago: show month and date + } else { // This year, long ago: show month and date dateString = Date.formatterDayMonth.string(from: self) } - + formatter.dateStyle = .none formatter.timeStyle = .short - + return "\(dateString), \(formatter.string(from: self))" } - + formatter.dateStyle = .medium formatter.timeStyle = .short return formatter.string(from: self) } - - func humanizedDateTimeFull() -> String { + + public func humanizedDateTimeFull() -> String { let formatter = defaultFormatter formatter.dateStyle = .long formatter.timeStyle = .short return formatter.string(from: self) } - + /// Returns readable day string. "Today, Yesterday, etc" - func humanizedDay(useTimeFormat: Bool) -> String { + public func humanizedDay(useTimeFormat: Bool) -> String { let dateString: String - if isToday { // Today + if isToday { // Today if useTimeFormat { let formatter = defaultFormatter formatter.dateStyle = .none @@ -66,25 +66,25 @@ public extension Date { } else { dateString = String.localized("Chats.Date.Today") } - } else if daysAgo < 2 { // Yesterday + } else if daysAgo < 2 { // Yesterday dateString = elapsedTime(from: self) - } else if weeksAgo < 1 { // This week, show weekday, month and date + } else if weeksAgo < 1 { // This week, show weekday, month and date dateString = Date.formatterWeekDayMonth.string(from: self) - } else if yearsAgo < 1 { // This year, long ago: show month and date + } else if yearsAgo < 1 { // This year, long ago: show month and date dateString = Date.formatterDayMonth.string(from: self) - } else { // Show full date + } else { // Show full date dateString = DateFormatter.localizedString(from: self, dateStyle: .medium, timeStyle: .none) } - + return dateString } - + /// Returns readable time string. "Just now, minutes ago, 11:30, etc" /// - Returns: Readable string, and time when string will be expired and needs an update - func humanizedTime() -> (string: String, expireIn: TimeInterval?) { + public func humanizedTime() -> (string: String, expireIn: TimeInterval?) { let timeString: String let expire: TimeInterval? - + let seconds = secondsAgo if seconds < 30 { timeString = String.localized("Chats.Date.JustNow") @@ -100,18 +100,18 @@ public extension Date { formatter.dateStyle = .none formatter.timeStyle = .short timeString = formatter.string(from: self) - + expire = nil } - + return (timeString, expire) } } -private extension Date { +extension Date { // MARK: Formatters - - static var formatterWeekDayMonth: DateFormatter { + + fileprivate static var formatterWeekDayMonth: DateFormatter { let formatter = DateFormatter() if let localeRaw = UserDefaults.standard.string(forKey: StoreKey.language.languageLocale) { formatter.locale = Locale(identifier: localeRaw) @@ -119,8 +119,8 @@ private extension Date { formatter.setLocalizedDateFormatFromTemplate("MMMMEEEEd") return formatter } - - static var formatterDayMonth: DateFormatter { + + fileprivate static var formatterDayMonth: DateFormatter { let formatter = DateFormatter() if let localeRaw = UserDefaults.standard.string(forKey: StoreKey.language.languageLocale) { formatter.locale = Locale(identifier: localeRaw) @@ -128,29 +128,29 @@ private extension Date { formatter.setLocalizedDateFormatFromTemplate("MMMMd") return formatter } - - var defaultFormatter: DateFormatter { + + fileprivate var defaultFormatter: DateFormatter { let formatter = DateFormatter() if let localeRaw = UserDefaults.standard.string(forKey: StoreKey.language.languageLocale) { formatter.locale = Locale(identifier: localeRaw) } return formatter } - + // MARK: Helpers - func elapsedTime(from date: Date) -> String { + fileprivate func elapsedTime(from date: Date) -> String { let currentDate = Date() let timeInterval = currentDate.timeIntervalSince(date) - + let seconds = Int(timeInterval) let minutes = seconds / 60 let hours = minutes / 60 let days = hours / 24 - + let formatter = DateComponentsFormatter() formatter.unitsStyle = .full - + if days > 0 { return String.adamant.dateChatList.days(days) } else if hours > 0 { @@ -168,17 +168,23 @@ extension String.adamant { static func days(_ days: Int) -> String { return String.localizedStringWithFormat(.localized("Chats.Date.For.Days", comment: "Date chats: Duration in days if longer than one."), days) } - + static func hours(_ hours: Int) -> String { return String.localizedStringWithFormat(.localized("Chats.Date.For.Hours", comment: "Date chats: Duration in hours if longer than one."), hours) } - + static func minutes(_ minutes: Int) -> String { - return String.localizedStringWithFormat(.localized("Chats.Date.For.Minutes", comment: "Date chats: Duration in minutes if longer than one."), minutes) + return String.localizedStringWithFormat( + .localized("Chats.Date.For.Minutes", comment: "Date chats: Duration in minutes if longer than one."), + minutes + ) } - + static func seconds(_ seconds: Int) -> String { - return String.localizedStringWithFormat(.localized("Chats.Date.For.Seconds", comment: "Date chats: Duration in seconds if longer than one."), seconds) + return String.localizedStringWithFormat( + .localized("Chats.Date.For.Seconds", comment: "Date chats: Duration in seconds if longer than one."), + seconds + ) } } } diff --git a/CommonKit/Sources/CommonKit/Helpers/Decimal+adamant.swift b/CommonKit/Sources/CommonKit/Helpers/Decimal+adamant.swift index ef1aefe1e..66874400d 100644 --- a/CommonKit/Sources/CommonKit/Helpers/Decimal+adamant.swift +++ b/CommonKit/Sources/CommonKit/Helpers/Decimal+adamant.swift @@ -8,15 +8,15 @@ import Foundation -public extension Decimal { +extension Decimal { public func shiftedFromAdamant() -> Decimal { return Decimal(sign: self.isSignMinus ? .minus : .plus, exponent: AdamantUtilities.admCurrencyExponent, significand: self) } - + public func shiftedToAdamant() -> Decimal { return Decimal(sign: self.isSignMinus ? .minus : .plus, exponent: -AdamantUtilities.admCurrencyExponent, significand: self) } - + public var doubleValue: Double { // NSDecimalNumber loses decimal precision when deserializing numbers by doubleValue. // Try to get string value and deserialize it diff --git a/CommonKit/Sources/CommonKit/Helpers/Double+adamant.swift b/CommonKit/Sources/CommonKit/Helpers/Double+adamant.swift index 1a0ad09a8..48ecc493a 100644 --- a/CommonKit/Sources/CommonKit/Helpers/Double+adamant.swift +++ b/CommonKit/Sources/CommonKit/Helpers/Double+adamant.swift @@ -8,8 +8,8 @@ import Foundation -public extension Double { - func format(with formatter: NumberFormatter) -> String { +extension Double { + public func format(with formatter: NumberFormatter) -> String { let number = NSNumber(value: self) let formattedValue = formatter.string(from: number) ?? "\(number)" return formattedValue diff --git a/CommonKit/Sources/CommonKit/Helpers/Encodable+Dictionary.swift b/CommonKit/Sources/CommonKit/Helpers/Encodable+Dictionary.swift index 201048be4..d7fd86a85 100644 --- a/CommonKit/Sources/CommonKit/Helpers/Encodable+Dictionary.swift +++ b/CommonKit/Sources/CommonKit/Helpers/Encodable+Dictionary.swift @@ -8,13 +8,13 @@ import Foundation -public extension Encodable { - func asDictionary() -> [String: Any]? { +extension Encodable { + public func asDictionary() -> [String: Any]? { guard let data = try? JSONEncoder().encode(self), let object = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) else { return nil } - + return object as? [String: Any] } } diff --git a/CommonKit/Sources/CommonKit/Helpers/FilePresentationHelper.swift b/CommonKit/Sources/CommonKit/Helpers/FilePresentationHelper.swift index 1d7f56bd7..0421017bd 100644 --- a/CommonKit/Sources/CommonKit/Helpers/FilePresentationHelper.swift +++ b/CommonKit/Sources/CommonKit/Helpers/FilePresentationHelper.swift @@ -1,6 +1,6 @@ // // FilePresentationHelper.swift -// +// // // Created by Stanislav Jelezoglo on 26.04.2024. // @@ -13,41 +13,44 @@ public class FilePresentationHelper { otherFilesCount: Int, comment: String ) -> String { - let mediaCountText = mediaFilesCount > 1 - ? "\(mediaFilesCount)" - : .empty - - let otherFilesCountText = otherFilesCount > 1 - ? "\(otherFilesCount)" - : .empty - + let mediaCountText = + mediaFilesCount > 1 + ? "\(mediaFilesCount)" + : .empty + + let otherFilesCountText = + otherFilesCount > 1 + ? "\(otherFilesCount)" + : .empty + let mediaText = mediaFilesCount > 0 ? "📸\(mediaCountText)" : .empty let fileText = otherFilesCount > 0 ? "📄\(otherFilesCountText)" : .empty - + let text = [mediaText, fileText, comment].filter { !$0.isEmpty }.joined(separator: " ") - + return text } - + public static func getFilePresentationText(_ richContent: [String: Any]) -> String { let content = richContent[RichContentKeys.reply.replyMessage] as? [String: Any] ?? richContent - + let files = content[RichContentKeys.file.files] as? [[String: Any]] ?? [] - + let mediaFilesCount = files.filter { file in let mimeType = file[RichContentKeys.file.mimeType] as? String ?? .empty let fileType = FileType(mimeType: mimeType) ?? .other return fileType == .image || fileType == .video }.count - + let otherFilesCount = files.count - mediaFilesCount - - let comment = (content[RichContentKeys.file.comment] as? String).flatMap { - $0.isEmpty ? nil : $0 - } ?? .empty - + + let comment = + (content[RichContentKeys.file.comment] as? String).flatMap { + $0.isEmpty ? nil : $0 + } ?? .empty + return Self.getFilePresentationText( mediaFilesCount: mediaFilesCount, otherFilesCount: otherFilesCount, diff --git a/CommonKit/Sources/CommonKit/Helpers/FilesConstants.swift b/CommonKit/Sources/CommonKit/Helpers/FilesConstants.swift index 42c74b911..23439aa64 100644 --- a/CommonKit/Sources/CommonKit/Helpers/FilesConstants.swift +++ b/CommonKit/Sources/CommonKit/Helpers/FilesConstants.swift @@ -1,6 +1,6 @@ // // FilesConstants.swift -// +// // // Created by Stanislav Jelezoglo on 10.04.2024. // diff --git a/CommonKit/Sources/CommonKit/Helpers/GCDUtilites.swift b/CommonKit/Sources/CommonKit/Helpers/GCDUtilites.swift index 4ba1d42bd..345d62ea7 100644 --- a/CommonKit/Sources/CommonKit/Helpers/GCDUtilites.swift +++ b/CommonKit/Sources/CommonKit/Helpers/GCDUtilites.swift @@ -8,21 +8,21 @@ import Foundation -public extension DispatchQueue { +extension DispatchQueue { @discardableResult - static func onMainThreadSyncSafe(_ action: @MainActor () -> T) -> T { + public static func onMainThreadSyncSafe(_ action: @MainActor () -> T) -> T { Thread.isMainThread ? MainActor.assumeIsolated(action) : DispatchQueue.main.sync(execute: action) } - + /// Do not use it anymore. It makes unclear in which order code is executed. - static func onMainAsync(_ action: @escaping @MainActor () -> Void) { + public static func onMainAsync(_ action: @escaping @MainActor () -> Void) { guard Thread.isMainThread else { DispatchQueue.main.async(execute: action) return } - + MainActor.assumeIsolated(action) } } diff --git a/CommonKit/Sources/CommonKit/Helpers/HashableIDWrapper.swift b/CommonKit/Sources/CommonKit/Helpers/HashableIDWrapper.swift index f3c2ac52e..fb3b009d4 100644 --- a/CommonKit/Sources/CommonKit/Helpers/HashableIDWrapper.swift +++ b/CommonKit/Sources/CommonKit/Helpers/HashableIDWrapper.swift @@ -1,6 +1,6 @@ // // File.swift -// +// // // Created by Stanislav Jelezoglo on 19.10.2023. // @@ -10,16 +10,16 @@ import Foundation public struct HashableIDWrapper: Hashable { public let identifier: ComplexIdentifier public let value: Value - + public init(identifier: ComplexIdentifier, value: Value) { self.identifier = identifier self.value = value } - + public func hash(into hasher: inout Hasher) { hasher.combine(identifier) } - + public static func == (lhs: Self, rhs: Self) -> Bool { lhs.identifier == rhs.identifier } @@ -28,7 +28,7 @@ public struct HashableIDWrapper: Hashable { public struct ComplexIdentifier: Hashable { public let identifier: String public let index: Int - + public init(identifier: String, index: Int) { self.identifier = identifier self.index = index diff --git a/CommonKit/Sources/CommonKit/Helpers/IDWrapper.swift b/CommonKit/Sources/CommonKit/Helpers/IDWrapper.swift index d30a5208f..d006aa1ed 100644 --- a/CommonKit/Sources/CommonKit/Helpers/IDWrapper.swift +++ b/CommonKit/Sources/CommonKit/Helpers/IDWrapper.swift @@ -11,7 +11,7 @@ import Foundation public struct IDWrapper: Identifiable { public let id: String public let value: T - + public init(id: String, value: T) { self.id = id self.value = value diff --git a/CommonKit/Sources/CommonKit/Helpers/MacOSDeterminer.swift b/CommonKit/Sources/CommonKit/Helpers/MacOSDeterminer.swift index 15dbf8c6e..bf9fd323c 100644 --- a/CommonKit/Sources/CommonKit/Helpers/MacOSDeterminer.swift +++ b/CommonKit/Sources/CommonKit/Helpers/MacOSDeterminer.swift @@ -9,8 +9,8 @@ import Foundation public let isMacOS: Bool = { #if targetEnvironment(macCatalyst) - true + true #else - ProcessInfo.processInfo.isiOSAppOnMac + ProcessInfo.processInfo.isiOSAppOnMac #endif }() diff --git a/CommonKit/Sources/CommonKit/Helpers/MainActor+Extension.swift b/CommonKit/Sources/CommonKit/Helpers/MainActor+Extension.swift index 544d02767..923d08092 100644 --- a/CommonKit/Sources/CommonKit/Helpers/MainActor+Extension.swift +++ b/CommonKit/Sources/CommonKit/Helpers/MainActor+Extension.swift @@ -7,9 +7,9 @@ import Foundation -public extension MainActor { +extension MainActor { @discardableResult - static func assumeIsolatedSafe(_ action: @MainActor () -> T) -> T { + public static func assumeIsolatedSafe(_ action: @MainActor () -> T) -> T { assertIsolated() return DispatchQueue.onMainThreadSyncSafe(action) } diff --git a/CommonKit/Sources/CommonKit/Helpers/MessageProcessHelper.swift b/CommonKit/Sources/CommonKit/Helpers/MessageProcessHelper.swift index d2251564b..ebf4a4872 100644 --- a/CommonKit/Sources/CommonKit/Helpers/MessageProcessHelper.swift +++ b/CommonKit/Sources/CommonKit/Helpers/MessageProcessHelper.swift @@ -11,7 +11,7 @@ public final class MessageProcessHelper { public static func process(_ text: String) -> String { text.replacingOccurrences(of: "\n", with: "↵ ") } - + public static func process(attributedText: NSMutableAttributedString) -> NSMutableAttributedString { attributedText.mutableString.replaceOccurrences( of: "\n", @@ -19,14 +19,14 @@ public final class MessageProcessHelper { range: .init(location: .zero, length: attributedText.length) ) let raw = attributedText.string - + var ranges: [Range] = [] var searchRange = raw.startIndex..(_ object: T) -> T? { +extension NSManagedObjectContext { + public func existingObject(_ object: T) -> T? { try? existingObject(with: object.objectID) as? T } } diff --git a/CommonKit/Sources/CommonKit/Helpers/NodeGroup+Constants.swift b/CommonKit/Sources/CommonKit/Helpers/NodeGroup+Constants.swift index d4d9bdb73..723b21006 100644 --- a/CommonKit/Sources/CommonKit/Helpers/NodeGroup+Constants.swift +++ b/CommonKit/Sources/CommonKit/Helpers/NodeGroup+Constants.swift @@ -1,14 +1,14 @@ // // NodeGroup+Constants.swift -// +// // // Created by Andrew G on 18.11.2023. // import Foundation -public extension NodeGroup { - var defaultFastestNodeMode: Bool { +extension NodeGroup { + public var defaultFastestNodeMode: Bool { switch self { case .adm: return false diff --git a/CommonKit/Sources/CommonKit/Helpers/Nodes+Allowance.swift b/CommonKit/Sources/CommonKit/Helpers/Nodes+Allowance.swift index d9aef7516..55cf442a5 100644 --- a/CommonKit/Sources/CommonKit/Helpers/Nodes+Allowance.swift +++ b/CommonKit/Sources/CommonKit/Helpers/Nodes+Allowance.swift @@ -6,14 +6,14 @@ // Copyright © 2022 Adamant. All rights reserved. // -public extension Collection where Element == Node { - func getAllowedNodes(sortedBySpeedDescending: Bool, needWS: Bool) -> [Node] { +extension Collection where Element == Node { + public func getAllowedNodes(sortedBySpeedDescending: Bool, needWS: Bool) -> [Node] { let allowedNodes = filter { $0.connectionStatus == .allowed - && $0.isEnabled - && (!needWS || $0.wsEnabled) + && $0.isEnabled + && (!needWS || $0.wsEnabled) } - + return sortedBySpeedDescending ? allowedNodes.sorted { $0.ping ?? .greatestFiniteMagnitude < $1.ping ?? .greatestFiniteMagnitude diff --git a/CommonKit/Sources/CommonKit/Helpers/Observation/AnySendableAsyncSequence.swift b/CommonKit/Sources/CommonKit/Helpers/Observation/AnySendableAsyncSequence.swift index 3e1f52205..2f0f27010 100644 --- a/CommonKit/Sources/CommonKit/Helpers/Observation/AnySendableAsyncSequence.swift +++ b/CommonKit/Sources/CommonKit/Helpers/Observation/AnySendableAsyncSequence.swift @@ -11,7 +11,7 @@ @available(watchOS, deprecated: 11, message: "`AsyncSequence.Failure` is available, so use the `any` keyword") public struct AnySendableAsyncSequence: Sendable { private let _makeAsyncIterator: @Sendable () -> AsyncIterator - + public init(_ wrapped: Wrapped) where Wrapped.Element == Element { _makeAsyncIterator = { .init(wrapped.makeAsyncIterator()) } } @@ -20,13 +20,13 @@ public struct AnySendableAsyncSequence: Sendable { extension AnySendableAsyncSequence: AsyncSequence { public struct AsyncIterator { private let _next: () async throws -> Element? - + init(_ wrapped: Wrapped) where Wrapped.Element == Element { var iterator = wrapped _next = { try await iterator.next() } } } - + public func makeAsyncIterator() -> AsyncIterator { _makeAsyncIterator() } @@ -38,8 +38,8 @@ extension AnySendableAsyncSequence.AsyncIterator: AsyncIteratorProtocol { } } -public extension AsyncSequence where Self: Sendable { - func eraseToAnySendableAsyncSequence() -> AnySendableAsyncSequence { +extension AsyncSequence where Self: Sendable { + public func eraseToAnySendableAsyncSequence() -> AnySendableAsyncSequence { .init(self) } } diff --git a/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncSequence+Extension.swift b/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncSequence+Extension.swift index 96110ea8d..fb44b7ff9 100644 --- a/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncSequence+Extension.swift +++ b/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncSequence+Extension.swift @@ -5,11 +5,11 @@ // Created by Andrew G on 09.10.2024. // -import Combine import AsyncAlgorithms +import Combine -public extension AsyncSequence where Self: Sendable { - func sink( +extension AsyncSequence where Self: Sendable { + public func sink( receiveValue: @escaping @Sendable (Element) async -> Void, receiveCompletion: @escaping @Sendable (Error?) async -> Void = { _ in } ) -> AnyCancellable { @@ -18,25 +18,25 @@ public extension AsyncSequence where Self: Sendable { for try await newValue in self { await receiveValue(newValue) } - + await receiveCompletion(nil) } catch { await receiveCompletion(error) } }.eraseToAnyCancellable() } - - func combineLatest(_ other: T) -> AsyncCombineLatest2Sequence { + + public func combineLatest(_ other: T) -> AsyncCombineLatest2Sequence { AsyncAlgorithms.combineLatest(self, other) } } -public extension AsyncSequence { - var first: Element? { +extension AsyncSequence { + public var first: Element? { get async throws { try await first { _ in true } } } - - func handleEvents(receiveOutput: @escaping (Element) async throws -> Void) -> AsyncMapSequence { + + public func handleEvents(receiveOutput: @escaping (Element) async throws -> Void) -> AsyncMapSequence { map { [receiveOutput] in try? await receiveOutput($0) return $0 diff --git a/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncStreamSender.swift b/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncStreamSender.swift index 01fbda4d9..1bbf29af3 100644 --- a/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncStreamSender.swift +++ b/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncStreamSender.swift @@ -8,18 +8,18 @@ public final class AsyncStreamSender: Sendable { public let stream: AsyncStream private let continuation: AsyncStream.Continuation? - + public init() { var continuation: AsyncStream.Continuation? stream = .init { continuation = $0 } assert(continuation != nil) self.continuation = continuation } - + public func send(_ value: Element) { continuation?.yield(value) } - + public func finish() { continuation?.finish() } diff --git a/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncStreamable/AnyAsyncStreamable.swift b/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncStreamable/AnyAsyncStreamable.swift index 018849e44..7f676c206 100644 --- a/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncStreamable/AnyAsyncStreamable.swift +++ b/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncStreamable/AnyAsyncStreamable.swift @@ -13,18 +13,18 @@ import Foundation @available(watchOS, deprecated: 11, message: "`AsyncSequence.Failure` is available, so use the `any` keyword") public struct AnyAsyncStreamable: AsyncStreamable { private let _makeSequence: @Sendable () -> AnySendableAsyncSequence - + public init(_ wrapped: Wrapped) where Wrapped.Element == Element { _makeSequence = { wrapped.makeSequence().eraseToAnySendableAsyncSequence() } } - + public func makeSequence() -> AnySendableAsyncSequence { _makeSequence() } } -public extension AsyncStreamable { - func eraseToAnyAsyncStreamable() -> AnyAsyncStreamable { +extension AsyncStreamable { + public func eraseToAnyAsyncStreamable() -> AnyAsyncStreamable { .init(self) } } diff --git a/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncStreamable/AsyncMapStreamable.swift b/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncStreamable/AsyncMapStreamable.swift index 58b8a5148..91f01748a 100644 --- a/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncStreamable/AsyncMapStreamable.swift +++ b/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncStreamable/AsyncMapStreamable.swift @@ -13,7 +13,7 @@ public struct AsyncMapStreamable< >: AsyncStreamable where NewProducedSequence.Element: Sendable { private let wrapped: Wrapped private let transformation: @Sendable (Wrapped.ProducedSequence) -> NewProducedSequence - + public init( wrapped: Wrapped, transformation: @escaping @Sendable (Wrapped.ProducedSequence) -> NewProducedSequence @@ -21,14 +21,14 @@ public struct AsyncMapStreamable< self.wrapped = wrapped self.transformation = transformation } - + public func makeSequence() -> NewProducedSequence { transformation(wrapped.makeSequence()) } } -public extension AsyncStreamable { - func map( +extension AsyncStreamable { + public func map( _ transformation: @escaping @Sendable (ProducedSequence) -> NewProducedSequence ) -> AsyncMapStreamable { .init(wrapped: self, transformation: transformation) diff --git a/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncStreamable/AsyncStreamable.swift b/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncStreamable/AsyncStreamable.swift index aae91fcbb..7124d3df7 100644 --- a/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncStreamable/AsyncStreamable.swift +++ b/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncStreamable/AsyncStreamable.swift @@ -10,6 +10,6 @@ import Foundation public protocol AsyncStreamable: Sendable { associatedtype Element: Sendable associatedtype ProducedSequence: AsyncSequence & Sendable where ProducedSequence.Element == Element - + func makeSequence() -> ProducedSequence } diff --git a/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncStreamable/SendablePublisher+Extension.swift b/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncStreamable/SendablePublisher+Extension.swift index f4c17aa49..86081853e 100644 --- a/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncStreamable/SendablePublisher+Extension.swift +++ b/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncStreamable/SendablePublisher+Extension.swift @@ -7,18 +7,18 @@ import Combine -public extension SendablePublisher where P: Subject { - func send(_ value: Element) { +extension SendablePublisher where P: Subject { + public func send(_ value: Element) { publisher.send(value) } - - func send(completion: Subscribers.Completion) { + + public func send(completion: Subscribers.Completion) { publisher.send(completion: completion) } } -public extension SendableObservableValue where P: ValueSubject { - var value: Element { +extension SendableObservableValue where P: ValueSubject { + public var value: Element { get { publisher.value } set { publisher.value = newValue } } diff --git a/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncStreamable/SendablePublisher.swift b/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncStreamable/SendablePublisher.swift index 1925177b1..4e5faf07b 100644 --- a/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncStreamable/SendablePublisher.swift +++ b/CommonKit/Sources/CommonKit/Helpers/Observation/AsyncStreamable/SendablePublisher.swift @@ -5,22 +5,23 @@ // Created by Andrew G on 12.10.2024. // -import Foundation import Combine +import Foundation public actor SendablePublisher< P: Publisher >: StreamSendableActor where P.Output: Sendable, P.Failure: Sendable { public var streamSubscription: AnyCancellable? - - nonisolated public let streamSender: AsyncStreamSender< - @Sendable (isolated SendablePublisher

) -> Void - > = .init() - + + nonisolated public let streamSender: + AsyncStreamSender< + @Sendable (isolated SendablePublisher

) -> Void + > = .init() + private var subscriptions = [UUID: AnyCancellable]() - + public let publisher: P - + public init(_ publisher: @autoclosure @Sendable () -> P) { self.publisher = publisher() Task { await configureStream() } @@ -35,27 +36,27 @@ extension SendablePublisher: AsyncStreamable { } } -private extension SendablePublisher { - func subscribe(_ continuation: AsyncThrowingStream.Continuation) { +extension SendablePublisher { + fileprivate func subscribe(_ continuation: AsyncThrowingStream.Continuation) { let id = UUID() - + subscriptions[id] = publisher.sink( receiveCompletion: { continuation.finish(throwing: $0.error) }, receiveValue: { continuation.yield($0) } ) - + continuation.onTermination = { [self] _ in Task { await removeSubscription(id: id) } } } - - func removeSubscription(id: UUID) { + + fileprivate func removeSubscription(id: UUID) { subscriptions.removeValue(forKey: id) } } -private extension Subscribers.Completion { - var error: Failure? { +extension Subscribers.Completion { + fileprivate var error: Failure? { switch self { case .finished: return nil diff --git a/CommonKit/Sources/CommonKit/Helpers/Observation/ObservableValue.swift b/CommonKit/Sources/CommonKit/Helpers/Observation/ObservableValue.swift index 42276c0a9..4ffa43c8a 100644 --- a/CommonKit/Sources/CommonKit/Helpers/Observation/ObservableValue.swift +++ b/CommonKit/Sources/CommonKit/Helpers/Observation/ObservableValue.swift @@ -13,18 +13,18 @@ import Combine @propertyWrapper public final class ObservableValue: Publisher { public typealias Output = Output public typealias Failure = Never - + private let subject: CurrentValueSubject public var wrappedValue: Output { get { value } set { value = newValue } } - + public var projectedValue: some Observable { subject } - + public init(_ value: Output) { subject = .init(value) } @@ -39,19 +39,19 @@ extension ObservableValue: ValueSubject { get { subject.value } set { subject.value = newValue } } - + public func send(_ value: Output) { subject.send(value) } - + public func send(completion: Subscribers.Completion) { subject.send(completion: completion) } - + public func send(subscription: any Subscription) { subject.send(subscription: subscription) } - + public func receive( subscriber: S ) where S: Subscriber, Never == S.Failure, Output == S.Input { @@ -59,8 +59,8 @@ extension ObservableValue: ValueSubject { } } -public extension Publisher where Failure == Never { - func assign(to observableValue: ObservableValue) -> AnyCancellable { +extension Publisher where Failure == Never { + public func assign(to observableValue: ObservableValue) -> AnyCancellable { assign(to: \.wrappedValue, on: observableValue) } } diff --git a/CommonKit/Sources/CommonKit/Helpers/RichMessageTools.swift b/CommonKit/Sources/CommonKit/Helpers/RichMessageTools.swift index e0c1a2fc2..7e38e2d97 100644 --- a/CommonKit/Sources/CommonKit/Helpers/RichMessageTools.swift +++ b/CommonKit/Sources/CommonKit/Helpers/RichMessageTools.swift @@ -13,27 +13,27 @@ public enum RichMessageTools { guard let jsonRaw = try? JSONSerialization.jsonObject(with: data, options: []) else { return nil } - + switch jsonRaw { - // Valid format - case var json as [String:String]: + // Valid format + case var json as [String: String]: if let key = json[RichContentKeys.type] { json[RichContentKeys.type] = key.lowercased() } - + return json - - // Broken format, try to fix it - case var json as [String:Any]: + + // Broken format, try to fix it + case var json as [String: Any]: if let key = json[RichContentKeys.type] as? String { json[RichContentKeys.type] = key.lowercased() } - + var fixedJson: [String: Any] = [:] - + let formatter = AdamantBalanceFormat.rawNumberDotFormatter formatter.decimalSeparator = "." - + for (key, raw) in json { if let value = raw as? String { fixedJson[key] = value @@ -45,9 +45,9 @@ public enum RichMessageTools { fixedJson[key] = raw } } - + return fixedJson - + default: return nil } diff --git a/CommonKit/Sources/CommonKit/Helpers/SafeDecodingArray.swift b/CommonKit/Sources/CommonKit/Helpers/SafeDecodingArray.swift index f3898e692..d06072ce8 100644 --- a/CommonKit/Sources/CommonKit/Helpers/SafeDecodingArray.swift +++ b/CommonKit/Sources/CommonKit/Helpers/SafeDecodingArray.swift @@ -7,7 +7,7 @@ public struct SafeDecodingArray { public let values: [T] - + init(_ values: [T]) { self.values = values } @@ -16,7 +16,7 @@ public struct SafeDecodingArray { extension SafeDecodingArray: Sequence { public typealias Element = T public typealias Iterator = IndexingIterator<[Element]> - + public func makeIterator() -> Iterator { values.makeIterator() } @@ -33,7 +33,7 @@ extension SafeDecodingArray: Decodable { struct Item { let value: Value? } - + public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let items = try container.decode([Item].self) diff --git a/CommonKit/Sources/CommonKit/Helpers/SafeDecodingDictionary.swift b/CommonKit/Sources/CommonKit/Helpers/SafeDecodingDictionary.swift index 4b7dcf028..bcc0d341c 100644 --- a/CommonKit/Sources/CommonKit/Helpers/SafeDecodingDictionary.swift +++ b/CommonKit/Sources/CommonKit/Helpers/SafeDecodingDictionary.swift @@ -7,7 +7,7 @@ public struct SafeDecodingDictionary { public let values: [Key: Value] - + init(_ values: [Key: Value]) { self.values = values } @@ -24,24 +24,24 @@ extension SafeDecodingDictionary: Decodable { struct KeyItem: Hashable { let value: T? } - + struct ValueItem { let value: T? } - + public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let items = try container.decode([KeyItem: ValueItem].self) - + let keysAndValues: [(Key, Value)] = items.compactMap { keyItem, valueItem in guard let key = keyItem.value, let value = valueItem.value else { return nil } - + return (key, value) } - + values = .init(uniqueKeysWithValues: keysAndValues) } } diff --git a/CommonKit/Sources/CommonKit/Helpers/ServerResponse+Resolver.swift b/CommonKit/Sources/CommonKit/Helpers/ServerResponse+Resolver.swift index c59a2d339..4d5cb36f9 100644 --- a/CommonKit/Sources/CommonKit/Helpers/ServerResponse+Resolver.swift +++ b/CommonKit/Sources/CommonKit/Helpers/ServerResponse+Resolver.swift @@ -6,8 +6,8 @@ // Copyright © 2023 Adamant. All rights reserved. // -public extension ServerModelResponse { - func resolved() -> ApiServiceResult { +extension ServerModelResponse { + public func resolved() -> ApiServiceResult { if let model = model { return .success(model) } else { @@ -16,8 +16,8 @@ public extension ServerModelResponse { } } -public extension ServerCollectionResponse { - func resolved() -> ApiServiceResult<[T]> { +extension ServerCollectionResponse { + public func resolved() -> ApiServiceResult<[T]> { if let collection = collection { return .success(collection) } else { @@ -26,8 +26,8 @@ public extension ServerCollectionResponse { } } -public extension TransactionIdResponse { - func resolved() -> ApiServiceResult { +extension TransactionIdResponse { + public func resolved() -> ApiServiceResult { if let ransactionId = transactionId { return .success(ransactionId) } else { @@ -36,8 +36,8 @@ public extension TransactionIdResponse { } } -public extension GetPublicKeyResponse { - func resolved() -> ApiServiceResult { +extension GetPublicKeyResponse { + public func resolved() -> ApiServiceResult { if let publicKey = publicKey { return .success(publicKey) } else { @@ -48,7 +48,7 @@ public extension GetPublicKeyResponse { private func translateServerError(_ error: String?) -> ApiServiceError { guard let error = error else { return .internalError(error: InternalAPIError.unknownError) } - + switch error { case "Account not found": return .accountNotFound diff --git a/CommonKit/Sources/CommonKit/Helpers/StreamSendableActor.swift b/CommonKit/Sources/CommonKit/Helpers/StreamSendableActor.swift index d3453a3be..bb8064fb2 100644 --- a/CommonKit/Sources/CommonKit/Helpers/StreamSendableActor.swift +++ b/CommonKit/Sources/CommonKit/Helpers/StreamSendableActor.swift @@ -12,12 +12,12 @@ public protocol StreamSendableActor: Actor { nonisolated var streamSender: AsyncStreamSender<@Sendable (isolated Self) -> Void> { get } } -public extension StreamSendableActor { - nonisolated func task(_ action: @escaping @Sendable (isolated Self) -> Void) { +extension StreamSendableActor { + public nonisolated func task(_ action: @escaping @Sendable (isolated Self) -> Void) { streamSender.send(action) } - - func configureStream() { + + public func configureStream() { streamSubscription = Task { [weak self, streamSender] in for await action in streamSender.stream { guard let self else { return } diff --git a/CommonKit/Sources/CommonKit/Helpers/String+utilites.swift b/CommonKit/Sources/CommonKit/Helpers/String+utilites.swift index 2fd6525dd..150dcb645 100644 --- a/CommonKit/Sources/CommonKit/Helpers/String+utilites.swift +++ b/CommonKit/Sources/CommonKit/Helpers/String+utilites.swift @@ -8,10 +8,10 @@ import Foundation -public extension String { - static let empty: String = "" - - func toDictionary() -> [String: Any]? { +extension String { + public static let empty: String = "" + + public func toDictionary() -> [String: Any]? { if let data = self.data(using: .utf8) { do { return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] @@ -21,12 +21,12 @@ public extension String { } return nil } - - func matches(for regex: String) -> [String] { + + public func matches(for regex: String) -> [String] { do { let regex = try NSRegularExpression(pattern: regex) let results = regex.matches(in: self, range: NSRange(self.startIndex..., in: self)) - + return results.map { String(self[Range($0.range, in: self)!]) } @@ -35,42 +35,42 @@ public extension String { return [] } } - - func checkAndReplaceSystemWallets() -> String { + + public func checkAndReplaceSystemWallets() -> String { AdamantContacts(nodeNameKey: self)?.name ?? AdamantContacts(address: self)?.name ?? self } - - subscript(i: Int) -> Character { + + public subscript(i: Int) -> Character { return self[index(startIndex, offsetBy: i)] } - - func separateFileExtension() -> (name: String, extension: String?) { + + public func separateFileExtension() -> (name: String, extension: String?) { guard let dotIndex = lastIndex(of: ".") else { return (name: self, extension: nil) } - + return ( - name: .init(self[startIndex ..< dotIndex]), - extension: .init(self[dotIndex ..< endIndex].dropFirst()) + name: .init(self[startIndex.. String { + + public func withoutFileExtensionDuplication() -> String { let dotsCount = count { $0 == "." } guard dotsCount > 1 else { return self } - + var nameAndExtension = separateFileExtension() var filename = nameAndExtension.name guard let ext = nameAndExtension.extension else { return self } - - for _ in 1 ..< dotsCount { + + for _ in 1.. String? { guard let string = obj as? String else { return nil } - + return String(string.prefix(maxLength)) } @@ -35,7 +35,7 @@ public class StringMaxLengthFormatter: Formatter { if let obj = obj { obj.pointee = self.string(for: string) as AnyObject? } - + return true } } diff --git a/CommonKit/Sources/CommonKit/Helpers/SwiftUI/Animation.swift b/CommonKit/Sources/CommonKit/Helpers/SwiftUI/Animation.swift index 953e0f7eb..0cbba6017 100644 --- a/CommonKit/Sources/CommonKit/Helpers/SwiftUI/Animation.swift +++ b/CommonKit/Sources/CommonKit/Helpers/SwiftUI/Animation.swift @@ -1,6 +1,6 @@ // // Animation.swift -// +// // // Created by Stanislav Jelezoglo on 01.08.2023. // diff --git a/CommonKit/Sources/CommonKit/Helpers/SwiftUI/BlockingView.swift b/CommonKit/Sources/CommonKit/Helpers/SwiftUI/BlockingView.swift index 752dac21b..f70ee709e 100644 --- a/CommonKit/Sources/CommonKit/Helpers/SwiftUI/BlockingView.swift +++ b/CommonKit/Sources/CommonKit/Helpers/SwiftUI/BlockingView.swift @@ -1,6 +1,6 @@ // // File.swift -// +// // // Created by Andrey Golubenko on 06.12.2022. // @@ -12,9 +12,9 @@ public struct BlockingView: UIViewRepresentable { public func makeUIView(context _: Context) -> some UIView { UIBlockingView() } - + public func updateUIView(_: UIViewType, context _: Context) {} - + public init() {} } diff --git a/CommonKit/Sources/CommonKit/Helpers/SwiftUI/Blur.swift b/CommonKit/Sources/CommonKit/Helpers/SwiftUI/Blur.swift index 08185c989..3263ce909 100644 --- a/CommonKit/Sources/CommonKit/Helpers/SwiftUI/Blur.swift +++ b/CommonKit/Sources/CommonKit/Helpers/SwiftUI/Blur.swift @@ -1,33 +1,33 @@ // // Blur.swift -// +// // // Created by Andrey Golubenko on 06.12.2022. // -import SwiftUI import Combine +import SwiftUI public struct Blur: UIViewRepresentable { private let style: UIBlurEffect.Style private let sensetivity: Double - + public func makeUIView(context: Context) -> UIVisualEffectView { BlurEffectView( effect: UIBlurEffect(style: style), sensivity: sensetivity ) } - + public func updateUIView(_ uiView: UIVisualEffectView, context _: Context) { uiView.effect = UIBlurEffect(style: style) } - + public init(style: UIBlurEffect.Style) { self.style = style self.sensetivity = 1.0 } - + public init(style: UIBlurEffect.Style, sensetivity: Double) { self.style = style self.sensetivity = sensetivity @@ -36,58 +36,58 @@ public struct Blur: UIViewRepresentable { final class BlurEffectView: UIVisualEffectView { // MARK: Proprieties - + private var animator: UIViewPropertyAnimator? private var sensivity = 0.2 private var visualEffect: UIVisualEffect? private var subscriptions = Set() - + // MARK: Init - + init(effect: UIVisualEffect?, sensivity: Double) { super.init(effect: effect) self.sensivity = sensivity self.visualEffect = effect } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func didMoveToSuperview() { guard let superview = superview else { return } - + backgroundColor = .clear frame = superview.bounds - + guard animator == nil else { setupSensivity(sensivity: sensivity) return } - + setupBlur() addObservers() } - + func setupBlur() { if animator == nil { animator = UIViewPropertyAnimator(duration: 1, curve: .linear) } - + animator?.stopAnimation(true) effect = nil animator?.addAnimations { [weak self] in self?.effect = self?.visualEffect } - + animator?.fractionComplete = sensivity } - + func setupSensivity(sensivity: Double) { animator?.fractionComplete = sensivity } - + func addObservers() { NotificationCenter.default .notifications(named: UIApplication.willEnterForegroundNotification, object: nil) diff --git a/CommonKit/Sources/CommonKit/Helpers/SwiftUI/NavigationButton.swift b/CommonKit/Sources/CommonKit/Helpers/SwiftUI/NavigationButton.swift index c9379afe1..44cf97c69 100644 --- a/CommonKit/Sources/CommonKit/Helpers/SwiftUI/NavigationButton.swift +++ b/CommonKit/Sources/CommonKit/Helpers/SwiftUI/NavigationButton.swift @@ -11,7 +11,7 @@ import SwiftUI public struct NavigationButton: View { private let action: () -> Void private let content: () -> Content - + public var body: some View { Button(action: action) { HStack { @@ -22,7 +22,7 @@ public struct NavigationButton: View { } }.buttonStyle(.plain) } - + public init(action: @escaping () -> Void, content: @escaping () -> Content) { self.action = action self.content = content diff --git a/CommonKit/Sources/CommonKit/Helpers/SwiftUI/SafariWebView.swift b/CommonKit/Sources/CommonKit/Helpers/SwiftUI/SafariWebView.swift index 53d9c4eb8..7d4e6f009 100644 --- a/CommonKit/Sources/CommonKit/Helpers/SwiftUI/SafariWebView.swift +++ b/CommonKit/Sources/CommonKit/Helpers/SwiftUI/SafariWebView.swift @@ -6,19 +6,19 @@ // Copyright © 2023 Adamant. All rights reserved. // -import SwiftUI import SafariServices +import SwiftUI public struct SafariWebView: UIViewControllerRepresentable { public let url: URL - + public init(url: URL) { self.url = url } - + public func makeUIViewController(context: Context) -> SFSafariViewController { .init(url: url) } - + public func updateUIViewController(_: SFSafariViewController, context _: Context) {} } diff --git a/CommonKit/Sources/CommonKit/Helpers/SwiftUI/UIViewControllerWrapper.swift b/CommonKit/Sources/CommonKit/Helpers/SwiftUI/UIViewControllerWrapper.swift index 4bbfe957d..8bf3fe1a9 100644 --- a/CommonKit/Sources/CommonKit/Helpers/SwiftUI/UIViewControllerWrapper.swift +++ b/CommonKit/Sources/CommonKit/Helpers/SwiftUI/UIViewControllerWrapper.swift @@ -1,6 +1,6 @@ // // UIViewControllerWrapper.swift -// +// // // Created by Stanislav Jelezoglo on 23.06.2023. // @@ -9,14 +9,14 @@ import SwiftUI public struct UIViewControllerWrapper: UIViewControllerRepresentable { private let wrappedViewController: T - + public init(_ wrappedViewController: T) { self.wrappedViewController = wrappedViewController } - + public func makeUIViewController(context: Context) -> T { return wrappedViewController } - - public func updateUIViewController(_ uiViewController: T, context: Context) { } + + public func updateUIViewController(_ uiViewController: T, context: Context) {} } diff --git a/CommonKit/Sources/CommonKit/Helpers/SwiftUI/UIViewWrapper.swift b/CommonKit/Sources/CommonKit/Helpers/SwiftUI/UIViewWrapper.swift index 045283177..f3dd8c325 100644 --- a/CommonKit/Sources/CommonKit/Helpers/SwiftUI/UIViewWrapper.swift +++ b/CommonKit/Sources/CommonKit/Helpers/SwiftUI/UIViewWrapper.swift @@ -1,6 +1,6 @@ // // UIViewWrapper.swift -// +// // // Created by Stanislav Jelezoglo on 01.08.2023. // @@ -10,13 +10,13 @@ import UIKit public struct UIViewWrapper: UIViewRepresentable { public let view: UIView - + public func makeUIView(context: Context) -> UIView { return view } - - public func updateUIView(_ uiView: UIView, context: Context) { } - + + public func updateUIView(_ uiView: UIView, context: Context) {} + public init(view: UIView) { self.view = view } diff --git a/CommonKit/Sources/CommonKit/Helpers/SwiftUI/View+Extension.swift b/CommonKit/Sources/CommonKit/Helpers/SwiftUI/View+Extension.swift index 28c871e56..4734602e2 100644 --- a/CommonKit/Sources/CommonKit/Helpers/SwiftUI/View+Extension.swift +++ b/CommonKit/Sources/CommonKit/Helpers/SwiftUI/View+Extension.swift @@ -8,45 +8,47 @@ import SwiftUI -public extension Axis.Set { - static var all: Axis.Set { .init([.vertical, .horizontal]) } +extension Axis.Set { + public static var all: Axis.Set { .init([.vertical, .horizontal]) } } -public extension View { - func frame(squareSize: CGFloat, alignment: Alignment = .center) -> some View { +extension View { + public func frame(squareSize: CGFloat, alignment: Alignment = .center) -> some View { frame(width: squareSize, height: squareSize, alignment: alignment) } - - func eraseToAnyView() -> AnyView { + + public func eraseToAnyView() -> AnyView { .init(self) } - - func expanded( + + public func expanded( axes: Axis.Set = .all, alignment: Alignment = .center ) -> some View { var resultView = eraseToAnyView() if axes.contains(.vertical) { - resultView = resultView + resultView = + resultView .frame(maxHeight: .infinity, alignment: alignment) .eraseToAnyView() } if axes.contains(.horizontal) { - resultView = resultView + resultView = + resultView .frame(maxWidth: .infinity, alignment: alignment) .eraseToAnyView() } return resultView } - + // TODO: Remove this function (or fix) - func fullScreen() -> some View { + public func fullScreen() -> some View { return frame(width: .infinity, height: .infinity) .ignoresSafeArea() } - + @ViewBuilder - func withoutListBackground() -> some View { + public func withoutListBackground() -> some View { if #available(iOS 16.0, *) { self.scrollContentBackground(.hidden) } else { diff --git a/CommonKit/Sources/CommonKit/Helpers/Task+Extension.swift b/CommonKit/Sources/CommonKit/Helpers/Task+Extension.swift index 8f9228475..bf2761ca7 100644 --- a/CommonKit/Sources/CommonKit/Helpers/Task+Extension.swift +++ b/CommonKit/Sources/CommonKit/Helpers/Task+Extension.swift @@ -6,29 +6,29 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit import Combine +import UIKit -public extension Task { - func eraseToAnyCancellable() -> AnyCancellable { +extension Task { + public func eraseToAnyCancellable() -> AnyCancellable { .init(cancel) } - - func store(in collection: inout C) where C: RangeReplaceableCollection, C.Element == AnyCancellable { + + public func store(in collection: inout C) where C: RangeReplaceableCollection, C.Element == AnyCancellable { eraseToAnyCancellable().store(in: &collection) } - func store(in set: inout Set) { + public func store(in set: inout Set) { eraseToAnyCancellable().store(in: &set) } } -public extension Task where Success == Never, Failure == Never { - static func sleep(interval: TimeInterval, pauseInBackground: Bool = false) async throws { +extension Task where Success == Never, Failure == Never { + public static func sleep(interval: TimeInterval, pauseInBackground: Bool = false) async throws { guard pauseInBackground else { return try await Task.sleep(nanoseconds: UInt64(interval * 1_000_000_000)) } - + let startDate = Date.now let counter = BackgroundTimeCounter() await counter.start() @@ -38,14 +38,14 @@ public extension Task where Success == Never, Failure == Never { let endDate = Date.now let actualInterval = endDate.timeIntervalSince(startDate) let additionalInterval = timeSpentInBackground + interval - actualInterval - + guard additionalInterval > .zero else { return } try await Task.sleep(interval: additionalInterval, pauseInBackground: true) } - + /// Avoid using it. It lowers performance due to changing threads. @discardableResult - static func sync(_ action: @Sendable @escaping () async -> T) -> T { + public static func sync(_ action: @Sendable @escaping () async -> T) -> T { _sync(action) } } @@ -54,12 +54,12 @@ public extension Task where Success == Never, Failure == Never { private func _sync(_ action: @Sendable @escaping () async -> T) -> T { var result: T? let semaphore = DispatchSemaphore(value: .zero) - + Task { result = await action() semaphore.signal() } - + semaphore.wait() return result! } @@ -69,43 +69,43 @@ private actor BackgroundTimeCounter { private var subscriptions = Set() private var backgroundEnteringDate: Date = .adamantNullDate private var isInBackground = false - + var total: TimeInterval? { guard !subscriptions.isEmpty else { return nil } - + return isInBackground ? _total + Date.now.timeIntervalSince(backgroundEnteringDate) : _total } - + func start() { Task { backgroundEnteringDate = .now isInBackground = await UIApplication.shared.applicationState == .background - + NotificationCenter.default .notifications(named: UIApplication.didBecomeActiveNotification) .sink { [weak self] _ in await self?.didBecomeActive() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: UIApplication.didEnterBackgroundNotification) .sink { [weak self] _ in await self?.didEnterBeckground() } .store(in: &subscriptions) } } - + func stopAndReset() { _total = .zero subscriptions = .init() backgroundEnteringDate = .adamantNullDate } - + private func didEnterBeckground() { backgroundEnteringDate = .now isInBackground = true } - + private func didBecomeActive() { _total += Date.now.timeIntervalSince(backgroundEnteringDate) isInBackground = false diff --git a/CommonKit/Sources/CommonKit/Helpers/TimeInterval+Extension.swift b/CommonKit/Sources/CommonKit/Helpers/TimeInterval+Extension.swift index f4db942b0..df4e019ff 100644 --- a/CommonKit/Sources/CommonKit/Helpers/TimeInterval+Extension.swift +++ b/CommonKit/Sources/CommonKit/Helpers/TimeInterval+Extension.swift @@ -8,8 +8,8 @@ import Foundation -public extension TimeInterval { - init(milliseconds: Int) { +extension TimeInterval { + public init(milliseconds: Int) { self.init(Double(milliseconds) / 1000) } } diff --git a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/CGSize+adamant.swift b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/CGSize+adamant.swift index bed51f6b8..a299e081e 100644 --- a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/CGSize+adamant.swift +++ b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/CGSize+adamant.swift @@ -8,8 +8,8 @@ import CoreGraphics -public extension CGSize { - init(squareSize: CGFloat) { +extension CGSize { + public init(squareSize: CGFloat) { self.init(width: squareSize, height: squareSize) } } diff --git a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/CollectionCellWrapper.swift b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/CollectionCellWrapper.swift index e61bb7618..ff8185b23 100644 --- a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/CollectionCellWrapper.swift +++ b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/CollectionCellWrapper.swift @@ -6,29 +6,29 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit import SnapKit +import UIKit public final class CollectionCellWrapper: UICollectionViewCell { public let wrappedView = View() - + public override init(frame: CGRect) { super.init(frame: frame) configure() } - + required init?(coder: NSCoder) { super.init(coder: coder) configure() } - + public override func prepareForReuse() { wrappedView.prepareForReuse() } } -private extension CollectionCellWrapper { - func configure() { +extension CollectionCellWrapper { + fileprivate func configure() { contentView.addSubview(wrappedView) wrappedView.snp.makeConstraints { $0.directionalEdges.equalToSuperview() diff --git a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/KeyboardObservingViewController.swift b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/KeyboardObservingViewController.swift index 4ba17e1fd..7eb6781f3 100644 --- a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/KeyboardObservingViewController.swift +++ b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/KeyboardObservingViewController.swift @@ -6,64 +6,63 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit import Combine +import UIKit open class KeyboardObservingViewController: UIViewController { private var subscription: AnyCancellable? private var keyboardFrame: CGRect = .zero - + open override func viewDidLoad() { super.viewDidLoad() subscription = makeKeyboardSubscription() } - + open override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() additionalSafeAreaInsets.bottom = getAdditionalBottomInset(keyboardFrame: keyboardFrame) } } -private extension KeyboardObservingViewController { - func getAdditionalBottomInset(keyboardFrame: CGRect) -> CGFloat { +extension KeyboardObservingViewController { + fileprivate func getAdditionalBottomInset(keyboardFrame: CGRect) -> CGFloat { let keyboardFrameInView = view.convert(keyboardFrame, from: nil) - + let safeAreaFrame = view.safeAreaLayoutGuide.layoutFrame.insetBy( dx: .zero, dy: -additionalSafeAreaInsets.bottom ) - + return safeAreaFrame.intersection(keyboardFrameInView).height } - - func makeKeyboardSubscription() -> AnyCancellable { + + fileprivate func makeKeyboardSubscription() -> AnyCancellable { NotificationCenter.default .notifications(named: UIResponder.keyboardWillChangeFrameNotification, object: nil) .sink { @MainActor [weak self] in self?.onKeyboardFrameChange($0) } } - - func onKeyboardFrameChange(_ notification: Notification) { + + fileprivate func onKeyboardFrameChange(_ notification: Notification) { guard let userInfo = notification.userInfo, let keyboardFrameInfo = userInfo[UIResponder.keyboardFrameEndUserInfoKey], let keyboardFrame = (keyboardFrameInfo as? NSValue)?.cgRectValue else { return } - + self.keyboardFrame = keyboardFrame - - let animationDuration: TimeInterval = ( - notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] - as? NSNumber - )?.doubleValue ?? .zero - - let animationCurveRawNSN = notification + + let animationDuration: TimeInterval = + (notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] + as? NSNumber)?.doubleValue ?? .zero + + let animationCurveRawNSN = + notification .userInfo?[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber - - let animationCurveRaw = animationCurveRawNSN?.uintValue ?? - UIView.AnimationOptions.curveEaseInOut.rawValue - + + let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIView.AnimationOptions.curveEaseInOut.rawValue + let animationCurve = UIView.AnimationOptions(rawValue: animationCurveRaw) - + UIView.animate( withDuration: animationDuration, delay: .zero, diff --git a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/TableCellWrapper.swift b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/TableCellWrapper.swift index 96a1885fb..316c2653d 100644 --- a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/TableCellWrapper.swift +++ b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/TableCellWrapper.swift @@ -6,29 +6,29 @@ // Copyright © 2023 Adamant. All rights reserved. // -import UIKit import SnapKit +import UIKit public final class TableCellWrapper: UITableViewCell { public let wrappedView = View() - + public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) configure() } - + required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) configure() } - + public override func prepareForReuse() { wrappedView.prepareForReuse() } } -private extension TableCellWrapper { - func configure() { +extension TableCellWrapper { + fileprivate func configure() { contentView.addSubview(wrappedView) wrappedView.snp.makeConstraints { $0.directionalEdges.equalToSuperview() diff --git a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/TransparentWindow.swift b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/TransparentWindow.swift index bafae35c8..0545531ab 100644 --- a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/TransparentWindow.swift +++ b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/TransparentWindow.swift @@ -1,6 +1,6 @@ // // TransparentWindow.swift -// +// // // Created by Andrey Golubenko on 06.12.2022. // @@ -20,7 +20,7 @@ public final class TransparentWindow: UIWindow { /// we therefor still return the default hit test result, but only if the tap was detected within the bounds of the _deepest view_ if #available(iOS 18, *) { guard let view = rootViewController?.view else { return false } - + let hit = Self._hitTest( point, with: event, @@ -28,13 +28,13 @@ public final class TransparentWindow: UIWindow { /// not advisable when added subviews are potentially non-interactive, as `rootViewController?.view` itself is part of `self.subviews`, and therefor participates in hit testing view: subviews.count > 1 ? self : view ) - + return hit != nil } else { return super.point(inside: point, with: event) } } - + public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if #available(iOS 18, *) { return super.hitTest(point, with: event) @@ -45,44 +45,45 @@ public final class TransparentWindow: UIWindow { } } -private extension TransparentWindow { - static func _hitTest( +extension TransparentWindow { + fileprivate static func _hitTest( _ point: CGPoint, with event: UIEvent?, view: UIView, depth: Int = .zero ) -> (view: UIView, depth: Int)? { var deepest: (view: UIView, depth: Int)? - + /// views are ordered back-to-front for subview in view.subviews.reversed() { let converted = view.convert(point, to: subview) - + guard subview.isUserInteractionEnabled, !subview.isHidden, subview.alpha > .zero, subview.point(inside: converted, with: event) else { continue } - - let result = if let hit = _hitTest( - converted, - with: event, - view: subview, - depth: depth + 1 - ) { - hit - } else { - (view: subview, depth: depth) - } - + + let result = + if let hit = _hitTest( + converted, + with: event, + view: subview, + depth: depth + 1 + ) { + hit + } else { + (view: subview, depth: depth) + } + if case .none = deepest { deepest = result } else if let current = deepest, result.depth > current.depth { deepest = result } } - + return deepest } } diff --git a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIAlertController+Extension.swift b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIAlertController+Extension.swift index 251302e53..43744392f 100644 --- a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIAlertController+Extension.swift +++ b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIAlertController+Extension.swift @@ -1,30 +1,31 @@ // // UIAlertController+Extension.swift -// +// // // Created by Andrey Golubenko on 23.08.2023. // import UIKit -public extension UIAlertController { - enum SourceView { +extension UIAlertController { + public enum SourceView { case view(UIView) case barButtonItem(UIBarButtonItem) } - - convenience init( + + public convenience init( title: String?, message: String?, preferredStyleSafe: UIAlertController.Style, source: SourceView? ) { - let style = source == nil && UIScreen.main.traitCollection.userInterfaceIdiom == .pad + let style = + source == nil && UIScreen.main.traitCollection.userInterfaceIdiom == .pad ? .alert : preferredStyleSafe - + self.init(title: title, message: message, preferredStyle: style) - + switch source { case let .view(view): popoverPresentationController?.sourceView = view diff --git a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIColor+adamant.swift b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIColor+adamant.swift index c775b2100..92b8bf5bc 100644 --- a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIColor+adamant.swift +++ b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIColor+adamant.swift @@ -10,7 +10,7 @@ import UIKit extension UIColor { public struct adamant { - + public static func returnColorByTheme( colorWhiteTheme: UIColor, colorDarkTheme: UIColor @@ -19,257 +19,261 @@ extension UIColor { return traits.userInterfaceStyle == .dark ? colorDarkTheme : colorWhiteTheme } } - + // MARK: Colors from Adamant Guideline - public static let active = #colorLiteral(red: 0.09019607843, green: 0.6117647059, blue: 0.9215686275, alpha: 1) //#179CEB - public static let attention = #colorLiteral(red: 0.9902971387, green: 0.6896653175, blue: 0.4256819189, alpha: 1) //#faa05a - public static let success = #colorLiteral(red: 0.2102436721, green: 0.8444728255, blue: 0.6537195444, alpha: 1) //#32d296 - public static let warning = #colorLiteral(red: 0.9622407556, green: 0.4130832553, blue: 0.5054324269, alpha: 1) //#f0506e - public static let inactive = #colorLiteral(red: 0.5025414228, green: 0.5106091499, blue: 0.5218499899, alpha: 1) //#6d6f72 - + public static let active = #colorLiteral(red: 0.09019607843, green: 0.6117647059, blue: 0.9215686275, alpha: 1) //#179CEB + public static let attention = #colorLiteral(red: 0.9902971387, green: 0.6896653175, blue: 0.4256819189, alpha: 1) //#faa05a + public static let success = #colorLiteral(red: 0.2102436721, green: 0.8444728255, blue: 0.6537195444, alpha: 1) //#32d296 + public static let warning = #colorLiteral(red: 0.9622407556, green: 0.4130832553, blue: 0.5054324269, alpha: 1) //#f0506e + public static let inactive = #colorLiteral(red: 0.5025414228, green: 0.5106091499, blue: 0.5218499899, alpha: 1) //#6d6f72 + public static let imageBackground = #colorLiteral(red: 0.8470588235, green: 0.8470588235, blue: 0.8470588235, alpha: 1) + public static let imageBlack = #colorLiteral(red: 0.0117647059, green: 0.0039215686, blue: 0.0156862745, alpha: 1) + public static var background: UIColor { - let colorWhiteTheme = #colorLiteral(red: 0.9590962529, green: 0.9721178412, blue: 0.9845080972, alpha: 1) //f2f6fa - let colorDarkTheme = #colorLiteral(red: 0.1462407112, green: 0.1462407112, blue: 0.1462407112, alpha: 1) //1c1c1c + let colorWhiteTheme = #colorLiteral(red: 0.9590962529, green: 0.9721178412, blue: 0.9845080972, alpha: 1) //f2f6fa + let colorDarkTheme = #colorLiteral(red: 0.1462407112, green: 0.1462407112, blue: 0.1462407112, alpha: 1) //1c1c1c return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + // MARK: Global colors - + /// Income Arrow View Background Color public static var incomeArrowBackgroundColor: UIColor { - return #colorLiteral(red: 0.2381577492, green: 0.7938874364, blue: 0.2725245357, alpha: 1) //36C436 + return #colorLiteral(red: 0.2381577492, green: 0.7938874364, blue: 0.2725245357, alpha: 1) //36C436 } - + /// Outcome Arrow View Background Color public static var outcomeArrowBackgroundColor: UIColor { - return #colorLiteral(red: 0.9752754569, green: 0.3635693789, blue: 0.3339065611, alpha: 1) //F44444 + return #colorLiteral(red: 0.9752754569, green: 0.3635693789, blue: 0.3339065611, alpha: 1) //F44444 } - + /// Default background color public static var backgroundColor: UIColor { - let colorWhiteTheme = UIColor.white - let colorDarkTheme = #colorLiteral(red: 0.1726317406, green: 0.1726317406, blue: 0.1726317406, alpha: 1) //212121 + let colorWhiteTheme = UIColor.white + let colorDarkTheme = #colorLiteral(red: 0.1726317406, green: 0.1726317406, blue: 0.1726317406, alpha: 1) //212121 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + /// Second default background color public static var secondBackgroundColor: UIColor { - let colorWhiteTheme = #colorLiteral(red: 0.9594989419, green: 0.956831634, blue: 0.9719926715, alpha: 1) //f2f1f6 - let colorDarkTheme = #colorLiteral(red: 0.1725490196, green: 0.1725490196, blue: 0.1725490196, alpha: 1) //2C2C2C + let colorWhiteTheme = #colorLiteral(red: 0.9594989419, green: 0.956831634, blue: 0.9719926715, alpha: 1) //f2f1f6 + let colorDarkTheme = #colorLiteral(red: 0.1725490196, green: 0.1725490196, blue: 0.1725490196, alpha: 1) //2C2C2C return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + /// Welcome background color public static var welcomeBackgroundColor: UIColor { - let colorWhiteTheme = UIColor(patternImage: .asset(named: "stripeBg") ?? .init()) - let colorDarkTheme = #colorLiteral(red: 0.1294117647, green: 0.1294117647, blue: 0.1294117647, alpha: 1) //212121 + let colorWhiteTheme = UIColor(patternImage: .asset(named: "stripeBg") ?? .init()) + let colorDarkTheme = #colorLiteral(red: 0.1294117647, green: 0.1294117647, blue: 0.1294117647, alpha: 1) //212121 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } /// Default text color public static var textColor: UIColor { - let colorWhiteTheme = UIColor.black - let colorDarkTheme = UIColor.white + let colorWhiteTheme = UIColor.black + let colorDarkTheme = UIColor.white return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + /// Default cell alert text color public static var cellAlertTextColor: UIColor { - let colorWhiteTheme = UIColor.white - let colorDarkTheme = #colorLiteral(red: 0.1294117647, green: 0.1294117647, blue: 0.1294117647, alpha: 1) //212121 + let colorWhiteTheme = UIColor.white + let colorDarkTheme = #colorLiteral(red: 0.1294117647, green: 0.1294117647, blue: 0.1294117647, alpha: 1) //212121 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + /// Default cell color public static var cellColor: UIColor { guard !isMacOS else { return .secondarySystemGroupedBackground } - let colorWhiteTheme = UIColor.white - let colorDarkTheme = #colorLiteral(red: 0.1098039216, green: 0.1098039216, blue: 0.1137254902, alpha: 1) //1c1c1d + let colorWhiteTheme = UIColor.white + let colorDarkTheme = #colorLiteral(red: 0.1098039216, green: 0.1098039216, blue: 0.1137254902, alpha: 1) //1c1c1d return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + /// Code block background color public static var codeBlock: UIColor { let colorWhiteTheme = UIColor(hex: "#4a4a4a").withAlphaComponent(0.1) - let colorDarkTheme = #colorLiteral(red: 0.1647058824, green: 0.1647058824, blue: 0.168627451, alpha: 1) //2a2a2b + let colorDarkTheme = #colorLiteral(red: 0.1647058824, green: 0.1647058824, blue: 0.168627451, alpha: 1) //2a2a2b return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + /// Code block text color public static var codeBlockText: UIColor { - let colorWhiteTheme = #colorLiteral(red: 0.3215686275, green: 0.3215686275, blue: 0.3215686275, alpha: 1) //525252 + let colorWhiteTheme = #colorLiteral(red: 0.3215686275, green: 0.3215686275, blue: 0.3215686275, alpha: 1) //525252 let colorDarkTheme = UIColor.white.withAlphaComponent(0.8) return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + /// Reactions background color public static var reactionsBackground: UIColor { - let colorWhiteTheme = #colorLiteral(red: 0.9490196078, green: 0.9490196078, blue: 0.9490196078, alpha: 1) //F2F2F2 - let colorDarkTheme = #colorLiteral(red: 0.262745098, green: 0.262745098, blue: 0.262745098, alpha: 1) //434343 + let colorWhiteTheme = #colorLiteral(red: 0.9490196078, green: 0.9490196078, blue: 0.9490196078, alpha: 1) //F2F2F2 + let colorDarkTheme = #colorLiteral(red: 0.262745098, green: 0.262745098, blue: 0.262745098, alpha: 1) //434343 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + /// More reactions background button color public static var moreReactionsBackground: UIColor { let colorWhiteTheme = UIColor.white - let colorDarkTheme = #colorLiteral(red: 0.2196078431, green: 0.2196078431, blue: 0.2196078431, alpha: 1) //383838 + let colorDarkTheme = #colorLiteral(red: 0.2196078431, green: 0.2196078431, blue: 0.2196078431, alpha: 1) //383838 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + /// Picked reaction background color public static var pickedReactionBackground: UIColor { - let colorWhiteTheme = UIColor(hex: "#EBECED").withAlphaComponent(0.85) - let colorDarkTheme = #colorLiteral(red: 0.3294117647, green: 0.3294117647, blue: 0.3294117647, alpha: 1) //545454 + let colorWhiteTheme = UIColor(hex: "#EBECED").withAlphaComponent(0.85) + let colorDarkTheme = #colorLiteral(red: 0.3294117647, green: 0.3294117647, blue: 0.3294117647, alpha: 1) //545454 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + /// Main dark gray, ~70% gray public static var primary: UIColor { - let colorWhiteTheme = #colorLiteral(red: 0.2901960784, green: 0.2901960784, blue: 0.2901960784, alpha: 1) //4A4A4A + let colorWhiteTheme = #colorLiteral(red: 0.2901960784, green: 0.2901960784, blue: 0.2901960784, alpha: 1) //4A4A4A let colorDarkTheme = UIColor.white return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + /// Secondary color, ~50% gray public static var secondary: UIColor { - let colorWhiteTheme = #colorLiteral(red: 0.4784313725, green: 0.4784313725, blue: 0.4784313725, alpha: 1) //7A7A7A - let colorDarkTheme = #colorLiteral(red: 0.8784313725, green: 0.8784313725, blue: 0.8784313725, alpha: 1) //E0E0E0 + let colorWhiteTheme = #colorLiteral(red: 0.4784313725, green: 0.4784313725, blue: 0.4784313725, alpha: 1) //7A7A7A + let colorDarkTheme = #colorLiteral(red: 0.8784313725, green: 0.8784313725, blue: 0.8784313725, alpha: 1) //E0E0E0 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + /// Chat icons color, ~40% gray public static var chatIcons: UIColor { - let colorWhiteTheme = #colorLiteral(red: 0.6196078431, green: 0.6196078431, blue: 0.6196078431, alpha: 1) //9E9E9E - let colorDarkTheme = #colorLiteral(red: 0.2784313725, green: 0.2784313725, blue: 0.2784313725, alpha: 1) //474747 + let colorWhiteTheme = #colorLiteral(red: 0.6196078431, green: 0.6196078431, blue: 0.6196078431, alpha: 1) //9E9E9E + let colorDarkTheme = #colorLiteral(red: 0.2784313725, green: 0.2784313725, blue: 0.2784313725, alpha: 1) //474747 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + /// Table row icons color, ~45% gray public static var tableRowIcons: UIColor { let colorWhiteTheme = UIColor(red: 0.45, green: 0.45, blue: 0.45, alpha: 1) - let colorDarkTheme = UIColor(red: 0.878, green: 0.878, blue: 0.878, alpha: 1) + let colorDarkTheme = UIColor(red: 0.878, green: 0.878, blue: 0.878, alpha: 1) return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + /// Chat list, swipe color public static var swipeMoreColor: UIColor { - let colorWhiteTheme = #colorLiteral(red: 0.8784313725, green: 0.8784313725, blue: 0.8784313725, alpha: 1) //E0E0E0 - let colorDarkTheme = #colorLiteral(red: 0.3294117647, green: 0.3294117647, blue: 0.3294117647, alpha: 1) //545454 + let colorWhiteTheme = #colorLiteral(red: 0.8784313725, green: 0.8784313725, blue: 0.8784313725, alpha: 1) //E0E0E0 + let colorDarkTheme = #colorLiteral(red: 0.3294117647, green: 0.3294117647, blue: 0.3294117647, alpha: 1) //545454 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + public static var swipeBlockColor: UIColor { - let colorWhiteTheme = #colorLiteral(red: 0.9254901961, green: 0.9254901961, blue: 0.9254901961, alpha: 1) //ECECEC - let colorDarkTheme = #colorLiteral(red: 0.2705882353, green: 0.2705882353, blue: 0.2705882353, alpha: 1) //474747 + let colorWhiteTheme = #colorLiteral(red: 0.9254901961, green: 0.9254901961, blue: 0.9254901961, alpha: 1) //ECECEC + let colorDarkTheme = #colorLiteral(red: 0.2705882353, green: 0.2705882353, blue: 0.2705882353, alpha: 1) //474747 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + /// Switch onTintColor public static var switchColor: UIColor { - let colorWhiteTheme = #colorLiteral(red: 0.09019607843, green: 0.6117647059, blue: 0.9254901961, alpha: 1) //179cec - let colorDarkTheme = #colorLiteral(red: 0.01960784314, green: 0.2705882353, blue: 0.4196078431, alpha: 1) //05456b + let colorWhiteTheme = #colorLiteral(red: 0.09019607843, green: 0.6117647059, blue: 0.9254901961, alpha: 1) //179cec + let colorDarkTheme = #colorLiteral(red: 0.01960784314, green: 0.2705882353, blue: 0.4196078431, alpha: 1) //05456b return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + /// Secondary color, ~50% gray public static var errorOkButton: UIColor { - let colorWhiteTheme = #colorLiteral(red: 0.4784313725, green: 0.4784313725, blue: 0.4784313725, alpha: 1) //7A7A7A - let colorDarkTheme = #colorLiteral(red: 0.3098039216, green: 0.3098039216, blue: 0.3098039216, alpha: 1) //4F4F4F + let colorWhiteTheme = #colorLiteral(red: 0.4784313725, green: 0.4784313725, blue: 0.4784313725, alpha: 1) //7A7A7A + let colorDarkTheme = #colorLiteral(red: 0.3098039216, green: 0.3098039216, blue: 0.3098039216, alpha: 1) //4F4F4F return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + // MARK: Chat colors - + /// User chat bubble background, ~4% gray public static var chatRecipientBackground: UIColor { - let colorWhiteTheme = #colorLiteral(red: 0.9647058824, green: 0.9725490196, blue: 0.9843137255, alpha: 1) //F6F8FB - let colorDarkTheme = #colorLiteral(red: 0.2705882353, green: 0.2705882353, blue: 0.2705882353, alpha: 1) //454545 + let colorWhiteTheme = #colorLiteral(red: 0.9647058824, green: 0.9725490196, blue: 0.9843137255, alpha: 1) //F6F8FB + let colorDarkTheme = #colorLiteral(red: 0.2705882353, green: 0.2705882353, blue: 0.2705882353, alpha: 1) //454545 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + public static var pendingChatBackground: UIColor { - let colorWhiteTheme = UIColor(white: 0.98, alpha: 1.0) - let colorDarkTheme = #colorLiteral(red: 0.4196078431, green: 0.4196078431, blue: 0.4196078431, alpha: 1) //6B6B6B + let colorWhiteTheme = UIColor(white: 0.98, alpha: 1.0) + let colorDarkTheme = #colorLiteral(red: 0.4196078431, green: 0.4196078431, blue: 0.4196078431, alpha: 1) //6B6B6B return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + public static var failChatBackground: UIColor { - let colorWhiteTheme = UIColor(white: 0.8, alpha: 1.0) - let colorDarkTheme = #colorLiteral(red: 0.4588235294, green: 0.4588235294, blue: 0.4588235294, alpha: 1) //757575 + let colorWhiteTheme = UIColor(white: 0.8, alpha: 1.0) + let colorDarkTheme = #colorLiteral(red: 0.4588235294, green: 0.4588235294, blue: 0.4588235294, alpha: 1) //757575 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + /// Partner chat bubble background, ~8% gray public static var chatSenderBackground: UIColor { - let colorWhiteTheme = #colorLiteral(red: 0.9254901961, green: 0.9254901961, blue: 0.9254901961, alpha: 1) //ECECEC - let colorDarkTheme = #colorLiteral(red: 0.2117647059, green: 0.2117647059, blue: 0.2117647059, alpha: 1) //363636 + let colorWhiteTheme = #colorLiteral(red: 0.9254901961, green: 0.9254901961, blue: 0.9254901961, alpha: 1) //ECECEC + let colorDarkTheme = #colorLiteral(red: 0.2117647059, green: 0.2117647059, blue: 0.2117647059, alpha: 1) //363636 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + /// Partner chat bubble background, ~8% gray public static var chatInputBarBackground: UIColor { - let colorWhiteTheme = #colorLiteral(red: 0.968627451, green: 0.968627451, blue: 0.968627451, alpha: 1) //F7F7F7 - let colorDarkTheme = #colorLiteral(red: 0.2, green: 0.2, blue: 0.2, alpha: 1) //333333 + let colorWhiteTheme = #colorLiteral(red: 0.968627451, green: 0.968627451, blue: 0.968627451, alpha: 1) //F7F7F7 + let colorDarkTheme = #colorLiteral(red: 0.2, green: 0.2, blue: 0.2, alpha: 1) //333333 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + /// InputBar field background, ~8% gray public static var chatInputFieldBarBackground: UIColor { - let colorWhiteTheme = UIColor.white - let colorDarkTheme = #colorLiteral(red: 0.1294117647, green: 0.1294117647, blue: 0.1294117647, alpha: 1) //212121 + let colorWhiteTheme = UIColor.white + let colorDarkTheme = #colorLiteral(red: 0.1294117647, green: 0.1294117647, blue: 0.1294117647, alpha: 1) //212121 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + /// Border colors for readOnly mode public static var disableBorderColor: UIColor { - let colorWhiteTheme = #colorLiteral(red: 0.6901960784, green: 0.6901960784, blue: 0.6901960784, alpha: 1) //B0B0B0 - let colorDarkTheme = #colorLiteral(red: 0.5294117647, green: 0.5294117647, blue: 0.5294117647, alpha: 1) //878787 + let colorWhiteTheme = #colorLiteral(red: 0.6901960784, green: 0.6901960784, blue: 0.6901960784, alpha: 1) //B0B0B0 + let colorDarkTheme = #colorLiteral(red: 0.5294117647, green: 0.5294117647, blue: 0.5294117647, alpha: 1) //878787 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - - public static let chatInputBarBorderColor = #colorLiteral(red: 0.7843137255, green: 0.7843137255, blue: 0.7843137255, alpha: 1) //C8C8C8 - + + public static let chatInputBarBorderColor = #colorLiteral(red: 0.7843137255, green: 0.7843137255, blue: 0.7843137255, alpha: 1) //C8C8C8 + /// Color of input bar placeholder - public static let chatPlaceholderTextColor = #colorLiteral(red: 0.6, green: 0.6, blue: 0.6, alpha: 1) //999999 - + public static let chatPlaceholderTextColor = #colorLiteral(red: 0.6, green: 0.6, blue: 0.6, alpha: 1) //999999 + // MARK: Context Menu - + public static var contextMenuLineColor: UIColor { - let colorWhiteTheme = UIColor(hex: "#BFBFBF").withAlphaComponent(0.8) - let colorDarkTheme = UIColor(hex: "#808080").withAlphaComponent(0.8) + let colorWhiteTheme = UIColor(hex: "#BFBFBF").withAlphaComponent(0.8) + let colorDarkTheme = UIColor(hex: "#808080").withAlphaComponent(0.8) return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + public static var contextMenuSelectColor: UIColor { - let colorWhiteTheme = UIColor.black.withAlphaComponent(0.10) - let colorDarkTheme = #colorLiteral(red: 0.2156862745, green: 0.2156862745, blue: 0.2156862745, alpha: 1) //#373737 + let colorWhiteTheme = UIColor.black.withAlphaComponent(0.10) + let colorDarkTheme = #colorLiteral(red: 0.2156862745, green: 0.2156862745, blue: 0.2156862745, alpha: 1) //#373737 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + public static var contextMenuDefaultBackgroundColor: UIColor { - let colorWhiteTheme = #colorLiteral(red: 0.9490196078, green: 0.9490196078, blue: 0.9490196078, alpha: 1) //#F2F2F2 - let colorDarkTheme = #colorLiteral(red: 0.262745098, green: 0.262745098, blue: 0.262745098, alpha: 1) //#434343 + let colorWhiteTheme = #colorLiteral(red: 0.9490196078, green: 0.9490196078, blue: 0.9490196078, alpha: 1) //#F2F2F2 + let colorDarkTheme = #colorLiteral(red: 0.262745098, green: 0.262745098, blue: 0.262745098, alpha: 1) //#434343 return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + public static var contextMenuOverlayMacColor: UIColor { - let colorWhiteTheme = UIColor.black.withAlphaComponent(0.3) - let colorDarkTheme = UIColor.white.withAlphaComponent(0.3) + let colorWhiteTheme = UIColor.black.withAlphaComponent(0.3) + let colorDarkTheme = UIColor.white.withAlphaComponent(0.3) return returnColorByTheme(colorWhiteTheme: colorWhiteTheme, colorDarkTheme: colorDarkTheme) } - + public static var contextMenuDestructive: UIColor { - #colorLiteral(red: 1, green: 0.2196078431, blue: 0.137254902, alpha: 1) //#FF3823 + #colorLiteral(red: 1, green: 0.2196078431, blue: 0.137254902, alpha: 1) //#FF3823 } - + // MARK: Pinpad /// Pinpad highligh button background, 12% gray - public static let pinpadHighlightButton = #colorLiteral(red: 0.8823529412, green: 0.8823529412, blue: 0.8823529412, alpha: 1) //#E1E1E1 - + public static let pinpadHighlightButton = #colorLiteral(red: 0.8823529412, green: 0.8823529412, blue: 0.8823529412, alpha: 1) //#E1E1E1 + // MARK: Transfers /// Income transfer icon background, light green - public static let transferIncomeIconBackground = #colorLiteral(red: 0.7019607843, green: 0.9294117647, blue: 0.5490196078, alpha: 1) //#B3ED8C - + public static let transferIncomeIconBackground = #colorLiteral(red: 0.7019607843, green: 0.9294117647, blue: 0.5490196078, alpha: 1) //#B3ED8C + // Outcome transfer icon background, light red - public static let transferOutcomeIconBackground = #colorLiteral(red: 0.9411764706, green: 0.5215686275, blue: 0.5294117647, alpha: 1) //#F08587 + public static let transferOutcomeIconBackground = #colorLiteral(red: 0.9411764706, green: 0.5215686275, blue: 0.5294117647, alpha: 1) //#F08587 + + public static let newMessageLineColor = #colorLiteral(red: 0.2705882353, green: 0.2705882353, blue: 0.2705882353, alpha: 1) } } diff --git a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIColor+hex.swift b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIColor+hex.swift index 834c60dc8..7aadf84b7 100644 --- a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIColor+hex.swift +++ b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIColor+hex.swift @@ -8,18 +8,21 @@ import UIKit -public extension UIColor { - convenience init(hex hexString: String) { +extension UIColor { + public convenience init(hex hexString: String) { let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) var int = UInt32() Scanner(string: hex).scanHexInt32(&int) - let a, r, g, b: UInt32 + let a: UInt32 + let r: UInt32 + let g: UInt32 + let b: UInt32 switch hex.count { - case 3: // RGB (12-bit) + case 3: // RGB (12-bit) (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) - case 6: // RGB (24-bit) + case 6: // RGB (24-bit) (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) - case 8: // ARGB (32-bit) + case 8: // ARGB (32-bit) (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) default: (a, r, g, b) = (255, 0, 0, 0) @@ -28,26 +31,29 @@ public extension UIColor { } } -public extension UIColor { +extension UIColor { /// if alpha == 1 it will return new color, if alpha == 0 it will return old color - func mixin(infusion:UIColor, alpha: CGFloat) -> UIColor { + public func mixin(infusion: UIColor, alpha: CGFloat) -> UIColor { let alpha2 = min(1.0, max(0, alpha)) let beta = 1.0 - alpha2 - - var r1: CGFloat = 0, r2: CGFloat = 0 - var g1: CGFloat = 0, g2: CGFloat = 0 - var b1: CGFloat = 0, b2: CGFloat = 0 - var a1: CGFloat = 0, a2: CGFloat = 0 - - if getRed(&r1, green: &g1, blue: &b1, alpha: &a1) && - infusion.getRed(&r2, green: &g2, blue: &b2, alpha: &a2) { - let red = r1 * beta + r2 * alpha2 - let green = g1 * beta + g2 * alpha2 - let blue = b1 * beta + b2 * alpha2 - let alpha = a1 * beta + a2 * alpha2 + + var r1: CGFloat = 0 + var r2: CGFloat = 0 + var g1: CGFloat = 0 + var g2: CGFloat = 0 + var b1: CGFloat = 0 + var b2: CGFloat = 0 + var a1: CGFloat = 0 + var a2: CGFloat = 0 + + if getRed(&r1, green: &g1, blue: &b1, alpha: &a1) && infusion.getRed(&r2, green: &g2, blue: &b2, alpha: &a2) { + let red = r1 * beta + r2 * alpha2 + let green = g1 * beta + g2 * alpha2 + let blue = b1 * beta + b2 * alpha2 + let alpha = a1 * beta + a2 * alpha2 return UIColor(red: red, green: green, blue: blue, alpha: alpha) } - + return self } } diff --git a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIEdgeInsets+adamant.swift b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIEdgeInsets+adamant.swift index bb1f7e8fc..9a8b96b9b 100644 --- a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIEdgeInsets+adamant.swift +++ b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIEdgeInsets+adamant.swift @@ -8,8 +8,8 @@ import UIKit -public extension UIEdgeInsets { - static func +(lhs: Self, rhs: Self) -> UIEdgeInsets { +extension UIEdgeInsets { + public static func + (lhs: Self, rhs: Self) -> UIEdgeInsets { .init( top: lhs.top + rhs.top, left: lhs.left + rhs.left, diff --git a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UILabel+adamant.swift b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UILabel+adamant.swift index f91edee97..7dc113fa0 100644 --- a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UILabel+adamant.swift +++ b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UILabel+adamant.swift @@ -8,8 +8,8 @@ import UIKit -public extension UILabel { - convenience init( +extension UILabel { + public convenience init( font: UIFont? = nil, textColor: UIColor? = nil, numberOfLines: Int? = nil, diff --git a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIScrollView+Extension.swift b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIScrollView+Extension.swift index fc7acb6f2..81e1a7360 100644 --- a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIScrollView+Extension.swift +++ b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIScrollView+Extension.swift @@ -8,12 +8,12 @@ import UIKit -public extension UIScrollView { - func scrollToBottom(animated: Bool) { +extension UIScrollView { + public func scrollToBottom(animated: Bool) { let fullInsets = contentInset + safeAreaInsets let visibleHeight = bounds.height - fullInsets.top - fullInsets.bottom guard contentSize.height > visibleHeight else { return } - + let maxOffset = contentSize.height - bounds.height + contentInset.bottom + safeAreaInsets.bottom setContentOffset(.init(x: .zero, y: maxOffset), animated: animated) } diff --git a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIView+Extension.swift b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIView+Extension.swift index 54db6da1b..d17d219f0 100644 --- a/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIView+Extension.swift +++ b/CommonKit/Sources/CommonKit/Helpers/UIHelpers/UIView+Extension.swift @@ -7,8 +7,8 @@ import UIKit -public extension UIView { - func addSubviews(_ subviews: UIView...) { +extension UIView { + public func addSubviews(_ subviews: UIView...) { subviews.forEach { addSubview($0) } } } diff --git a/CommonKit/Sources/CommonKit/Helpers/UIImage+adamant.swift b/CommonKit/Sources/CommonKit/Helpers/UIImage+adamant.swift index 88308c55d..81cdd5234 100644 --- a/CommonKit/Sources/CommonKit/Helpers/UIImage+adamant.swift +++ b/CommonKit/Sources/CommonKit/Helpers/UIImage+adamant.swift @@ -1,3 +1,4 @@ +import AdamantWalletsKit // // UIImage+adamant.swift // Adamant @@ -5,15 +6,21 @@ // Created by Stanislav Jelezoglo on 13.01.2023. // Copyright © 2023 Adamant. All rights reserved. // - import UIKit -public extension UIImage { - static func asset(named: String) -> UIImage? { - .init(named: named, in: .module, with: nil) +extension UIImage { + public static func asset(named: String) -> UIImage? { + if let image = UIImage(named: named, in: .module, with: nil) { + return image + } + if let image = WalletsImageProvider.image(named: named) { + return image + } + + return nil } - - func imageResized(to size: CGSize) -> UIImage { + + public func imageResized(to size: CGSize) -> UIImage { return UIGraphicsImageRenderer(size: size).image { _ in draw(in: CGRect(origin: .zero, size: size)) } diff --git a/CommonKit/Sources/CommonKit/Helpers/Version.swift b/CommonKit/Sources/CommonKit/Helpers/Version.swift index a4392edce..56c00d791 100644 --- a/CommonKit/Sources/CommonKit/Helpers/Version.swift +++ b/CommonKit/Sources/CommonKit/Helpers/Version.swift @@ -7,25 +7,26 @@ public struct Version: Sendable { public let versions: [Int] - + public init(_ versions: [Int]) { self.versions = versions } } -public extension Version { - static var zero: Self { .init([.zero]) } - - var string: String { +extension Version { + public static var zero: Self { .init([.zero]) } + + public var string: String { versions.map { String($0) }.joined(separator: ".") } - - init?(_ string: String) { - let versions = string + + public init?(_ string: String) { + let versions = + string .filter { $0.isNumber || $0 == "." } .split(separator: ".") .compactMap { Int($0) } - + guard !versions.isEmpty else { return nil } self.versions = versions } @@ -33,28 +34,28 @@ public extension Version { extension Version: Comparable { public static func < (lhs: Self, rhs: Self) -> Bool { - for i in .zero ..< max(lhs.versions.endIndex, rhs.versions.endIndex) { + for i in .zero.. right { return false } } - + return false } } extension Version: Equatable { public static func == (lhs: Self, rhs: Self) -> Bool { - for i in .zero ..< max(lhs.versions.endIndex, rhs.versions.endIndex) { + for i in .zero.. String { String.localizedStringWithFormat( String.localized( @@ -25,16 +25,16 @@ public extension String.adamant { message ) } - + // MARK: - Transfer preview - + public static var newTransfer: String { String.localized( "transfer.notificationTitle", comment: "New transfer notification title" ) } - + public static func yourTransferBody(with amount: String) -> String { String.localizedStringWithFormat( String.localized( @@ -44,7 +44,7 @@ public extension String.adamant { amount ) } - + public static var yourAddress: String { String.localized( "transfer.notificationBody.yourAddress", diff --git a/CommonKit/Sources/CommonKit/Localization/AdamantLocalized.swift b/CommonKit/Sources/CommonKit/Localization/AdamantLocalized.swift index bd4768254..7c0308697 100644 --- a/CommonKit/Sources/CommonKit/Localization/AdamantLocalized.swift +++ b/CommonKit/Sources/CommonKit/Localization/AdamantLocalized.swift @@ -18,42 +18,42 @@ extension String: Localizable { } } -public extension String { - enum adamant {} - - static func locale() -> Locale { +extension String { + public enum adamant {} + + public static func locale() -> Locale { guard let languageRaw = UserDefaults.standard.string(forKey: StoreKey.language.language), - !languageRaw.isEmpty, - languageRaw != Language.auto.rawValue + !languageRaw.isEmpty, + languageRaw != Language.auto.rawValue else { return .current } - + return Locale(identifier: languageRaw) } - - static func localized(_ key: String, comment: String = .empty) -> String { + + public static func localized(_ key: String, comment: String = .empty) -> String { guard let languageRaw = UserDefaults.standard.string(forKey: StoreKey.language.language), - !languageRaw.isEmpty, - languageRaw != Language.auto.rawValue, - let path = Bundle.module.path(forResource: languageRaw, ofType: "lproj") + !languageRaw.isEmpty, + languageRaw != Language.auto.rawValue, + let path = Bundle.module.path(forResource: languageRaw, ofType: "lproj") else { return NSLocalizedString(key, bundle: .module, comment: comment) } - + let bundle: Bundle = Bundle(path: path) ?? .module return NSLocalizedString(key, bundle: bundle, comment: comment) } } -public extension String.adamant { - enum shared { +extension String.adamant { + public enum shared { public static var productName: String { String.localized("ADAMANT", comment: "Product name") } } - - enum sharedErrors { + + public enum sharedErrors { public static var userNotLogged: String { String.localized("Error.UserNotLogged", comment: "Shared error: User not logged") } @@ -72,46 +72,52 @@ public extension String.adamant { text ) } - + public static func accountNotFound(_ account: String) -> String { - String.localizedStringWithFormat(.localized("Error.AccountNotFoundFormat", comment: "Shared error: Account not found error. Using %@ for address."), account) + String.localizedStringWithFormat( + .localized("Error.AccountNotFoundFormat", comment: "Shared error: Account not found error. Using %@ for address."), + account + ) } - + public static var accountNotInitiated: String { String.localized("Error.AccountNotInitiated", comment: "Shared error: Account not initiated") } - + public static var unknownError: String { String.localized("Error.UnknownError", comment: "Shared unknown error") } public static func admNodeErrorMessage(_ coin: String) -> String { - String.localizedStringWithFormat(.localized("ApiService.InternalError.NoAdmNodesAvailable", comment: "No active ADM nodes to fetch the partner's %@ address"), coin) + String.localizedStringWithFormat( + .localized("ApiService.InternalError.NoAdmNodesAvailable", comment: "No active ADM nodes to fetch the partner's %@ address"), + coin + ) } - + public static var notEnoughMoney: String { String.localized("WalletServices.SharedErrors.notEnoughMoney", comment: "Wallet Services: Shared error, user do not have enought money.") } - + public static var dustError: String { String.localized("TransferScene.Dust.Error", comment: "Tranfser: Dust error.") } - + public static var transactionUnavailable: String { String.localized("WalletServices.SharedErrors.transactionUnavailable", comment: "Wallet Services: Transaction unavailable") } - + public static var inconsistentTransaction: String { String.localized("WalletServices.SharedErrors.inconsistentTransaction", comment: "Wallet Services: Cannot verify transaction") } - + public static var walletFrezzed: String { String.localized("WalletServices.SharedErrors.walletFrezzed", comment: "Wallet Services: Wait until other transactions approved") } - + public static func internalError(message: String) -> String { String.localizedStringWithFormat(.localized("Error.InternalErrorFormat", comment: "Shared error: Internal error format, %@ for message"), message) } - + public static func remoteServerError(message: String) -> String { String.localizedStringWithFormat(.localized("Error.RemoteServerErrorFormat", comment: "Shared error: Remote error format, %@ for message"), message) } diff --git a/CommonKit/Sources/CommonKit/Models/AMenu/AMenuItem+Style.swift b/CommonKit/Sources/CommonKit/Models/AMenu/AMenuItem+Style.swift index 2a4317b35..15e1db5a2 100644 --- a/CommonKit/Sources/CommonKit/Models/AMenu/AMenuItem+Style.swift +++ b/CommonKit/Sources/CommonKit/Models/AMenu/AMenuItem+Style.swift @@ -1,6 +1,6 @@ // // AMenuItem+Style.swift -// +// // // Created by Stanislav Jelezoglo on 26.07.2023. // @@ -8,18 +8,18 @@ import SwiftUI extension AMenuItem { - + public enum Style { - + /* Enum for defining the style of menu elements - + It can be initialised as plain (uses menu defaults), styled or uiStyled - the styled cases allow customisation of font, text colour, icon colour & background colour */ - + /// The plain style case case plain - + /// The destructive style case case destructive @@ -35,9 +35,9 @@ extension AMenuItem { // MARK: - Internal -public extension AMenuItem.Style { +extension AMenuItem.Style { @MainActor - func configure( + public func configure( titleLabel: UILabel, icon: UIImageView?, backgroundView: UIView?, @@ -50,13 +50,13 @@ public extension AMenuItem.Style { titleLabel.font = menuFont titleLabel.textColor = color icon?.tintColor = color - + case .destructive: let color = UIColor.adamant.contextMenuDestructive titleLabel.font = menuFont titleLabel.textColor = color icon?.tintColor = color - + case .styled(let font, let textColor, let iconColor, let backgroundColor): if let font = font { titleLabel.font = font @@ -70,8 +70,8 @@ public extension AMenuItem.Style { } } } - - var backgroundColor: UIColor? { + + public var backgroundColor: UIColor? { switch self { case .plain, .destructive: return nil diff --git a/CommonKit/Sources/CommonKit/Models/AMenu/AMenuItem.swift b/CommonKit/Sources/CommonKit/Models/AMenu/AMenuItem.swift index 0860a36b1..50c89b92c 100644 --- a/CommonKit/Sources/CommonKit/Models/AMenu/AMenuItem.swift +++ b/CommonKit/Sources/CommonKit/Models/AMenu/AMenuItem.swift @@ -1,6 +1,6 @@ // // AMenuItem.swift -// +// // // Created by Stanislav Jelezoglo on 23.07.2023. // @@ -8,7 +8,7 @@ import SwiftUI public enum AMenuItem { - + /// The case for an action menu item /// - Parameters: /// - name: the menu item name (as it appears in the menu) @@ -23,7 +23,7 @@ public enum AMenuItem { style: Style = .plain, action: (() -> Void) ) - + /// Creates an action menu item /// - Parameters: /// - name: the menu item name (as it appears in the menu) @@ -45,7 +45,7 @@ public enum AMenuItem { action: action ) } - + /// Creates an action menu item /// - Parameters: /// - name: the menu item name (as it appears in the menu) @@ -71,15 +71,15 @@ public enum AMenuItem { // MARK: - Internal -public extension AMenuItem { - var name: String { +extension AMenuItem { + public var name: String { switch self { case .action(let name, _, _, _, _): return name } } - - var iconImage: UIImage? { + + public var iconImage: UIImage? { switch self { case .action(_, let imageName, let systemImageName, _, _): if let imageName = imageName { @@ -90,15 +90,15 @@ public extension AMenuItem { return nil } } - - var style: Style { + + public var style: Style { switch self { case .action(_, _, _, let style, _): return style } } - - var action: () -> Void { + + public var action: () -> Void { switch self { case .action(_, _, _, _, let action): return action diff --git a/CommonKit/Sources/CommonKit/Models/AMenu/AMenuSection.swift b/CommonKit/Sources/CommonKit/Models/AMenu/AMenuSection.swift index 573a41956..3c7f1bc42 100644 --- a/CommonKit/Sources/CommonKit/Models/AMenu/AMenuSection.swift +++ b/CommonKit/Sources/CommonKit/Models/AMenu/AMenuSection.swift @@ -1,6 +1,6 @@ // // AMenuSection.swift -// +// // // Created by Stanislav Jelezoglo on 26.07.2023. // @@ -10,7 +10,7 @@ import Foundation public struct AMenuSection { public let menuItems: [AMenuItem] - + public init(_ menuItems: [AMenuItem]) { self.menuItems = menuItems } diff --git a/CommonKit/Sources/CommonKit/Models/APIParametersEncoding.swift b/CommonKit/Sources/CommonKit/Models/APIParametersEncoding.swift index e2e85b2b5..d70429f72 100644 --- a/CommonKit/Sources/CommonKit/Models/APIParametersEncoding.swift +++ b/CommonKit/Sources/CommonKit/Models/APIParametersEncoding.swift @@ -14,7 +14,7 @@ public enum APIParametersEncoding { case json case bodyString case forceQueryItems([URLQueryItem]) - + public var parametersEncoding: ParameterEncoding { switch self { case .url: diff --git a/CommonKit/Sources/CommonKit/Models/APIResponseModel.swift b/CommonKit/Sources/CommonKit/Models/APIResponseModel.swift index 302f4d5c3..ae6f66911 100644 --- a/CommonKit/Sources/CommonKit/Models/APIResponseModel.swift +++ b/CommonKit/Sources/CommonKit/Models/APIResponseModel.swift @@ -12,7 +12,7 @@ public struct APIResponseModel: Sendable { public let result: ApiServiceResult public let data: Data? public let code: Int? - + public init(result: ApiServiceResult, data: Data?, code: Int?) { self.result = result self.data = data diff --git a/CommonKit/Sources/CommonKit/Models/AccountServiceStoreKey.swift b/CommonKit/Sources/CommonKit/Models/AccountServiceStoreKey.swift index 9a2533039..a26463111 100644 --- a/CommonKit/Sources/CommonKit/Models/AccountServiceStoreKey.swift +++ b/CommonKit/Sources/CommonKit/Models/AccountServiceStoreKey.swift @@ -1,13 +1,13 @@ // -// SecuredStore+Account.swift +// SecureStore+Account.swift // Adamant // // Created by Andrey Golubenko on 21.11.2022. // Copyright © 2022 Adamant. All rights reserved. // -public extension StoreKey { - enum accountService { +extension StoreKey { + public enum accountService { public static let publicKey = "accountService.publicKey" public static let privateKey = "accountService.privateKey" public static let pin = "accountService.pin" diff --git a/CommonKit/Sources/CommonKit/Models/AdamantAccount.swift b/CommonKit/Sources/CommonKit/Models/AdamantAccount.swift index fb29c5671..4c3cb4e40 100644 --- a/CommonKit/Sources/CommonKit/Models/AdamantAccount.swift +++ b/CommonKit/Sources/CommonKit/Models/AdamantAccount.swift @@ -19,11 +19,11 @@ public struct AdamantAccount: @unchecked Sendable { public let multisignatures: [String]? public let uMultisignatures: [String]? public var isDummy: Bool - + public init( address: String, unconfirmedBalance: Decimal, - balance: Decimal, + balance: Decimal, publicKey: String?, unconfirmedSignature: Int, secondSignature: Int, @@ -57,10 +57,10 @@ extension AdamantAccount: Decodable { case multisignatures case uMultisignatures = "u_multisignatures" } - + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - + self.address = try container.decode(String.self, forKey: .address) self.unconfirmedSignature = try container.decode(Int.self, forKey: .unconfirmedSignature) self.publicKey = try? container.decode(String.self, forKey: .publicKey) @@ -68,7 +68,7 @@ extension AdamantAccount: Decodable { self.secondPublicKey = try? container.decode(String.self, forKey: .secondPublicKey) self.multisignatures = try? container.decode([String].self, forKey: .multisignatures) self.uMultisignatures = try? container.decode([String].self, forKey: .uMultisignatures) - + let unconfirmedBalance = Decimal(string: try container.decode(String.self, forKey: .unconfirmedBalance))! self.unconfirmedBalance = unconfirmedBalance.shiftedFromAdamant() let balance = Decimal(string: try container.decode(String.self, forKey: .balance))! @@ -79,7 +79,7 @@ extension AdamantAccount: Decodable { extension AdamantAccount: WrappableModel { public static let ModelKey = "account" - + public static func makeEmptyAccount(publicKey: String) -> Self { .init( address: AdamantUtilities.generateAddress(publicKey: publicKey), @@ -95,7 +95,11 @@ extension AdamantAccount: WrappableModel { ) } } - +extension AdamantAccount { + public var isEnoughMoneyForTransaction: Bool { + balance >= AdamantApiService.KvsFee + } +} // MARK: - JSON /* { diff --git a/CommonKit/Sources/CommonKit/Models/AdamantError.swift b/CommonKit/Sources/CommonKit/Models/AdamantError.swift index b853fc333..c7468c276 100644 --- a/CommonKit/Sources/CommonKit/Models/AdamantError.swift +++ b/CommonKit/Sources/CommonKit/Models/AdamantError.swift @@ -10,7 +10,7 @@ import Foundation public struct AdamantError: Error, LocalizedError { public let errorDescription: String? - + public init(message: String) { self.errorDescription = message } diff --git a/CommonKit/Sources/CommonKit/Models/ApiServiceError.swift b/CommonKit/Sources/CommonKit/Models/ApiServiceError.swift index d63aff4a1..5d9f94165 100644 --- a/CommonKit/Sources/CommonKit/Models/ApiServiceError.swift +++ b/CommonKit/Sources/CommonKit/Models/ApiServiceError.swift @@ -17,31 +17,31 @@ public enum ApiServiceError: LocalizedError, Sendable { case requestCancelled case commonError(message: String) case noEndpointsAvailable(nodeGroupName: String) - + public var errorDescription: String? { switch self { case .notLogged: return String.adamant.sharedErrors.userNotLogged - + case .accountNotFound: return String.adamant.sharedErrors.accountNotFound("") - + case let .serverError(error): return String.adamant.sharedErrors.remoteServerError(message: error) - + case let .internalError(msg, error): let message = error?.localizedDescription ?? msg return String.adamant.sharedErrors.internalError(message: message) - + case let .networkError(error): return error.localizedDescription - + case .requestCancelled: return String.adamant.sharedErrors.requestCancelled - + case let .commonError(message): return String.adamant.sharedErrors.commonError(message) - + case let .noEndpointsAvailable(nodeGroupName): return .localizedStringWithFormat( .localized( @@ -52,7 +52,7 @@ public enum ApiServiceError: LocalizedError, Sendable { ).localized } } - + public static func internalError(error: InternalAPIError) -> Self { .internalError(message: error.localizedDescription, error: error) } @@ -63,19 +63,22 @@ extension ApiServiceError: Equatable { switch (lhs, rhs) { case (.notLogged, .notLogged): return true - + case (.accountNotFound, .accountNotFound): return true - + case (.serverError(let le), .serverError(let re)): return le == re - + case (.internalError(let lm, _), .internalError(let rm, _)): return lm == rm - + case (.networkError, .networkError): return true - + + case (.requestCancelled, .requestCancelled): + return true + default: return false } @@ -91,11 +94,11 @@ extension ApiServiceError: HealthCheckableError { return false } } - + public static var noNetworkError: ApiServiceError { .networkError(error: AdamantError(message: .adamant.sharedErrors.networkError)) } - + public static func noEndpointsError(nodeGroupName: String) -> ApiServiceError { .noEndpointsAvailable(nodeGroupName: nodeGroupName) } diff --git a/CommonKit/Sources/CommonKit/Models/BlockchainHealthCheckParams.swift b/CommonKit/Sources/CommonKit/Models/BlockchainHealthCheckParams.swift index 93edde097..6ee745058 100644 --- a/CommonKit/Sources/CommonKit/Models/BlockchainHealthCheckParams.swift +++ b/CommonKit/Sources/CommonKit/Models/BlockchainHealthCheckParams.swift @@ -14,7 +14,7 @@ public struct BlockchainHealthCheckParams: Sendable { public let crucialUpdateInterval: TimeInterval public let minNodeVersion: Version? public let nodeHeightEpsilon: Int - + public init( group: NodeGroup, name: String, diff --git a/CommonKit/Sources/CommonKit/Models/BodyStringEncoding.swift b/CommonKit/Sources/CommonKit/Models/BodyStringEncoding.swift index ece7a7b30..6e2157c64 100644 --- a/CommonKit/Sources/CommonKit/Models/BodyStringEncoding.swift +++ b/CommonKit/Sources/CommonKit/Models/BodyStringEncoding.swift @@ -15,22 +15,24 @@ public struct BodyStringEncoding: ParameterEncoding { with parameters: Parameters? ) throws -> URLRequest { var urlRequest = try urlRequest.asURLRequest() - + guard let string = parameters?.first?.value as? String, let data = string.data(using: .utf8) else { throw AFError.parameterEncodingFailed( - reason: .customEncodingFailed(error: AdamantError( - message: "String encoding problem" - )) + reason: .customEncodingFailed( + error: AdamantError( + message: "String encoding problem" + ) + ) ) } - + if parameters?.count != 1 { assertionFailure("BodyStringEncoding uses just first parameter for encoding") } - + urlRequest.httpBody = data return urlRequest } diff --git a/CommonKit/Sources/CommonKit/Models/ChatContextMenuArguments.swift b/CommonKit/Sources/CommonKit/Models/ChatContextMenuArguments.swift index 049e0d34e..a4cf196d1 100644 --- a/CommonKit/Sources/CommonKit/Models/ChatContextMenuArguments.swift +++ b/CommonKit/Sources/CommonKit/Models/ChatContextMenuArguments.swift @@ -1,6 +1,6 @@ // // ChatContextMenuArguments.swift -// +// // // Created by Stanislav Jelezoglo on 25.08.2023. // @@ -16,7 +16,7 @@ public struct ChatContextMenuArguments: @unchecked Sendable { public let menu: AMenuSection public let selectedEmoji: String? public let getPositionOnScreen: () -> CGPoint - + public init( copyView: UIView, size: CGSize, @@ -36,5 +36,5 @@ public struct ChatContextMenuArguments: @unchecked Sendable { self.selectedEmoji = selectedEmoji self.getPositionOnScreen = getPositionOnScreen } - + } diff --git a/CommonKit/Sources/CommonKit/Models/ChatRoomLoadingStatus.swift b/CommonKit/Sources/CommonKit/Models/ChatRoomLoadingStatus.swift index b8dcc1af0..ccd6fd2ef 100644 --- a/CommonKit/Sources/CommonKit/Models/ChatRoomLoadingStatus.swift +++ b/CommonKit/Sources/CommonKit/Models/ChatRoomLoadingStatus.swift @@ -1,6 +1,6 @@ // // ChatRoomLoadingStatus.swift -// +// // // Created by Stanislav Jelezoglo on 16.08.2023. // diff --git a/CommonKit/Sources/CommonKit/Models/CoinHealthCheckParameters.swift b/CommonKit/Sources/CommonKit/Models/CoinHealthCheckParameters.swift index 42d71ea75..d0bd9c086 100644 --- a/CommonKit/Sources/CommonKit/Models/CoinHealthCheckParameters.swift +++ b/CommonKit/Sources/CommonKit/Models/CoinHealthCheckParameters.swift @@ -1,6 +1,6 @@ // // CoinHealthCheckParameters.swift -// +// // // Created by Stanislav Jelezoglo on 28.12.2023. // @@ -15,7 +15,7 @@ public struct CoinHealthCheckParameters: Sendable { public let normalServiceUpdateInterval: TimeInterval public let crucialServiceUpdateInterval: TimeInterval public let onScreenServiceUpdateInterval: TimeInterval - + public init( normalUpdateInterval: TimeInterval, crucialUpdateInterval: TimeInterval, diff --git a/CommonKit/Sources/CommonKit/Models/ComparableAttributedString.swift b/CommonKit/Sources/CommonKit/Models/ComparableAttributedString.swift index 0f69d2e14..17f82438c 100644 --- a/CommonKit/Sources/CommonKit/Models/ComparableAttributedString.swift +++ b/CommonKit/Sources/CommonKit/Models/ComparableAttributedString.swift @@ -10,11 +10,11 @@ import Foundation public struct ComparableAttributedString: Equatable, @unchecked Sendable { public let string: NSAttributedString - + public init(string: NSAttributedString) { self.string = string } - + public static func == (lhs: Self, rhs: Self) -> Bool { guard lhs.string.hash == rhs.string.hash else { return false } return lhs.string.string == rhs.string.string diff --git a/CommonKit/Sources/CommonKit/Models/DelegateVote.swift b/CommonKit/Sources/CommonKit/Models/DelegateVote.swift index ed8f0c5db..66e8c0650 100644 --- a/CommonKit/Sources/CommonKit/Models/DelegateVote.swift +++ b/CommonKit/Sources/CommonKit/Models/DelegateVote.swift @@ -11,7 +11,7 @@ import Foundation public enum DelegateVote { case upvote(publicKey: String) case downvote(publicKey: String) - + public func asString() -> String { switch self { case .upvote(let key): return "+\(key)" diff --git a/CommonKit/Sources/CommonKit/Models/FileResult.swift b/CommonKit/Sources/CommonKit/Models/FileResult.swift index 85374c1d6..ec065d094 100644 --- a/CommonKit/Sources/CommonKit/Models/FileResult.swift +++ b/CommonKit/Sources/CommonKit/Models/FileResult.swift @@ -1,6 +1,6 @@ // // FileResult.swift -// +// // // Created by Stanislav Jelezoglo on 06.03.2024. // @@ -11,7 +11,7 @@ public enum FileType: Sendable { case image case video case other - + public var isMedia: Bool { switch self { case .image, .video: @@ -22,8 +22,8 @@ public enum FileType: Sendable { } } -public extension FileType { - init?(mimeType: String) { +extension FileType { + public init?(mimeType: String) { if mimeType.hasPrefix("image/") { self = .image } else if mimeType.hasPrefix("video/") { @@ -32,8 +32,8 @@ public extension FileType { self = .other } } - - init?(raw: String) { + + public init?(raw: String) { switch raw.uppercased() { case "JPG", "JPEG", "PNG", "GIF", "WEBP", "TIF", "TIFF", "BMP", "HEIF", "HEIC", "JP2": self = .image @@ -58,7 +58,7 @@ public struct FileResult: Sendable { public let data: Data? public let duration: Float64? public let mimeType: String? - + public init( assetId: String? = nil, url: URL, @@ -90,8 +90,8 @@ public struct FileResult: Sendable { } } -public extension FileResult { - init( +extension FileResult { + public init( assetId: String? = nil, url: URL, type: FileType, @@ -107,11 +107,12 @@ public extension FileResult { mimeType: String? = nil ) { let nameWithExtension = namePossiblyWithExtension.separateFileExtension() - - let name = nameWithExtension.extension == extenstion + + let name = + nameWithExtension.extension == extenstion ? nameWithExtension.name : namePossiblyWithExtension - + self.init( assetId: assetId, url: url, diff --git a/CommonKit/Sources/CommonKit/Models/FileValidationError.swift b/CommonKit/Sources/CommonKit/Models/FileValidationError.swift index 68b439014..355ebfde4 100644 --- a/CommonKit/Sources/CommonKit/Models/FileValidationError.swift +++ b/CommonKit/Sources/CommonKit/Models/FileValidationError.swift @@ -1,6 +1,6 @@ // // FileValidationError.swift -// +// // // Created by Stanislav Jelezoglo on 11.04.2024. // @@ -15,10 +15,13 @@ extension FilePickersError: LocalizedError { public var errorDescription: String? { switch self { case let .cantSelectFile(name): - return String.localizedStringWithFormat(.localized( - "FileValidationError.CantSelectFile", - comment: "File picker error 'Cant select file'" - ), name) + return String.localizedStringWithFormat( + .localized( + "FileValidationError.CantSelectFile", + comment: "File picker error 'Cant select file'" + ), + name + ) } } } @@ -34,15 +37,21 @@ extension FileValidationError: LocalizedError { public var errorDescription: String? { switch self { case .tooManyFiles: - return String.localizedStringWithFormat(.localized( - "FileValidationError.TooManyFiles", - comment: "File validation error 'Too many files'" - ), FilesConstants.maxFilesCount) + return String.localizedStringWithFormat( + .localized( + "FileValidationError.TooManyFiles", + comment: "File validation error 'Too many files'" + ), + FilesConstants.maxFilesCount + ) case .fileSizeExceedsLimit: - return String.localizedStringWithFormat(.localized( - "FileValidationError.FileSizeExceedsLimit", - comment: "File validation error 'File size exceeds limit'" - ), Int(FilesConstants.maxFileSize / (1024 * 1024))) + return String.localizedStringWithFormat( + .localized( + "FileValidationError.FileSizeExceedsLimit", + comment: "File validation error 'File size exceeds limit'" + ), + Int(FilesConstants.maxFileSize / (1024 * 1024)) + ) case .fileNotFound: return .localized("FileValidationError.FileNotFound") case let .unknownError(error): diff --git a/CommonKit/Sources/CommonKit/Models/ForceQueryItemsEncoding.swift b/CommonKit/Sources/CommonKit/Models/ForceQueryItemsEncoding.swift index ccc664acb..aaae42e52 100644 --- a/CommonKit/Sources/CommonKit/Models/ForceQueryItemsEncoding.swift +++ b/CommonKit/Sources/CommonKit/Models/ForceQueryItemsEncoding.swift @@ -11,25 +11,25 @@ import Foundation public struct ForceQueryItemsEncoding: ParameterEncoding { public let queryItems: [URLQueryItem] - + public func encode( _ urlRequest: URLRequestConvertible, with parameters: Parameters? ) throws -> URLRequest { var urlRequest = try urlRequest.asURLRequest() - + guard let url = urlRequest.url, var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else { throw AFError.parameterEncodingFailed(reason: .missingURL) } - + urlComponents.queryItems = queryItems urlRequest.url = urlComponents.url return urlRequest } - + public init(queryItems: [URLQueryItem]) { self.queryItems = queryItems } diff --git a/CommonKit/Sources/CommonKit/Models/InternalAPIError.swift b/CommonKit/Sources/CommonKit/Models/InternalAPIError.swift index 02fc8f75c..51bf5daad 100644 --- a/CommonKit/Sources/CommonKit/Models/InternalAPIError.swift +++ b/CommonKit/Sources/CommonKit/Models/InternalAPIError.swift @@ -13,11 +13,11 @@ public enum InternalAPIError: LocalizedError { case signTransactionFailed case parsingFailed case unknownError - + public func apiServiceErrorWith(error: Error) -> ApiServiceError { .internalError(message: localizedDescription, error: error) } - + public var errorDescription: String? { switch self { case .endpointBuildFailed: diff --git a/CommonKit/Sources/CommonKit/Models/KVSValueModel.swift b/CommonKit/Sources/CommonKit/Models/KVSValueModel.swift index 481dd2af5..1c9e71662 100644 --- a/CommonKit/Sources/CommonKit/Models/KVSValueModel.swift +++ b/CommonKit/Sources/CommonKit/Models/KVSValueModel.swift @@ -9,7 +9,7 @@ public struct KVSValueModel: Sendable { public let key: String public let value: String public let keypair: Keypair - + public init( key: String, value: String, diff --git a/CommonKit/Sources/CommonKit/Models/Keychain/NodesKeychainDTO.swift b/CommonKit/Sources/CommonKit/Models/Keychain/NodesKeychainDTO.swift index 2aa61b692..400bf6770 100644 --- a/CommonKit/Sources/CommonKit/Models/Keychain/NodesKeychainDTO.swift +++ b/CommonKit/Sources/CommonKit/Models/Keychain/NodesKeychainDTO.swift @@ -10,7 +10,7 @@ import Foundation struct NodesKeychainDTO: Codable { let version: String let data: SafeDecodingDictionary> - + init(_ data: [NodeGroup: [NodeKeychainDTO]]) { self.version = "1.0.0" self.data = .init(data.mapValues { .init($0) }) diff --git a/CommonKit/Sources/CommonKit/Models/Keychain/OldNodeKeychainDTO.swift b/CommonKit/Sources/CommonKit/Models/Keychain/OldNodeKeychainDTO.swift index 03207c190..ca57e2763 100644 --- a/CommonKit/Sources/CommonKit/Models/Keychain/OldNodeKeychainDTO.swift +++ b/CommonKit/Sources/CommonKit/Models/Keychain/OldNodeKeychainDTO.swift @@ -33,19 +33,19 @@ extension OldNodeKeychainDTO.NodeData { enum RejectedReason: Codable, Equatable { case outdatedApiVersion } - + enum ConnectionStatus: Equatable, Codable { case offline case synchronizing case allowed case notAllowed(RejectedReason) } - + enum URLScheme: String, Codable { case http case https } - + func mapToModernDto(group: NodeGroup) -> NodeKeychainDTO { .init( mainOrigin: .init( @@ -68,8 +68,8 @@ extension OldNodeKeychainDTO.NodeData { } } -private extension OldNodeKeychainDTO.NodeData.URLScheme { - func map() -> NodeOrigin.URLScheme { +extension OldNodeKeychainDTO.NodeData.URLScheme { + fileprivate func map() -> NodeOrigin.URLScheme { switch self { case .http: return .http @@ -79,8 +79,8 @@ private extension OldNodeKeychainDTO.NodeData.URLScheme { } } -private extension OldNodeKeychainDTO.NodeData.ConnectionStatus { - func map() -> NodeConnectionStatusKeychainDTO { +extension OldNodeKeychainDTO.NodeData.ConnectionStatus { + fileprivate func map() -> NodeConnectionStatusKeychainDTO { switch self { case .offline: return .offline @@ -94,8 +94,8 @@ private extension OldNodeKeychainDTO.NodeData.ConnectionStatus { } } -private extension OldNodeKeychainDTO.NodeData.RejectedReason { - func map() -> NodeConnectionStatusKeychainDTO.RejectedReason { +extension OldNodeKeychainDTO.NodeData.RejectedReason { + fileprivate func map() -> NodeConnectionStatusKeychainDTO.RejectedReason { switch self { case .outdatedApiVersion: return .outdatedApiVersion diff --git a/CommonKit/Sources/CommonKit/Models/Keypair.swift b/CommonKit/Sources/CommonKit/Models/Keypair.swift index b2b056b66..cb4e59717 100644 --- a/CommonKit/Sources/CommonKit/Models/Keypair.swift +++ b/CommonKit/Sources/CommonKit/Models/Keypair.swift @@ -11,7 +11,7 @@ import Foundation public struct Keypair: Equatable, Sendable { public let publicKey: String public let privateKey: String - + public init(publicKey: String, privateKey: String) { self.publicKey = publicKey self.privateKey = privateKey diff --git a/CommonKit/Sources/CommonKit/Models/Language.swift b/CommonKit/Sources/CommonKit/Models/Language.swift index dad1fa667..e307872cc 100644 --- a/CommonKit/Sources/CommonKit/Models/Language.swift +++ b/CommonKit/Sources/CommonKit/Models/Language.swift @@ -8,8 +8,8 @@ import Foundation -public extension Notification.Name { - struct LanguageStorageService { +extension Notification.Name { + public struct LanguageStorageService { public static let languageUpdated = Notification.Name("adamant.language.languageUpdated") } } @@ -20,7 +20,7 @@ public enum Language: String, Sendable { case de case zh case auto - + public var name: String { switch self { case .ru: return "Русский" @@ -30,7 +30,7 @@ public enum Language: String, Sendable { case .auto: return .localized("Language.Auto", comment: "Account tab: Language auto") } } - + public var locale: String { switch self { case .ru: return "ru_RU" @@ -40,6 +40,6 @@ public enum Language: String, Sendable { case .auto: return "en_EN" } } - + public static let all: [Language] = [.auto, .en, .ru, .de, .zh] } diff --git a/CommonKit/Sources/CommonKit/Models/MultipartFormDataModel.swift b/CommonKit/Sources/CommonKit/Models/MultipartFormDataModel.swift index fe48f796a..952ff3963 100644 --- a/CommonKit/Sources/CommonKit/Models/MultipartFormDataModel.swift +++ b/CommonKit/Sources/CommonKit/Models/MultipartFormDataModel.swift @@ -12,7 +12,7 @@ public struct MultipartFormDataModel: Sendable { public let keyName: String public let fileName: String public let data: Data - + public init(keyName: String, fileName: String, data: Data) { self.keyName = keyName self.fileName = fileName diff --git a/CommonKit/Sources/CommonKit/Models/Node/Node.swift b/CommonKit/Sources/CommonKit/Models/Node/Node.swift index 0ea8a6e2d..57b6ae5c3 100644 --- a/CommonKit/Sources/CommonKit/Models/Node/Node.swift +++ b/CommonKit/Sources/CommonKit/Models/Node/Node.swift @@ -22,7 +22,7 @@ public struct Node: Equatable, Identifiable, @unchecked Sendable { public var preferMainOrigin: Bool? public var isEnabled: Bool public var type: NodeType - + public init( id: UUID, isEnabled: Bool, @@ -50,14 +50,18 @@ public struct Node: Equatable, Identifiable, @unchecked Sendable { } } -public extension Node { - var preferredOrigin: NodeOrigin { +extension Node { + public var preferredOrigin: NodeOrigin { preferMainOrigin ?? true ? mainOrigin : altOrigin ?? mainOrigin } - - static func makeDefaultNode(url: URL, altUrl: URL? = nil) -> Self { + + public var isSupported: Bool { + isEnabled && connectionStatus == .allowed && version != nil + } + + public static func makeDefaultNode(url: URL, altUrl: URL? = nil) -> Self { .init( id: .init(), isEnabled: true, @@ -72,20 +76,20 @@ public extension Node { type: .default(isHidden: false) ) } - - func asSocketURL() -> URL? { + + public func asSocketURL() -> URL? { preferredOrigin.asSocketURL() } - func asURL() -> URL? { + public func asURL() -> URL? { preferredOrigin.asURL() } - - func isSame(_ node: Node) -> Bool { + + public func isSame(_ node: Node) -> Bool { mainOrigin.host == node.mainOrigin.host } - - mutating func updateWsPort(_ wsPort: Int?) { + + public mutating func updateWsPort(_ wsPort: Int?) { mainOrigin.wsPort = wsPort altOrigin?.wsPort = wsPort } diff --git a/CommonKit/Sources/CommonKit/Models/Node/NodeConnectionStatus.swift b/CommonKit/Sources/CommonKit/Models/Node/NodeConnectionStatus.swift index 9003f416e..a94350376 100644 --- a/CommonKit/Sources/CommonKit/Models/Node/NodeConnectionStatus.swift +++ b/CommonKit/Sources/CommonKit/Models/Node/NodeConnectionStatus.swift @@ -14,14 +14,14 @@ public enum NodeConnectionStatus: Equatable, Codable, Sendable { case notAllowed(RejectedReason) } -public extension NodeConnectionStatus { - enum RejectedReason: Codable, Equatable, Sendable { +extension NodeConnectionStatus { + public enum RejectedReason: Codable, Equatable, Sendable { case outdatedApiVersion } } -public extension NodeConnectionStatus.RejectedReason { - var text: String { +extension NodeConnectionStatus.RejectedReason { + public var text: String { switch self { case .outdatedApiVersion: return String.localized( diff --git a/CommonKit/Sources/CommonKit/Models/Node/NodeGroup.swift b/CommonKit/Sources/CommonKit/Models/Node/NodeGroup.swift index abb90fada..8fbee0f5e 100644 --- a/CommonKit/Sources/CommonKit/Models/Node/NodeGroup.swift +++ b/CommonKit/Sources/CommonKit/Models/Node/NodeGroup.swift @@ -1,6 +1,6 @@ // // NodeGroup.swift -// +// // // Created by Andrew G on 30.10.2023. // diff --git a/CommonKit/Sources/CommonKit/Models/Node/NodeOrigin.swift b/CommonKit/Sources/CommonKit/Models/Node/NodeOrigin.swift index 004322791..864ecde22 100644 --- a/CommonKit/Sources/CommonKit/Models/Node/NodeOrigin.swift +++ b/CommonKit/Sources/CommonKit/Models/Node/NodeOrigin.swift @@ -12,7 +12,7 @@ public struct NodeOrigin: Codable, Equatable, Hashable, @unchecked Sendable { public var host: String public var port: Int? public var wsPort: Int? - + public init( scheme: URLScheme, host: String, @@ -26,8 +26,8 @@ public struct NodeOrigin: Codable, Equatable, Hashable, @unchecked Sendable { } } -public extension NodeOrigin { - enum URLScheme: String, Codable, Sendable { +extension NodeOrigin { + public enum URLScheme: String, Codable, Sendable { case http, https public static let `default`: URLScheme = .https @@ -39,34 +39,34 @@ public extension NodeOrigin { } } } - - init(url: URL) { + + public init(url: URL) { self.init( scheme: URLScheme(rawValue: url.scheme ?? .empty) ?? .https, host: url.host ?? .empty, port: url.port ) } - - func asString() -> String { + + public func asString() -> String { if let url = asURL(forcePort: scheme != .https) { return url.absoluteString } else { return host } } - - func asSocketURL() -> URL? { + + public func asSocketURL() -> URL? { asURL(forcePort: false, useWsPort: true) } - func asURL() -> URL? { + public func asURL() -> URL? { asURL(forcePort: true) } } -private extension NodeOrigin { - func asURL(forcePort: Bool, useWsPort: Bool = false) -> URL? { +extension NodeOrigin { + fileprivate func asURL(forcePort: Bool, useWsPort: Bool = false) -> URL? { var components = URLComponents() components.scheme = scheme.rawValue components.host = host diff --git a/CommonKit/Sources/CommonKit/Models/Node/NodeStatusInfo.swift b/CommonKit/Sources/CommonKit/Models/Node/NodeStatusInfo.swift index b4663d6c3..de99cd288 100644 --- a/CommonKit/Sources/CommonKit/Models/Node/NodeStatusInfo.swift +++ b/CommonKit/Sources/CommonKit/Models/Node/NodeStatusInfo.swift @@ -14,7 +14,7 @@ public struct NodeStatusInfo: Equatable, Sendable { public let wsEnabled: Bool public let wsPort: Int? public let version: Version? - + public init( ping: TimeInterval, height: Int, diff --git a/CommonKit/Sources/CommonKit/Models/NodesListInfo.swift b/CommonKit/Sources/CommonKit/Models/NodesListInfo.swift index 2599ff653..c417ebed0 100644 --- a/CommonKit/Sources/CommonKit/Models/NodesListInfo.swift +++ b/CommonKit/Sources/CommonKit/Models/NodesListInfo.swift @@ -10,6 +10,6 @@ import Foundation public struct NodesListInfo: Equatable, Sendable { public let nodes: [Node] public let chosenNodeId: UUID? - + static let `default` = Self(nodes: .init(), chosenNodeId: .none) } diff --git a/CommonKit/Sources/CommonKit/Models/RichAdditionalType.swift b/CommonKit/Sources/CommonKit/Models/RichAdditionalType.swift index 678fbecd5..fe4227079 100644 --- a/CommonKit/Sources/CommonKit/Models/RichAdditionalType.swift +++ b/CommonKit/Sources/CommonKit/Models/RichAdditionalType.swift @@ -1,6 +1,6 @@ // // RichAdditionalType.swift -// +// // // Created by Stanislav Jelezoglo on 01.08.2023. // diff --git a/CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatAsset.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatAsset.swift index 990131292..e259bba9d 100644 --- a/CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatAsset.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatAsset.swift @@ -10,13 +10,15 @@ import Foundation public struct ChatAsset: Codable, Hashable, Sendable { public enum CodingKeys: String, CodingKey { - case message, ownMessage = "own_message", type + case message + case ownMessage = "own_message" + case type } - + public let message: String public let ownMessage: String public let type: ChatType - + public init(message: String, ownMessage: String, type: ChatType) { self.message = message self.ownMessage = ownMessage diff --git a/CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatRooms.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatRooms.swift index 4aca0fb3c..f76972cc8 100644 --- a/CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatRooms.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatRooms.swift @@ -8,11 +8,11 @@ import Foundation -public struct ChatRooms : Codable, Sendable { +public struct ChatRooms: Codable, Sendable { public let chats: [ChatRoomsChats]? public let messages: [Transaction]? public let count: Int? - + public enum CodingKeys: String, CodingKey { case chats = "chats" case count = "count" diff --git a/CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatRoomsChats.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatRoomsChats.swift index 4b9c820d9..40041144d 100644 --- a/CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatRoomsChats.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatRoomsChats.swift @@ -8,9 +8,9 @@ import Foundation -public struct ChatRoomsChats : Codable, Sendable { - public let lastTransaction : Transaction? - +public struct ChatRoomsChats: Codable, Sendable { + public let lastTransaction: Transaction? + public enum CodingKeys: String, CodingKey { case lastTransaction = "lastTransaction" } diff --git a/CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatType.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatType.swift index 410abddf7..77fb8f12d 100644 --- a/CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatType.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/ChatType.swift @@ -14,22 +14,22 @@ import Foundation /// - signal: hidden system message for/from services public enum ChatType: Hashable, Sendable { case unknown(raw: Int) - case messageOld // 0 - case message // 1 - case richMessage // 2 - case signal // 3 - + case messageOld // 0 + case message // 1 + case richMessage // 2 + case signal // 3 + public init(from int: Int) { self = int.toChatType() } - + public var rawValue: Int { switch self { case .messageOld: return 0 case .message: return 1 case .richMessage: return 2 case .signal: return 3 - + case .unknown(let raw): return raw } } @@ -39,10 +39,10 @@ extension ChatType: Codable { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let type = try container.decode(Int.self) - + self = type.toChatType() } - + public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(rawValue) @@ -55,8 +55,8 @@ extension ChatType: Equatable { } } -private extension Int { - func toChatType() -> ChatType { +extension Int { + fileprivate func toChatType() -> ChatType { switch self { case 0: return .messageOld case 1: return .message diff --git a/CommonKit/Sources/CommonKit/Models/ServerDTOs/ContactDescription.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/ContactDescription.swift index 8ed993890..c9a1ca160 100644 --- a/CommonKit/Sources/CommonKit/Models/ServerDTOs/ContactDescription.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/ContactDescription.swift @@ -10,7 +10,7 @@ import Foundation public struct ContactDescription: Codable { public let displayName: String? - + public init(displayName: String?) { self.displayName = displayName } diff --git a/CommonKit/Sources/CommonKit/Models/ServerDTOs/Delegate.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/Delegate.swift index e12894f1c..a62842d19 100644 --- a/CommonKit/Sources/CommonKit/Models/ServerDTOs/Delegate.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/Delegate.swift @@ -20,9 +20,9 @@ public final class Delegate: Decodable, @unchecked Sendable { public let rank: Int public let approval: Double public let productivity: Double - + @Atomic public var voted: Bool = false - + public enum CodingKeys: String, CodingKey { case username case address @@ -36,7 +36,7 @@ public final class Delegate: Decodable, @unchecked Sendable { case approval case productivity } - + public init( username: String, address: String, @@ -79,29 +79,29 @@ public struct DelegateForgeDetails: Decodable, Sendable { public let fees: Decimal public let rewards: Decimal public let forged: Decimal - + public enum CodingKeys: String, CodingKey { case nodeTimestamp case fees case rewards case forged } - + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - + let feesStr = try container.decode(String.self, forKey: .fees) let fees = Decimal(string: feesStr) ?? 0 self.fees = fees.shiftedFromAdamant() - + let rewardsStr = try container.decode(String.self, forKey: .forged) let rewards = Decimal(string: rewardsStr) ?? 0 self.rewards = rewards.shiftedFromAdamant() - + let forgedStr = try container.decode(String.self, forKey: .forged) let forged = Decimal(string: forgedStr) ?? 0 self.forged = forged.shiftedFromAdamant() - + let timestamp = try container.decode(UInt64.self, forKey: .nodeTimestamp) self.nodeTimestamp = AdamantUtilities.decodeAdamant(timestamp: TimeInterval(timestamp)) } @@ -118,7 +118,7 @@ public struct NextForgersResult: Decodable, Sendable { public let currentBlockSlot: UInt64 public let currentSlot: UInt64 public let delegates: [String] - + public enum CodingKeys: String, CodingKey { case nodeTimestamp case currentBlock @@ -126,15 +126,15 @@ public struct NextForgersResult: Decodable, Sendable { case currentSlot case delegates } - + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - + self.currentBlock = try container.decode(UInt64.self, forKey: .currentBlock) self.currentBlockSlot = try container.decode(UInt64.self, forKey: .currentBlockSlot) self.currentSlot = try container.decode(UInt64.self, forKey: .currentSlot) self.delegates = try container.decode([String].self, forKey: .delegates) - + let timestamp = try container.decode(UInt64.self, forKey: .nodeTimestamp) self.nodeTimestamp = AdamantUtilities.decodeAdamant(timestamp: TimeInterval(timestamp)) } @@ -145,7 +145,7 @@ public struct Block: Decodable { public let version: UInt public let timestamp: UInt64 public let height: UInt64 - public let previousBlock:String + public let previousBlock: String public let numberOfTransactions: UInt public let totalAmount: UInt public let totalFee: UInt diff --git a/CommonKit/Sources/CommonKit/Models/ServerDTOs/GetPublicKeyResponse.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/GetPublicKeyResponse.swift index 711b1599e..32dd96dda 100644 --- a/CommonKit/Sources/CommonKit/Models/ServerDTOs/GetPublicKeyResponse.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/GetPublicKeyResponse.swift @@ -10,14 +10,14 @@ import Foundation public final class GetPublicKeyResponse: ServerResponse { public let publicKey: String? - + public required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let success = try container.decode(Bool.self, forKey: .success) let error = try? container.decode(String.self, forKey: .error) let nodeTimestamp = try container.decode(TimeInterval.self, forKey: CodingKeys.nodeTimestamp) self.publicKey = try? container.decode(String.self, forKey: CodingKeys.init(stringValue: "publicKey")!) - + super.init(success: success, error: error, nodeTimestamp: nodeTimestamp) } } diff --git a/CommonKit/Sources/CommonKit/Models/ServerDTOs/NodeStatus.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/NodeStatus.swift index e6759bb73..54dba3531 100644 --- a/CommonKit/Sources/CommonKit/Models/ServerDTOs/NodeStatus.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/NodeStatus.swift @@ -19,18 +19,18 @@ public struct NodeStatus: Codable, Sendable { public let reward: Int? public let supply: Int? } - + public struct Version: Codable, Sendable { public let build: String? public let commit: String? public let version: String? } - + public struct WsClient: Codable, Sendable { public let enabled: Bool? public let port: Int? } - + public let success: Bool public let nodeTimestamp: TimeInterval public let network: Network? diff --git a/CommonKit/Sources/CommonKit/Models/ServerDTOs/NormalizedTransaction.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/NormalizedTransaction.swift index 5bd7331e6..6b6289001 100644 --- a/CommonKit/Sources/CommonKit/Models/ServerDTOs/NormalizedTransaction.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/NormalizedTransaction.swift @@ -8,6 +8,7 @@ import Foundation +/// Used to make a KVC transactions into the blockchain public struct NormalizedTransaction: SignableTransaction { public let type: TransactionType public let amount: Decimal @@ -16,28 +17,10 @@ public struct NormalizedTransaction: SignableTransaction { public let timestamp: UInt64 public let recipientId: String? public let asset: TransactionAsset - - init( - type: TransactionType, - amount: Decimal, - senderPublicKey: String, - requesterPublicKey: String?, - timestamp: UInt64, - recipientId: String?, - asset: TransactionAsset - ) { - self.type = type - self.amount = amount - self.senderPublicKey = senderPublicKey - self.requesterPublicKey = requesterPublicKey - self.timestamp = timestamp - self.recipientId = recipientId - self.asset = asset - } } -public extension NormalizedTransaction { - init( +extension NormalizedTransaction { + public init( type: TransactionType, amount: Decimal, senderPublicKey: String, @@ -54,8 +37,8 @@ public extension NormalizedTransaction { self.recipientId = recipientId self.asset = asset } - - var date: Date { + + public var date: Date { return AdamantUtilities.decodeAdamant(timestamp: TimeInterval(timestamp)) } } @@ -70,17 +53,17 @@ extension NormalizedTransaction: Decodable { case recipientId case asset } - + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - + self.type = try container.decode(TransactionType.self, forKey: .type) self.senderPublicKey = try container.decode(String.self, forKey: .senderPublicKey) self.requesterPublicKey = try? container.decode(String.self, forKey: .requesterPublicKey) self.timestamp = try container.decode(UInt64.self, forKey: .timestamp) self.recipientId = try container.decode(String.self, forKey: .recipientId) self.asset = try container.decode(TransactionAsset.self, forKey: .asset) - + let amount = try container.decode(Decimal.self, forKey: .amount) self.amount = amount.shiftedFromAdamant() } diff --git a/CommonKit/Sources/CommonKit/Models/ServerDTOs/RPCResponseModel.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/RPCResponseModel.swift index 3e11c671d..b68deed76 100644 --- a/CommonKit/Sources/CommonKit/Models/ServerDTOs/RPCResponseModel.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/RPCResponseModel.swift @@ -11,18 +11,18 @@ import Foundation public struct RPCResponseModel: Codable, Sendable { public let id: String public let result: Data - + private enum CodingKeys: String, CodingKey { case id case result } - + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(String.self, forKey: .id) result = try container.decode(forKey: .result) } - + public func serialize() -> Response? { try? JSONDecoder().decode(Response.self, from: result) } diff --git a/CommonKit/Sources/CommonKit/Models/ServerDTOs/RichMessage.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/RichMessage.swift index 0742057f4..c38def5eb 100644 --- a/CommonKit/Sources/CommonKit/Models/ServerDTOs/RichMessage.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/RichMessage.swift @@ -13,7 +13,7 @@ import Foundation public protocol RichMessage: Encodable, Sendable { var type: String { get } var additionalType: RichAdditionalType { get } - + func content() -> [String: Any] func serialized() -> String } @@ -31,21 +31,21 @@ extension RichMessage { public enum RichContentKeys { public static let type = "type" public static let hash = "hash" - + public enum reply { public static let reply = "reply" public static let replyToId = "replyto_id" public static let replyMessage = "reply_message" public static let decodedReplyMessage = "decodedMessage" } - + public enum react { public static let react = "react" public static let reactto_id = "reactto_id" public static let react_message = "react_message" public static let reactions = "reactions" } - + public enum file { public static let file = "file" public static let files = "files" @@ -72,18 +72,18 @@ public struct RichMessageReaction: RichMessage, @unchecked Sendable { public var additionalType: RichAdditionalType public var reactto_id: String public var react_message: String - + public enum CodingKeys: String, CodingKey { case reactto_id, react_message } - + public init(reactto_id: String, react_message: String) { self.type = RichContentKeys.reply.reply self.reactto_id = reactto_id self.react_message = react_message self.additionalType = .reaction } - + public func content() -> [String: Any] { return [ RichContentKeys.react.reactto_id: reactto_id, @@ -99,7 +99,7 @@ public struct RichMessageFile: RichMessage, @unchecked Sendable { public var id: String public var nonce: String public var `extension`: String? - + public init( id: String, nonce: String, @@ -109,32 +109,32 @@ public struct RichMessageFile: RichMessage, @unchecked Sendable { self.nonce = nonce self.extension = `extension` } - + public init(_ data: [String: Any]) { self.id = (data[RichContentKeys.file.id] as? String) ?? .empty self.nonce = data[RichContentKeys.file.nonce] as? String ?? .empty self.extension = data[RichContentKeys.file.extension] as? String ?? .empty } - + public func content() -> [String: Any] { - var contentDict: [String : Any] = [:] - + var contentDict: [String: Any] = [:] + if !id.isEmpty { contentDict[RichContentKeys.file.id] = id } - + if !nonce.isEmpty { contentDict[RichContentKeys.file.nonce] = nonce } - + if !nonce.isEmpty { contentDict[RichContentKeys.file.extension] = `extension` } - + return contentDict } } - + public struct File: Codable, Equatable, Hashable { public var preview: Preview? public var id: String @@ -145,7 +145,7 @@ public struct RichMessageFile: RichMessage, @unchecked Sendable { public var resolution: CGSize? public var name: String? public var duration: Float64? - + public init( id: String, size: Int64, @@ -167,21 +167,22 @@ public struct RichMessageFile: RichMessage, @unchecked Sendable { self.resolution = resolution self.duration = duration } - + public init(_ data: [String: Any]) { self.id = (data[RichContentKeys.file.id] as? String) ?? .empty - self.`extension` = data[RichContentKeys.file.extension] as? String - ?? data[RichContentKeys.file.type] as? String + self.`extension` = + data[RichContentKeys.file.extension] as? String + ?? data[RichContentKeys.file.type] as? String self.size = (data[RichContentKeys.file.size] as? Int64) ?? .zero self.name = data[RichContentKeys.file.name] as? String self.nonce = data[RichContentKeys.file.nonce] as? String ?? .empty self.duration = data[RichContentKeys.file.duration] as? Float64 self.mimeType = data[RichContentKeys.file.mimeType] as? String - + if let previewData = data[RichContentKeys.file.preview] as? [String: Any] { self.preview = Preview(previewData) } - + if let resolution = data[RichContentKeys.file.resolution] as? [CGFloat] { self.resolution = .init( width: resolution.first ?? .zero, @@ -193,72 +194,72 @@ public struct RichMessageFile: RichMessage, @unchecked Sendable { self.resolution = nil } } - + public func content() -> [String: Any] { - var contentDict: [String : Any] = [ + var contentDict: [String: Any] = [ RichContentKeys.file.id: id, RichContentKeys.file.size: size, RichContentKeys.file.nonce: nonce ] - + if let value = `extension`, !value.isEmpty { contentDict[RichContentKeys.file.extension] = value } - + if let preview = preview { contentDict[RichContentKeys.file.preview] = preview.content() } - + if let name = name, !name.isEmpty { contentDict[RichContentKeys.file.name] = name } - + if let resolution = resolution { contentDict[RichContentKeys.file.resolution] = resolution } - + if let duration = duration { contentDict[RichContentKeys.file.duration] = duration } - + if let mimeType = mimeType { contentDict[RichContentKeys.file.mimeType] = mimeType } - + return contentDict } } - + public struct Storage: Codable, Equatable, Hashable { public var id: String - + public init(id: String) { self.id = id } - + public init(_ data: [String: Any]) { self.id = (data[RichContentKeys.file.id] as? String) ?? .empty } - + public func content() -> [String: Any] { - let contentDict: [String : Any] = [ + let contentDict: [String: Any] = [ RichContentKeys.file.id: id ] - + return contentDict } } - + public var type: String public var additionalType: RichAdditionalType public var files: [File] public var storage: Storage public var comment: String? - + public enum CodingKeys: String, CodingKey { case files, storage, comment } - + public init(files: [File], storage: Storage, comment: String?) { self.type = RichContentKeys.file.file self.files = files @@ -266,13 +267,13 @@ public struct RichMessageFile: RichMessage, @unchecked Sendable { self.comment = comment self.additionalType = .file } - + public func content() -> [String: Any] { - var contentDict: [String : Any] = [ + var contentDict: [String: Any] = [ RichContentKeys.file.files: files.map { $0.content() }, RichContentKeys.file.storage: storage.content() ] - + if let comment = comment, !comment.isEmpty { contentDict[RichContentKeys.file.comment] = comment } @@ -285,18 +286,18 @@ public struct RichFileReply: RichMessage, @unchecked Sendable { public var additionalType: RichAdditionalType public var replyto_id: String public var reply_message: RichMessageFile - + public enum CodingKeys: String, CodingKey { case replyto_id, reply_message } - + public init(replyto_id: String, reply_message: RichMessageFile) { self.type = RichContentKeys.reply.reply self.replyto_id = replyto_id self.reply_message = reply_message self.additionalType = .reply } - + public func content() -> [String: Any] { return [ RichContentKeys.reply.replyToId: replyto_id, @@ -312,18 +313,18 @@ public struct RichMessageReply: RichMessage, @unchecked Sendable { public var additionalType: RichAdditionalType public var replyto_id: String public var reply_message: String - + public enum CodingKeys: String, CodingKey { case replyto_id, reply_message } - + public init(replyto_id: String, reply_message: String) { self.type = RichContentKeys.reply.reply self.replyto_id = replyto_id self.reply_message = reply_message self.additionalType = .reply } - + public func content() -> [String: Any] { return [ RichContentKeys.reply.replyToId: replyto_id, @@ -337,11 +338,11 @@ public struct RichTransferReply: RichMessage, @unchecked Sendable { public var additionalType: RichAdditionalType public var replyto_id: String public var reply_message: [String: String] - + public enum CodingKeys: String, CodingKey { case replyto_id, reply_message } - + public init( replyto_id: String, type: String, @@ -359,7 +360,7 @@ public struct RichTransferReply: RichMessage, @unchecked Sendable { ] self.additionalType = .reply } - + public func content() -> [String: Any] { return [ RichContentKeys.reply.replyToId: replyto_id, @@ -376,7 +377,7 @@ public struct RichMessageTransfer: RichMessage, @unchecked Sendable { public let hash: String public let comments: String public var additionalType: RichAdditionalType - + public func content() -> [String: Any] { return [ CodingKeys.type.stringValue: type, @@ -385,7 +386,7 @@ public struct RichMessageTransfer: RichMessage, @unchecked Sendable { CodingKeys.comments.stringValue: comments ] } - + public init(type: String, amount: Decimal, hash: String, comments: String) { self.type = type self.amount = amount @@ -393,38 +394,38 @@ public struct RichMessageTransfer: RichMessage, @unchecked Sendable { self.comments = comments self.additionalType = .base } - + public init?(content: [String: Any]) { var newContent = content - + if let content = content[RichContentKeys.reply.replyMessage] as? [String: String] { self.init(content: content) } else { newContent[RichContentKeys.react.reactions] = "" - + guard let content = newContent as? [String: String] else { return nil } - + self.init(content: content) } } - - public init?(content: [String:String]) { + + public init?(content: [String: String]) { guard let type = content[CodingKeys.type.stringValue] else { return nil } - + guard let hash = content[CodingKeys.hash.stringValue] else { return nil } - + self.type = type self.hash = hash - + if let raw = content[CodingKeys.amount.stringValue] { // NumberFormatter.number(from: string).decimalValue loses precision. - + if let number = Decimal(string: raw), number != 0.0 { self.amount = number } else if let number = Decimal(string: raw, locale: Locale.current), number != 0.0 { @@ -439,44 +440,44 @@ public struct RichMessageTransfer: RichMessage, @unchecked Sendable { } else { self.amount = 0 } - + if let comments = content[CodingKeys.comments.stringValue] { self.comments = comments } else { self.comments = "" } - + self.additionalType = .base } } -public extension RichContentKeys { - enum transfer { +extension RichContentKeys { + public enum transfer { public static let amount = "amount" public static let hash = "hash" public static let comments = "comments" } } -public extension RichMessageTransfer { - enum CodingKeys: String, CodingKey { +extension RichMessageTransfer { + public enum CodingKeys: String, CodingKey { case type, amount, hash, comments } - - func encode(to encoder: Encoder) throws { + + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(type, forKey: .type) try container.encode(hash, forKey: .hash) try container.encode(comments, forKey: .comments) try container.encode(amount, forKey: .amount) } - - init(from decoder: Decoder) throws { + + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) self.type = try container.decode(String.self, forKey: .type) self.hash = try container.decode(String.self, forKey: .hash) self.comments = try container.decode(String.self, forKey: .comments) - + if let raw = try? container.decode(String.self, forKey: .amount) { if let balance = AdamantBalanceFormat.deserializeBalance(from: raw) { self.amount = balance @@ -488,13 +489,13 @@ public extension RichMessageTransfer { } else { self.amount = 0 } - + self.additionalType = .base } } -public extension RichMessageTransfer { - static func serialize(balance: Decimal) -> String { +extension RichMessageTransfer { + public static func serialize(balance: Decimal) -> String { return AdamantBalanceFormat.rawNumberDotFormatter.string(from: balance) ?? "0" } } diff --git a/CommonKit/Sources/CommonKit/Models/ServerDTOs/RpcRequestModel.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/RpcRequestModel.swift index 15a20106e..a9d0787fc 100644 --- a/CommonKit/Sources/CommonKit/Models/ServerDTOs/RpcRequestModel.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/RpcRequestModel.swift @@ -13,19 +13,19 @@ public struct RpcRequest: Encodable, Sendable { public let id: String public let params: [Parameter] public let jsonrpc: String = "2.0" - + public init(method: String, id: String, params: [Parameter]) { self.method = method self.id = id self.params = params } - + public init(method: String, params: [Parameter]) { self.method = method self.id = method self.params = params } - + public init(method: String) { self.method = method self.id = method @@ -33,8 +33,8 @@ public struct RpcRequest: Encodable, Sendable { } } -public extension RpcRequest { - enum Parameter: Sendable { +extension RpcRequest { + public enum Parameter: Sendable { case string(String) case bool(Bool) } @@ -43,7 +43,7 @@ public extension RpcRequest { extension RpcRequest.Parameter: Encodable { public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - + switch self { case let .string(value): try container.encode(value) diff --git a/CommonKit/Sources/CommonKit/Models/ServerDTOs/ServerResponse.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/ServerResponse.swift index 129d0ed11..99bdf92d3 100644 --- a/CommonKit/Sources/CommonKit/Models/ServerDTOs/ServerResponse.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/ServerResponse.swift @@ -20,34 +20,34 @@ open class ServerResponse: Decodable, @unchecked Sendable { public struct CodingKeys: CodingKey { public var intValue: Int? public var stringValue: String - + public init?(intValue: Int) { self.intValue = intValue self.stringValue = "\(intValue)" } - + public init?(stringValue: String) { self.stringValue = stringValue } - + public static let success = CodingKeys(stringValue: "success")! public static let error = CodingKeys(stringValue: "error")! public static let nodeTimestamp = CodingKeys(stringValue: "nodeTimestamp")! } - + public let success: Bool public let error: String? public let nodeTimestamp: TimeInterval - + public init(success: Bool, error: String?, nodeTimestamp: TimeInterval) { self.success = success self.error = error self.nodeTimestamp = nodeTimestamp } - + required public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - + self.success = try container.decode(Bool.self, forKey: CodingKeys.success) self.error = try? container.decode(String.self, forKey: CodingKeys.error) self.nodeTimestamp = try container.decode(TimeInterval.self, forKey: CodingKeys.nodeTimestamp) @@ -56,28 +56,28 @@ open class ServerResponse: Decodable, @unchecked Sendable { public final class ServerModelResponse: ServerResponse, @unchecked Sendable { public let model: T? - + required public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let success = try container.decode(Bool.self, forKey: CodingKeys.success) let nodeTimestamp = try container.decode(TimeInterval.self, forKey: CodingKeys.nodeTimestamp) let error = try? container.decode(String.self, forKey: CodingKeys.error) self.model = try? container.decode(T.self, forKey: CodingKeys(stringValue: T.ModelKey)!) - + super.init(success: success, error: error, nodeTimestamp: nodeTimestamp) } } public final class ServerCollectionResponse: ServerResponse, @unchecked Sendable { public let collection: [T]? - + required public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let success = try container.decode(Bool.self, forKey: CodingKeys.success) let error = try? container.decode(String.self, forKey: CodingKeys.error) let nodeTimestamp = try container.decode(TimeInterval.self, forKey: CodingKeys.nodeTimestamp) self.collection = try? container.decode([T].self, forKey: CodingKeys(stringValue: T.CollectionKey)!) - + super.init(success: success, error: error, nodeTimestamp: nodeTimestamp) } } diff --git a/CommonKit/Sources/CommonKit/Models/ServerDTOs/StateAsset.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/StateAsset.swift index aeb9b5131..c495ae19b 100644 --- a/CommonKit/Sources/CommonKit/Models/ServerDTOs/StateAsset.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/StateAsset.swift @@ -12,7 +12,7 @@ public struct StateAsset: Codable, Hashable, Sendable { public let key: String public let value: String public let type: StateType - + public init(key: String, value: String, type: StateType) { self.key = key self.value = value diff --git a/CommonKit/Sources/CommonKit/Models/ServerDTOs/StateType.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/StateType.swift index fd8d46470..160da47ce 100644 --- a/CommonKit/Sources/CommonKit/Models/ServerDTOs/StateType.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/StateType.swift @@ -10,8 +10,8 @@ import Foundation public enum StateType: Equatable, Hashable, Sendable { case unknown(raw: Int) - case keyValue // 0 - + case keyValue // 0 + public var rawValue: Int { switch self { case .keyValue: return 0 @@ -26,18 +26,18 @@ extension StateType: Codable { let type = try container.decode(Int.self) self = type.toStateType() } - + public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(self.rawValue) } } -private extension Int { - func toStateType() -> StateType { +extension Int { + fileprivate func toStateType() -> StateType { switch self { case 0: return .keyValue - + default: return .unknown(raw: self) } } diff --git a/CommonKit/Sources/CommonKit/Models/ServerDTOs/Transaction.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/Transaction.swift index 2a19cc909..17ff47602 100644 --- a/CommonKit/Sources/CommonKit/Models/ServerDTOs/Transaction.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/Transaction.swift @@ -14,6 +14,7 @@ public struct Transaction: Sendable { public let blockId: String public let type: TransactionType public let timestamp: UInt64 + public let timestampMs: UInt64 public let senderPublicKey: String public let senderId: String public let requesterPublicKey: String? @@ -26,8 +27,8 @@ public struct Transaction: Sendable { public let confirmations: Int64 public let signatures: [String] public let asset: TransactionAsset - - public let date: Date // Calculated from timestamp + + public let date: Date // Calculated from timestamp } extension Transaction: Codable { @@ -37,6 +38,7 @@ extension Transaction: Codable { case blockId case type case timestamp + case timestampMs case senderPublicKey case senderId case requesterPublicKey @@ -50,15 +52,14 @@ extension Transaction: Codable { case signatures case asset } - + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - + self.id = UInt64(try container.decode(String.self, forKey: .id))! self.height = (try? container.decode(Int64.self, forKey: .height)) ?? 0 self.blockId = (try? container.decode(String.self, forKey: .blockId)) ?? "" self.type = try container.decode(TransactionType.self, forKey: .type) - self.timestamp = try container.decode(UInt64.self, forKey: .timestamp) self.senderPublicKey = try container.decode(String.self, forKey: .senderPublicKey) self.senderId = try container.decode(String.self, forKey: .senderId) self.recipientId = (try? container.decode(String.self, forKey: .recipientId)) ?? "" @@ -69,39 +70,43 @@ extension Transaction: Codable { self.signSignature = try? container.decode(String.self, forKey: .signSignature) self.signatures = (try? container.decode([String].self, forKey: .signatures)) ?? [] self.asset = try container.decode(TransactionAsset.self, forKey: .asset) - + let amount = try container.decode(Decimal.self, forKey: .amount) self.amount = amount.shiftedFromAdamant() - + let fee = try container.decode(Decimal.self, forKey: .fee) self.fee = fee.shiftedFromAdamant() - self.date = AdamantUtilities.decodeAdamant(timestamp: TimeInterval(self.timestamp)) + let timestamp = try container.decode(UInt64.self, forKey: .timestamp) + self.timestamp = timestamp + UInt64(AdamantUtilities.magicAdamantTimeInterval) + self.timestampMs = (try? container.decodeIfPresent(UInt64.self, forKey: .timestampMs)) ?? self.timestamp * 1000 + self.date = AdamantUtilities.decodeAdamant(timestamp: TimeInterval(timestamp)) } - + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(String(id), forKey: .id) // String - try container.encode(height, forKey: .height) // UInt64 - try container.encode(blockId, forKey: .blockId) // String - try container.encode(type, forKey: .type) // TransactionType - try container.encode(timestamp, forKey: .timestamp) // UInt64 - try container.encode(senderPublicKey, forKey: .senderPublicKey) // String - try container.encode(senderId, forKey: .senderId) // String - try container.encode(recipientId, forKey: .recipientId) // String? - try container.encode(recipientPublicKey, forKey: .recipientPublicKey) // String - try container.encode(signature, forKey: .signature) // String - try container.encode(requesterPublicKey, forKey: .requesterPublicKey) // String? - try container.encode(signatures, forKey: .signatures) // [String] - try container.encode(asset, forKey: .asset) // TransactionAsset - try container.encode(signSignature, forKey: .signSignature) // String? + + try container.encode(String(id), forKey: .id) // String + try container.encode(height, forKey: .height) // UInt64 + try container.encode(blockId, forKey: .blockId) // String + try container.encode(type, forKey: .type) // TransactionType + try container.encode(timestamp, forKey: .timestamp) // UInt64 + try container.encode(timestampMs, forKey: .timestampMs) // UInt64 + try container.encode(senderPublicKey, forKey: .senderPublicKey) // String + try container.encode(senderId, forKey: .senderId) // String + try container.encode(recipientId, forKey: .recipientId) // String? + try container.encode(recipientPublicKey, forKey: .recipientPublicKey) // String + try container.encode(signature, forKey: .signature) // String + try container.encode(requesterPublicKey, forKey: .requesterPublicKey) // String? + try container.encode(signatures, forKey: .signatures) // [String] + try container.encode(asset, forKey: .asset) // TransactionAsset + try container.encode(signSignature, forKey: .signSignature) // String? try container.encode(confirmations > .zero ? confirmations : nil, forKey: .confirmations) - - try container.encode(amount.shiftedToAdamant(), forKey: .amount) // Decimal - try container.encode(fee.shiftedToAdamant(), forKey: .fee) // Decimal + + try container.encode(amount.shiftedToAdamant(), forKey: .amount) // Decimal + try container.encode(fee.shiftedToAdamant(), forKey: .fee) // Decimal } - + } extension Transaction: WrappableModel { diff --git a/CommonKit/Sources/CommonKit/Models/ServerDTOs/TransactionAsset.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/TransactionAsset.swift index 45e155158..09475b997 100644 --- a/CommonKit/Sources/CommonKit/Models/ServerDTOs/TransactionAsset.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/TransactionAsset.swift @@ -12,7 +12,7 @@ public struct TransactionAsset: Codable, Hashable, Sendable { public let chat: ChatAsset? public let state: StateAsset? public let votes: VotesAsset? - + public init(chat: ChatAsset? = nil, state: StateAsset? = nil, votes: VotesAsset? = nil) { self.chat = chat self.state = state diff --git a/CommonKit/Sources/CommonKit/Models/ServerDTOs/TransactionIdResponse.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/TransactionIdResponse.swift index 49ca1d0ca..8529700af 100644 --- a/CommonKit/Sources/CommonKit/Models/ServerDTOs/TransactionIdResponse.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/TransactionIdResponse.swift @@ -10,19 +10,19 @@ import Foundation public final class TransactionIdResponse: ServerResponse { public let transactionId: UInt64? - + public required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let success = try container.decode(Bool.self, forKey: .success) let error = try? container.decode(String.self, forKey: .error) let nodeTimestamp = try container.decode(TimeInterval.self, forKey: .nodeTimestamp) - + if let idRaw = try? container.decode(String.self, forKey: CodingKeys(stringValue: "transactionId")!) { transactionId = UInt64(idRaw) } else { transactionId = nil } - + super.init(success: success, error: error, nodeTimestamp: nodeTimestamp) } } diff --git a/CommonKit/Sources/CommonKit/Models/ServerDTOs/TransactionType.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/TransactionType.swift index 87eb8b369..d66baab88 100644 --- a/CommonKit/Sources/CommonKit/Models/ServerDTOs/TransactionType.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/TransactionType.swift @@ -10,21 +10,21 @@ import Foundation public enum TransactionType: Hashable, Sendable { case unknown(raw: Int) - case send // 0 - case signature // 1 - case delegate // 2 - case vote // 3 - case multi // 4 - case dapp // 5 - case inTransfer // 6 - case outTransfer // 7 - case chatMessage // 8 - case state // 9 - + case send // 0 + case signature // 1 + case delegate // 2 + case vote // 3 + case multi // 4 + case dapp // 5 + case inTransfer // 6 + case outTransfer // 7 + case chatMessage // 8 + case state // 9 + public init(from int: Int) { self = int.toTransactionType() } - + public var rawValue: Int { switch self { case .send: return 0 @@ -37,7 +37,7 @@ public enum TransactionType: Hashable, Sendable { case .outTransfer: return 7 case .chatMessage: return 8 case .state: return 9 - + case .unknown(let raw): return raw } } @@ -49,7 +49,7 @@ extension TransactionType: Codable { let type = try container.decode(Int.self) self = type.toTransactionType() } - + public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(self.rawValue) @@ -62,8 +62,8 @@ extension TransactionType: Equatable { } } -private extension Int { - func toTransactionType() -> TransactionType { +extension Int { + fileprivate func toTransactionType() -> TransactionType { switch self { case 0: return .send case 1: return .signature @@ -75,7 +75,7 @@ private extension Int { case 7: return .outTransfer case 8: return .chatMessage case 9: return .state - + default: return .unknown(raw: self) } } diff --git a/CommonKit/Sources/CommonKit/Models/ServerDTOs/UnregisteredTransaction.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/UnregisteredTransaction.swift index dc5b00163..7442d9e18 100644 --- a/CommonKit/Sources/CommonKit/Models/ServerDTOs/UnregisteredTransaction.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/UnregisteredTransaction.swift @@ -6,8 +6,8 @@ // Copyright © 2022 Adamant. All rights reserved. // -import Foundation import BigInt +import Foundation public struct UnregisteredTransaction: Hashable, Sendable { public let type: TransactionType @@ -19,7 +19,7 @@ public struct UnregisteredTransaction: Hashable, Sendable { public let signature: String public let asset: TransactionAsset public let requesterPublicKey: String? - + public init( type: TransactionType, timestamp: UInt64, @@ -54,10 +54,10 @@ extension UnregisteredTransaction: Codable { case signature case asset } - + public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - + self.type = try container.decode(TransactionType.self, forKey: .type) self.timestamp = try container.decode(UInt64.self, forKey: .timestamp) self.senderPublicKey = try container.decode(String.self, forKey: .senderPublicKey) @@ -65,23 +65,23 @@ extension UnregisteredTransaction: Codable { self.recipientId = try? container.decode(String.self, forKey: .recipientId) self.signature = (try? container.decode(String.self, forKey: .signature)) ?? "" self.asset = try container.decode(TransactionAsset.self, forKey: .asset) - + let amount = try container.decode(Decimal.self, forKey: .amount) self.amount = amount.shiftedFromAdamant() self.requesterPublicKey = "" } - + public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - - try container.encode(type, forKey: .type) // TransactionType - try container.encode(timestamp, forKey: .timestamp) // UInt64 - try container.encode(senderPublicKey, forKey: .senderPublicKey) // String - try container.encode(senderId, forKey: .senderId) // String - try container.encode(recipientId, forKey: .recipientId) // String? - try container.encode(signature, forKey: .signature) // String - try container.encode(asset, forKey: .asset) // TransactionAsset - try container.encode(amount.shiftedToAdamant(), forKey: .amount) // Decimal + + try container.encode(type, forKey: .type) // TransactionType + try container.encode(timestamp, forKey: .timestamp) // UInt64 + try container.encode(senderPublicKey, forKey: .senderPublicKey) // String + try container.encode(senderId, forKey: .senderId) // String + try container.encode(recipientId, forKey: .recipientId) // String? + try container.encode(signature, forKey: .signature) // String + try container.encode(asset, forKey: .asset) // TransactionAsset + try container.encode(amount.shiftedToAdamant(), forKey: .amount) // Decimal } - + } diff --git a/CommonKit/Sources/CommonKit/Models/ServerDTOs/VotesAsset.swift b/CommonKit/Sources/CommonKit/Models/ServerDTOs/VotesAsset.swift index b31b8d4bd..15f1613a7 100644 --- a/CommonKit/Sources/CommonKit/Models/ServerDTOs/VotesAsset.swift +++ b/CommonKit/Sources/CommonKit/Models/ServerDTOs/VotesAsset.swift @@ -10,11 +10,11 @@ import Foundation public struct VotesAsset: Hashable, Sendable { public let votes: [String] - + public init(votes: [String]) { self.votes = votes } - + public init(votes: [DelegateVote]) { self.votes = votes.map { $0.asString() } } @@ -25,7 +25,7 @@ extension VotesAsset: Codable { let container = try decoder.singleValueContainer() self.votes = try container.decode([String].self) } - + public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(votes) diff --git a/CommonKit/Sources/CommonKit/Models/ethereumTokensList.swift b/CommonKit/Sources/CommonKit/Models/ethereumTokensList.swift deleted file mode 100644 index defd1f647..000000000 --- a/CommonKit/Sources/CommonKit/Models/ethereumTokensList.swift +++ /dev/null @@ -1,385 +0,0 @@ - import Foundation - - public extension ERC20Token { - static let supportedTokens: [ERC20Token] = [ - - ERC20Token(symbol: "BNB", - name: "Binance Coin", - contractAddress: "0xB8c77482e45F1F44dE1745F52C74426C631bDD52", - decimals: 18, - naturalUnits: 18, - defaultVisibility: false, - defaultOrdinalLevel: nil, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "BUSD", - name: "Binance USD", - contractAddress: "0x4fabb145d64652a948d72533023f6e7a623c7c53", - decimals: 18, - naturalUnits: 18, - defaultVisibility: false, - defaultOrdinalLevel: nil, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "BZZ", - name: "Swarm", - contractAddress: "0x19062190B1925b5b6689D7073fDfC8c2976EF8Cb", - decimals: 16, - naturalUnits: 16, - defaultVisibility: false, - defaultOrdinalLevel: 95, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "DAI", - name: "Dai", - contractAddress: "0x6b175474e89094c44da98b954eedeac495271d0f", - decimals: 18, - naturalUnits: 18, - defaultVisibility: false, - defaultOrdinalLevel: 80, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "ENS", - name: "Ethereum Name Service", - contractAddress: "0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72", - decimals: 18, - naturalUnits: 18, - defaultVisibility: false, - defaultOrdinalLevel: nil, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "FLOKI", - name: "Floki", - contractAddress: "0xcf0c122c6b73ff809c693db761e7baebe62b6a2e", - decimals: 9, - naturalUnits: 9, - defaultVisibility: false, - defaultOrdinalLevel: 100, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "FLUX", - name: "Flux", - contractAddress: "0x720CD16b011b987Da3518fbf38c3071d4F0D1495", - decimals: 8, - naturalUnits: 8, - defaultVisibility: false, - defaultOrdinalLevel: 90, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "GT", - name: "Gate", - contractAddress: "0xe66747a101bff2dba3697199dcce5b743b454759", - decimals: 18, - naturalUnits: 18, - defaultVisibility: false, - defaultOrdinalLevel: 115, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "HOT", - name: "Holo", - contractAddress: "0x6c6ee5e31d828de241282b9606c8e98ea48526e2", - decimals: 18, - naturalUnits: 18, - defaultVisibility: false, - defaultOrdinalLevel: nil, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "INJ", - name: "Injective", - contractAddress: "0xe28b3b32b6c345a34ff64674606124dd5aceca30", - decimals: 18, - naturalUnits: 18, - defaultVisibility: false, - defaultOrdinalLevel: nil, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "LINK", - name: "Chainlink", - contractAddress: "0x514910771af9ca656af840dff83e8264ecf986ca", - decimals: 18, - naturalUnits: 18, - defaultVisibility: false, - defaultOrdinalLevel: nil, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "MANA", - name: "Decentraland", - contractAddress: "0x0f5d2fb29fb7d3cfee444a200298f468908cc942", - decimals: 18, - naturalUnits: 18, - defaultVisibility: false, - defaultOrdinalLevel: nil, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "MATIC", - name: "Polygon", - contractAddress: "0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0", - decimals: 18, - naturalUnits: 18, - defaultVisibility: false, - defaultOrdinalLevel: nil, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "PAXG", - name: "PAX Gold", - contractAddress: "0x45804880de22913dafe09f4980848ece6ecbaf78", - decimals: 18, - naturalUnits: 18, - defaultVisibility: false, - defaultOrdinalLevel: nil, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "QNT", - name: "Quant", - contractAddress: "0x4a220E6096B25EADb88358cb44068A3248254675", - decimals: 18, - naturalUnits: 18, - defaultVisibility: false, - defaultOrdinalLevel: nil, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "REN", - name: "Ren", - contractAddress: "0x408e41876cccdc0f92210600ef50372656052a38", - decimals: 18, - naturalUnits: 18, - defaultVisibility: false, - defaultOrdinalLevel: nil, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "SKL", - name: "SKALE", - contractAddress: "0x00c83aecc790e8a4453e5dd3b0b4b3680501a7a7", - decimals: 18, - naturalUnits: 18, - defaultVisibility: false, - defaultOrdinalLevel: 85, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "SNT", - name: "Status", - contractAddress: "0x744d70fdbe2ba4cf95131626614a1763df805b9e", - decimals: 18, - naturalUnits: 18, - defaultVisibility: false, - defaultOrdinalLevel: nil, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "SNX", - name: "Synthetix Network", - contractAddress: "0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f", - decimals: 18, - naturalUnits: 18, - defaultVisibility: false, - defaultOrdinalLevel: nil, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "STORJ", - name: "Storj", - contractAddress: "0xb64ef51c888972c908cfacf59b47c1afbc0ab8ac", - decimals: 8, - naturalUnits: 8, - defaultVisibility: false, - defaultOrdinalLevel: 105, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "TUSD", - name: "TrueUSD", - contractAddress: "0x0000000000085d4780b73119b644ae5ecd22b376", - decimals: 18, - naturalUnits: 18, - defaultVisibility: false, - defaultOrdinalLevel: nil, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "UNI", - name: "Uniswap", - contractAddress: "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", - decimals: 18, - naturalUnits: 18, - defaultVisibility: false, - defaultOrdinalLevel: nil, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "USDC", - name: "USD Coin", - contractAddress: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", - decimals: 6, - naturalUnits: 6, - defaultVisibility: true, - defaultOrdinalLevel: 40, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "USDP", - name: "PAX Dollar", - contractAddress: "0x8e870d67f660d95d5be530380d0ec0bd388289e1", - decimals: 18, - naturalUnits: 18, - defaultVisibility: false, - defaultOrdinalLevel: nil, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "USDS", - name: "Stably USD", - contractAddress: "0xa4bdb11dc0a2bec88d24a3aa1e6bb17201112ebe", - decimals: 6, - naturalUnits: 6, - defaultVisibility: false, - defaultOrdinalLevel: nil, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "USDT", - name: "Tether", - contractAddress: "0xdac17f958d2ee523a2206206994597c13d831ec7", - decimals: 6, - naturalUnits: 6, - defaultVisibility: true, - defaultOrdinalLevel: 30, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "VERSE", - name: "Verse", - contractAddress: "0x249cA82617eC3DfB2589c4c17ab7EC9765350a18", - decimals: 18, - naturalUnits: 18, - defaultVisibility: false, - defaultOrdinalLevel: 95, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "WOO", - name: "WOO Network", - contractAddress: "0x4691937a7508860f876c9c0a2a617e7d9e945d4b", - decimals: 18, - naturalUnits: 18, - defaultVisibility: false, - defaultOrdinalLevel: nil, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ERC20Token(symbol: "XCN", - name: "Onyxcoin", - contractAddress: "0xa2cd3d43c775978a96bdbf12d733d5a1ed94fb18", - decimals: 18, - naturalUnits: 18, - defaultVisibility: false, - defaultOrdinalLevel: 110, - reliabilityGasPricePercent: 10, - reliabilityGasLimitPercent: 10, - defaultGasPriceGwei: 10, - defaultGasLimit: 58000, - warningGasPriceGwei: 25, - transferDecimals: 6), - ] - -} diff --git a/CommonKit/Sources/CommonKit/Protocols/APICoreProtocol.swift b/CommonKit/Sources/CommonKit/Protocols/APICoreProtocol.swift index 05dbdaad1..3a212ac05 100644 --- a/CommonKit/Sources/CommonKit/Protocols/APICoreProtocol.swift +++ b/CommonKit/Sources/CommonKit/Protocols/APICoreProtocol.swift @@ -6,8 +6,8 @@ // Copyright © 2023 Adamant. All rights reserved. // -import Foundation import Alamofire +import Foundation public enum ApiCommands {} @@ -16,6 +16,7 @@ public enum TimeoutSize: CaseIterable, Hashable { case extended } +// sourcery: AutoMockable public protocol APICoreProtocol: Actor { func sendRequestMultipartFormData( origin: NodeOrigin, @@ -24,34 +25,34 @@ public protocol APICoreProtocol: Actor { timeout: TimeoutSize, uploadProgress: @escaping ((Progress) -> Void) ) async -> APIResponseModel - + func sendRequestBasic( origin: NodeOrigin, path: String, - method: HTTPMethod, + method: Alamofire.HTTPMethod, parameters: Parameters, encoding: APIParametersEncoding, timeout: TimeoutSize, downloadProgress: @escaping ((Progress) -> Void) ) async -> APIResponseModel - + /// jsonParameters - arrays and dictionaries are allowed only func sendRequestBasic( origin: NodeOrigin, path: String, - method: HTTPMethod, + method: Alamofire.HTTPMethod, jsonParameters: Any, timeout: TimeoutSize ) async -> APIResponseModel } -public extension APICoreProtocol { - var emptyParameters: [String: Bool] { [:] } - - func sendRequest( +extension APICoreProtocol { + public var emptyParameters: [String: Bool] { [:] } + + public func sendRequest( origin: NodeOrigin, path: String, - method: HTTPMethod, + method: Alamofire.HTTPMethod, parameters: Parameters, encoding: APIParametersEncoding ) async -> ApiServiceResult { @@ -65,11 +66,11 @@ public extension APICoreProtocol { downloadProgress: { _ in } ).result } - - func sendRequest( + + public func sendRequest( origin: NodeOrigin, path: String, - method: HTTPMethod, + method: Alamofire.HTTPMethod, parameters: Parameters, encoding: APIParametersEncoding, timeout: TimeoutSize, @@ -85,11 +86,11 @@ public extension APICoreProtocol { downloadProgress: downloadProgress ) } - - func sendRequestJsonResponse( + + public func sendRequestJsonResponse( origin: NodeOrigin, path: String, - method: HTTPMethod, + method: Alamofire.HTTPMethod, parameters: Parameters, encoding: APIParametersEncoding ) async -> ApiServiceResult { @@ -101,8 +102,8 @@ public extension APICoreProtocol { encoding: encoding ).flatMap { parseJSON(data: $0) } } - - func sendRequestJsonResponse( + + public func sendRequestJsonResponse( origin: NodeOrigin, path: String ) async -> ApiServiceResult { @@ -114,8 +115,8 @@ public extension APICoreProtocol { encoding: .url ) } - - func sendRequest( + + public func sendRequest( origin: NodeOrigin, path: String ) async -> ApiServiceResult { @@ -127,8 +128,8 @@ public extension APICoreProtocol { encoding: .url ) } - - func sendRequest( + + public func sendRequest( origin: NodeOrigin, path: String, timeout: TimeoutSize, @@ -144,8 +145,8 @@ public extension APICoreProtocol { downloadProgress: downloadProgress ).result } - - func sendRequest( + + public func sendRequest( origin: NodeOrigin, path: String, timeout: TimeoutSize, @@ -161,11 +162,11 @@ public extension APICoreProtocol { downloadProgress: downloadProgress ) } - - func sendRequestJsonResponse( + + public func sendRequestJsonResponse( origin: NodeOrigin, path: String, - method: HTTPMethod, + method: Alamofire.HTTPMethod, jsonParameters: Any ) async -> ApiServiceResult { await sendRequestBasic( @@ -176,8 +177,8 @@ public extension APICoreProtocol { timeout: .common ).result.flatMap { parseJSON(data: $0) } } - - func sendRequestMultipartFormDataJsonResponse( + + public func sendRequestMultipartFormDataJsonResponse( origin: NodeOrigin, path: String, models: [MultipartFormDataModel], @@ -192,8 +193,8 @@ public extension APICoreProtocol { uploadProgress: uploadProgress ).result.flatMap { parseJSON(data: $0) } } - - func sendRequestRPC( + + public func sendRequestRPC( origin: NodeOrigin, path: String, requests: [RpcRequest] @@ -201,7 +202,7 @@ public extension APICoreProtocol { let parameters: [Any] = requests.compactMap { $0.asDictionary() } - + return await sendRequestJsonResponse( origin: origin, path: path, @@ -209,8 +210,8 @@ public extension APICoreProtocol { jsonParameters: parameters ) } - - func sendRequestRPC( + + public func sendRequestRPC( origin: NodeOrigin, path: String, request: RpcRequest @@ -224,8 +225,8 @@ public extension APICoreProtocol { } } -private extension APICoreProtocol { - func parseJSON(data: Data) -> ApiServiceResult { +extension APICoreProtocol { + fileprivate func parseJSON(data: Data) -> ApiServiceResult { do { let output = try JSONDecoder().decode(JSON.self, from: data) return .success(output) diff --git a/CommonKit/Sources/CommonKit/Protocols/AdamantApiServiceProtocol.swift b/CommonKit/Sources/CommonKit/Protocols/AdamantApiServiceProtocol.swift index d87919f9f..0a6bf0801 100644 --- a/CommonKit/Sources/CommonKit/Protocols/AdamantApiServiceProtocol.swift +++ b/CommonKit/Sources/CommonKit/Protocols/AdamantApiServiceProtocol.swift @@ -6,51 +6,52 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation import Alamofire +import Foundation +// sourcery: AutoMockable public protocol AdamantApiServiceProtocol: ApiServiceProtocol { // MARK: - Accounts func getAccount(byPassphrase passphrase: String) async -> ApiServiceResult func getAccount(byPublicKey publicKey: String) async -> ApiServiceResult func getAccount(byAddress address: String) async -> ApiServiceResult - + // MARK: - Keys - + func getPublicKey(byAddress address: String) async -> ApiServiceResult - + // MARK: - Transactions - - func getTransaction(id: UInt64) async -> ApiServiceResult - func getTransaction(id: UInt64, withAsset: Bool) async -> ApiServiceResult - + + func getTransaction(id: UInt64) async -> ApiServiceResult + func getTransaction(id: UInt64, withAsset: Bool) async -> ApiServiceResult + func getTransactions( forAccount: String, - type: TransactionType, + type: CommonKit.TransactionType, fromHeight: Int64?, offset: Int?, limit: Int?, waitsForConnectivity: Bool - ) async -> ApiServiceResult<[Transaction]> - + ) async -> ApiServiceResult<[CommonKit.Transaction]> + func getTransactions( forAccount account: String, - type: TransactionType, + type: CommonKit.TransactionType, fromHeight: Int64?, offset: Int?, limit: Int?, orderByTime: Bool?, waitsForConnectivity: Bool - ) async -> ApiServiceResult<[Transaction]> - + ) async -> ApiServiceResult<[CommonKit.Transaction]> + // MARK: - Chats Rooms - + func getChatRooms( address: String, offset: Int?, waitsForConnectivity: Bool ) async -> ApiServiceResult - + func getChatMessages( address: String, addressRecipient: String, @@ -59,37 +60,38 @@ public protocol AdamantApiServiceProtocol: ApiServiceProtocol { ) async -> ApiServiceResult // MARK: - Funds - + func transferFunds( sender: String, recipient: String, amount: Decimal, - keypair: Keypair + keypair: Keypair, + date: Date ) async -> ApiServiceResult func transferFunds( transaction: UnregisteredTransaction ) async -> ApiServiceResult - + // MARK: - States - + /// - Returns: Transaction ID - func store(_ model: KVSValueModel) async -> ApiServiceResult - + func store(_ model: KVSValueModel, date: Date) async -> ApiServiceResult + func get( key: String, sender: String ) async -> ApiServiceResult - + // MARK: - Chats - + func getMessageTransactions( address: String, height: Int64?, offset: Int?, waitsForConnectivity: Bool - ) async -> ApiServiceResult<[Transaction]> - + ) async -> ApiServiceResult<[CommonKit.Transaction]> + func sendTransaction( path: String, transaction: UnregisteredTransaction @@ -98,31 +100,34 @@ public protocol AdamantApiServiceProtocol: ApiServiceProtocol { func sendMessageTransaction( transaction: UnregisteredTransaction ) async -> ApiServiceResult - + // MARK: - Delegates - + /// Get delegates func getDelegates(limit: Int) async -> ApiServiceResult<[Delegate]> - + func getDelegatesWithVotes( for address: String, limit: Int ) async -> ApiServiceResult<[Delegate]> - + /// Get delegate forge details func getForgedByAccount( publicKey: String ) async -> ApiServiceResult - + /// Get delegate forgeing time func getForgingTime( for delegate: Delegate ) async -> ApiServiceResult - + /// Send vote transaction for delegates func voteForDelegates( from address: String, keypair: Keypair, - votes: [DelegateVote] + votes: [DelegateVote], + date: Date ) async -> ApiServiceResult + + func cancelCurrentTasks() } diff --git a/CommonKit/Sources/CommonKit/Protocols/AdamantCore.swift b/CommonKit/Sources/CommonKit/Protocols/AdamantCore.swift index 5e30e2a91..e98c053a2 100644 --- a/CommonKit/Sources/CommonKit/Protocols/AdamantCore.swift +++ b/CommonKit/Sources/CommonKit/Protocols/AdamantCore.swift @@ -8,26 +8,27 @@ import Foundation +// sourcery: AutoMockable public protocol AdamantCore: AnyObject, Sendable { // MARK: - Keys - func createHashFor(passphrase: String) -> String? - func createKeypairFor(passphrase: String) -> Keypair? - + func createSeedFor(passphrase: String, password: String) -> [UInt8]? + func createKeypairFor(passphrase: String, password: String) -> Keypair? + // MARK: - Signing transactions func sign(transaction: SignableTransaction, senderId: String, keypair: Keypair) -> String? - + // MARK: - Encoding messages func encodeMessage(_ message: String, recipientPublicKey: String, privateKey: String) -> (message: String, nonce: String)? func decodeMessage(rawMessage: String, rawNonce: String, senderPublicKey: String, privateKey: String) -> String? func encodeValue(_ value: [String: Any], privateKey: String) -> (message: String, nonce: String)? func decodeValue(rawMessage: String, rawNonce: String, privateKey: String) -> String? - + func encodeData( _ data: Data, recipientPublicKey publicKey: String, privateKey privateKeyHex: String ) -> (data: Data, nonce: String)? - + func decodeData( _ data: Data, rawNonce: String, @@ -43,6 +44,6 @@ public protocol SignableTransaction { var requesterPublicKey: String? { get } var timestamp: UInt64 { get } var recipientId: String? { get } - + var asset: TransactionAsset { get } } diff --git a/CommonKit/Sources/CommonKit/Protocols/ApiServiceProtocol.swift b/CommonKit/Sources/CommonKit/Protocols/ApiServiceProtocol.swift index 8b2b1338f..55670f6a4 100644 --- a/CommonKit/Sources/CommonKit/Protocols/ApiServiceProtocol.swift +++ b/CommonKit/Sources/CommonKit/Protocols/ApiServiceProtocol.swift @@ -11,24 +11,29 @@ import Foundation public protocol ApiServiceProtocol: Sendable { @MainActor var nodesInfo: NodesListInfo { get } - + @MainActor var nodesInfoPublisher: AnyObservable { get } - + func healthCheck() } -public extension ApiServiceProtocol { +extension ApiServiceProtocol { @MainActor - var hasEnabledNodePublisher: AnyObservable { + public var hasEnabledNodePublisher: AnyObservable { nodesInfoPublisher .map { $0.nodes.contains { $0.isEnabled } } .removeDuplicates() .eraseToAnyPublisher() } - + @MainActor - var hasEnabledNode: Bool { + public var hasEnabledNode: Bool { nodesInfo.nodes.contains { $0.isEnabled } } + + @MainActor + public var hasSupportedNode: Bool { + nodesInfo.nodes.contains { $0.isSupported } + } } diff --git a/CommonKit/Sources/CommonKit/Protocols/NodesAdditionalParamsStorageProtocol.swift b/CommonKit/Sources/CommonKit/Protocols/NodesAdditionalParamsStorageProtocol.swift index 1209229ff..e5a50cb9b 100644 --- a/CommonKit/Sources/CommonKit/Protocols/NodesAdditionalParamsStorageProtocol.swift +++ b/CommonKit/Sources/CommonKit/Protocols/NodesAdditionalParamsStorageProtocol.swift @@ -6,9 +6,9 @@ // Copyright © 2023 Adamant. All rights reserved. // -// MARK: - SecuredStore keys -public extension StoreKey { - enum NodesAdditionalParamsStorage { +// MARK: - SecureStore keys +extension StoreKey { + public enum NodesAdditionalParamsStorage { public static let fastestNodeMode = "nodesAdditionalParamsStorage.fastestNodeMode" } } diff --git a/CommonKit/Sources/CommonKit/Protocols/NodesStorageProtocol.swift b/CommonKit/Sources/CommonKit/Protocols/NodesStorageProtocol.swift index 67be1cc34..48309b3f3 100644 --- a/CommonKit/Sources/CommonKit/Protocols/NodesStorageProtocol.swift +++ b/CommonKit/Sources/CommonKit/Protocols/NodesStorageProtocol.swift @@ -8,16 +8,16 @@ import Foundation -// MARK: - SecuredStore keys -public extension StoreKey { - enum NodesStorage { +// MARK: - SecureStore keys +extension StoreKey { + public enum NodesStorage { public static let nodes = "nodesStorage.nodes" } } public protocol NodesStorageProtocol: Sendable { var nodesPublisher: AnyObservable<[NodeGroup: [Node]]> { get } - + func getNodesPublisher(group: NodeGroup) -> AnyObservable<[Node]> func addNode(_ node: Node, group: NodeGroup) func resetNodes(_ groups: Set) diff --git a/CommonKit/Sources/CommonKit/Protocols/SecureStorageProtocol.swift b/CommonKit/Sources/CommonKit/Protocols/SecureStorageProtocol.swift index d71eb3d9f..46302fd44 100644 --- a/CommonKit/Sources/CommonKit/Protocols/SecureStorageProtocol.swift +++ b/CommonKit/Sources/CommonKit/Protocols/SecureStorageProtocol.swift @@ -1,6 +1,6 @@ // // SecureStorageProtocol.swift -// +// // // Created by Stanislav Jelezoglo on 05.08.2024. // diff --git a/CommonKit/Sources/CommonKit/ProvidersCore/NotificationContent.swift b/CommonKit/Sources/CommonKit/ProvidersCore/NotificationContent.swift index c1259d30a..66d13a3d0 100644 --- a/CommonKit/Sources/CommonKit/ProvidersCore/NotificationContent.swift +++ b/CommonKit/Sources/CommonKit/ProvidersCore/NotificationContent.swift @@ -15,7 +15,7 @@ public struct NotificationContent { public let body: String public let attachments: [UNNotificationAttachment]? public let categoryIdentifier: String? - + public init( title: String, subtitle: String?, diff --git a/CommonKit/Sources/CommonKit/ProvidersCore/RichMessageNotificationProvider.swift b/CommonKit/Sources/CommonKit/ProvidersCore/RichMessageNotificationProvider.swift index 4e0d82ef2..2bee6dffb 100644 --- a/CommonKit/Sources/CommonKit/ProvidersCore/RichMessageNotificationProvider.swift +++ b/CommonKit/Sources/CommonKit/ProvidersCore/RichMessageNotificationProvider.swift @@ -10,7 +10,7 @@ import UIKit public protocol RichMessageNotificationProvider { static var richMessageType: String { get } - + func notificationContent( for transaction: Transaction, partnerAddress: String, diff --git a/CommonKit/Sources/CommonKit/RichMessageProviders/AdamantProvider.swift b/CommonKit/Sources/CommonKit/RichMessageProviders/AdamantProvider.swift index 66bc03a79..1873c021f 100644 --- a/CommonKit/Sources/CommonKit/RichMessageProviders/AdamantProvider.swift +++ b/CommonKit/Sources/CommonKit/RichMessageProviders/AdamantProvider.swift @@ -12,15 +12,15 @@ public final class AdamantProvider: TransferBaseProvider { public override class var richMessageType: String { return "adm_transaction" } - + public override var currencyLogoUrl: URL? { return Bundle.main.url(forResource: "adm_notificationContent", withExtension: "png") } - + public override var currencySymbol: String { return "ADM" } - + public override var currencyLogoLarge: UIImage { return .asset(named: "adamant_notification") ?? .init() } diff --git a/CommonKit/Sources/CommonKit/RichMessageProviders/BtcProvider.swift b/CommonKit/Sources/CommonKit/RichMessageProviders/BtcProvider.swift index 03f728288..dd1a39a30 100644 --- a/CommonKit/Sources/CommonKit/RichMessageProviders/BtcProvider.swift +++ b/CommonKit/Sources/CommonKit/RichMessageProviders/BtcProvider.swift @@ -12,15 +12,15 @@ public final class BtcProvider: TransferBaseProvider { public override class var richMessageType: String { return "btc_transaction" } - + public override var currencyLogoUrl: URL? { return Bundle.main.url(forResource: "btc_notificationContent", withExtension: "png") } - + public override var currencySymbol: String { return "BTC" } - + public override var currencyLogoLarge: UIImage { return .asset(named: "bitcoin_notification") ?? .init() } diff --git a/CommonKit/Sources/CommonKit/RichMessageProviders/DashProvider.swift b/CommonKit/Sources/CommonKit/RichMessageProviders/DashProvider.swift index 7007fe101..aa1f37d54 100644 --- a/CommonKit/Sources/CommonKit/RichMessageProviders/DashProvider.swift +++ b/CommonKit/Sources/CommonKit/RichMessageProviders/DashProvider.swift @@ -12,15 +12,15 @@ public final class DashProvider: TransferBaseProvider { public override class var richMessageType: String { return "dash_transaction" } - + public override var currencyLogoUrl: URL? { return Bundle.main.url(forResource: "dash_notificationContent", withExtension: "png") } - + public override var currencySymbol: String { return "DASH" } - + public override var currencyLogoLarge: UIImage { return .asset(named: "dash_notification") ?? .init() } diff --git a/CommonKit/Sources/CommonKit/RichMessageProviders/DogeProvider.swift b/CommonKit/Sources/CommonKit/RichMessageProviders/DogeProvider.swift index d2cade326..43ce5b576 100644 --- a/CommonKit/Sources/CommonKit/RichMessageProviders/DogeProvider.swift +++ b/CommonKit/Sources/CommonKit/RichMessageProviders/DogeProvider.swift @@ -12,15 +12,15 @@ public final class DogeProvider: TransferBaseProvider { public override class var richMessageType: String { return "doge_transaction" } - + public override var currencyLogoUrl: URL? { return Bundle.main.url(forResource: "doge_notificationContent", withExtension: "png") } - + public override var currencySymbol: String { return "DOGE" } - + public override var currencyLogoLarge: UIImage { return .asset(named: "doge_notification") ?? .init() } diff --git a/CommonKit/Sources/CommonKit/RichMessageProviders/ERC20Provider.swift b/CommonKit/Sources/CommonKit/RichMessageProviders/ERC20Provider.swift index fb1517fd8..0e162ee0f 100644 --- a/CommonKit/Sources/CommonKit/RichMessageProviders/ERC20Provider.swift +++ b/CommonKit/Sources/CommonKit/RichMessageProviders/ERC20Provider.swift @@ -12,19 +12,19 @@ public final class ERC20Provider: TransferBaseProvider { public override class var richMessageType: String { return "erc20_transaction" } - + public override var currencyLogoUrl: URL? { return Bundle.main.url(forResource: "\(token.symbol.lowercased())_notificationContent", withExtension: "png") } - + public override var currencySymbol: String { return token.symbol } - + public override var currencyLogoLarge: UIImage { return token.logo } - + private let token: ERC20Token public init(_ token: ERC20Token) { diff --git a/CommonKit/Sources/CommonKit/RichMessageProviders/EthProvider.swift b/CommonKit/Sources/CommonKit/RichMessageProviders/EthProvider.swift index 78dbfc6f9..b8c1da6a5 100644 --- a/CommonKit/Sources/CommonKit/RichMessageProviders/EthProvider.swift +++ b/CommonKit/Sources/CommonKit/RichMessageProviders/EthProvider.swift @@ -12,15 +12,15 @@ public final class EthProvider: TransferBaseProvider { public override class var richMessageType: String { return "eth_transaction" } - + public override var currencyLogoUrl: URL? { return Bundle.main.url(forResource: "eth_notificationContent", withExtension: "png") } - + public override var currencySymbol: String { return "ETH" } - + public override var currencyLogoLarge: UIImage { return .asset(named: "ethereum_notification") ?? .init() } diff --git a/CommonKit/Sources/CommonKit/RichMessageProviders/LskProvider.swift b/CommonKit/Sources/CommonKit/RichMessageProviders/LskProvider.swift index 8b7843fa9..5a0c5ada6 100644 --- a/CommonKit/Sources/CommonKit/RichMessageProviders/LskProvider.swift +++ b/CommonKit/Sources/CommonKit/RichMessageProviders/LskProvider.swift @@ -12,15 +12,15 @@ public final class KlyProvider: TransferBaseProvider { public override class var richMessageType: String { return "kly_transaction" } - + public override var currencyLogoUrl: URL? { return Bundle.main.url(forResource: "klayr_notificationContent", withExtension: "png") } - + public override var currencySymbol: String { return "KLY" } - + public override var currencyLogoLarge: UIImage { return .asset(named: "klayr_notification") ?? .init() } diff --git a/CommonKit/Sources/CommonKit/RichMessageProviders/TransferBaseProvider.swift b/CommonKit/Sources/CommonKit/RichMessageProviders/TransferBaseProvider.swift index 4156ed70b..e06c75d47 100644 --- a/CommonKit/Sources/CommonKit/RichMessageProviders/TransferBaseProvider.swift +++ b/CommonKit/Sources/CommonKit/RichMessageProviders/TransferBaseProvider.swift @@ -6,12 +6,12 @@ // Copyright © 2019 Adamant. All rights reserved. // +import MarkdownKit import UIKit import UserNotifications -import MarkdownKit public class TransferBaseProvider: TransferNotificationContentProvider { - + /// Create notification content for Rich messages public func notificationContent( for transaction: Transaction, @@ -20,87 +20,91 @@ public class TransferBaseProvider: TransferNotificationContentProvider { richContent: [String: Any] ) -> NotificationContent? { guard let amountRaw = richContent[RichContentKeys.transfer.amount] as? String, - let amount = Decimal(string: amountRaw) else { + let amount = Decimal(string: amountRaw) + else { return nil } - + let comment: String? if let raw = richContent[RichContentKeys.transfer.comments] as? String, - raw.count > 0 { + raw.count > 0 + { comment = raw } else { comment = nil } - + return notificationContent(partnerAddress: partnerAddress, partnerName: partnerName, amount: amount, comment: comment) } - + /// Create notification content for rich transfers with comments, such as ADM transfer public func notificationContent(partnerAddress: String, partnerName: String?, amount: Decimal, comment: String?) -> NotificationContent? { let amountFormated = AdamantBalanceFormat.full.format(amount, withCurrencySymbol: currencySymbol) var body = String.adamant.notifications.yourTransferBody(with: amountFormated) - + if let comment = comment { let stripped = MarkdownParser().parse(comment).string body = "\(body)\n\(stripped)" } - + let identifier = type(of: self).richMessageType let attachments: [UNNotificationAttachment]? if let url = currencyLogoUrl, - let attachment = try? UNNotificationAttachment(identifier: identifier, url: url) { + let attachment = try? UNNotificationAttachment(identifier: identifier, url: url) + { attachments = [attachment] } else { attachments = nil } - + return NotificationContent( title: partnerName ?? partnerAddress, subtitle: .adamant.notifications.newTransfer, body: body, attachments: attachments, - categoryIdentifier: AdamantNotificationCategories.transfer) + categoryIdentifier: AdamantNotificationCategories.transfer + ) } - + // MARK: - To override - + public class var richMessageType: String { assertionFailure("Provide richMessageType") return .empty } - + public var currencyLogoLarge: UIImage { assertionFailure("Provide currency logo") return .init() } - + public var currencyLogoUrl: URL? { assertionFailure("Provide currencyLogoUrl") return nil } - + public var currencySymbol: String { assertionFailure("Provide currencySymbol") return .empty } - + public init() {} - + // MARK: - Private - + private func saveLocally(image: UIImage, name: String) -> URL? { let fileManager = FileManager.default let cacheDirectory = fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0] let url = cacheDirectory.appendingPathComponent("\(name).png") - + if fileManager.fileExists(atPath: url.path) { return url } - + guard let data = image.pngData() else { return nil } - + fileManager.createFile(atPath: url.path, contents: data, attributes: nil) return url } diff --git a/CommonKit/Sources/CommonKit/Services/APICore.swift b/CommonKit/Sources/CommonKit/Services/APICore.swift index b8bf47cd0..2f5ec41b3 100644 --- a/CommonKit/Sources/CommonKit/Services/APICore.swift +++ b/CommonKit/Sources/CommonKit/Services/APICore.swift @@ -6,17 +6,17 @@ // Copyright © 2023 Adamant. All rights reserved. // -import Foundation @preconcurrency import Alamofire +import Foundation public actor APICore: APICoreProtocol { private let responseQueue = DispatchQueue( label: "com.adamant.response-queue", qos: .userInteractive ) - + private var sessions: [TimeoutSize: Session] = .init() - + public func sendRequestMultipartFormData( origin: NodeOrigin, path: String, @@ -37,7 +37,7 @@ public actor APICore: APICoreProtocol { }, to: try buildUrl(origin: origin, path: path) ).uploadProgress(queue: .global(), closure: uploadProgress) - + return await sendRequest(request: request) } catch { return .init( @@ -47,7 +47,7 @@ public actor APICore: APICoreProtocol { ) } } - + public func sendRequestBasic( origin: NodeOrigin, path: String, @@ -65,7 +65,7 @@ public actor APICore: APICoreProtocol { encoding: encoding.parametersEncoding, headers: HTTPHeaders(["Content-Type": "application/json"]) ).downloadProgress(closure: downloadProgress) - + return await sendRequest(request: request) } catch { return .init( @@ -75,7 +75,7 @@ public actor APICore: APICoreProtocol { ) } } - + public func sendRequestBasic( origin: NodeOrigin, path: String, @@ -87,12 +87,12 @@ public actor APICore: APICoreProtocol { let data = try JSONSerialization.data( withJSONObject: jsonParameters ) - + var request = try URLRequest( url: try buildUrl(origin: origin, path: path), method: method ) - + request.httpBody = data request.headers.update(.contentType("application/json")) return await sendRequest(request: getSession(timeout).request(request)) @@ -104,52 +104,55 @@ public actor APICore: APICoreProtocol { ) } } - + public init() {} } -private extension APICore { - func sendRequest(request: DataRequest) async -> APIResponseModel { +extension APICore { + fileprivate func sendRequest(request: DataRequest) async -> APIResponseModel { await withTaskCancellationHandler( operation: { await withCheckedContinuation { continuation in request.responseData(queue: responseQueue) { response in - continuation.resume(returning: .init( - result: response.result.mapError { .init(error: $0) }, - data: response.data, - code: response.response?.statusCode - )) + continuation.resume( + returning: .init( + result: response.result.mapError { .init(error: $0) }, + data: response.data, + code: response.response?.statusCode + ) + ) } } }, onCancel: { request.cancel() } ) } - - func buildUrl(origin: NodeOrigin, path: String) throws -> URL { + + fileprivate func buildUrl(origin: NodeOrigin, path: String) throws -> URL { guard let url = origin.asURL()?.appendingPathComponent(path, conformingTo: .url) else { throw InternalAPIError.endpointBuildFailed } return url } - - func getSession(_ timeout: TimeoutSize) -> Session { + + fileprivate func getSession(_ timeout: TimeoutSize) -> Session { if let session = sessions[timeout] { return session } - + let configuration = AF.sessionConfiguration configuration.waitsForConnectivity = true configuration.timeoutIntervalForRequest = requestTimeout configuration.requestCachePolicy = .reloadIgnoringLocalCacheData configuration.httpMaximumConnectionsPerHost = maximumConnectionsPerHost - - configuration.timeoutIntervalForResource = switch timeout { - case .common: - resourceTimeout - case .extended: - extendedResourceTimeout - } - + + configuration.timeoutIntervalForResource = + switch timeout { + case .common: + resourceTimeout + case .extended: + extendedResourceTimeout + } + let session = Alamofire.Session.init(configuration: configuration) sessions[timeout] = session return session diff --git a/CommonKit/Sources/CommonKit/Services/AdamantAvatarService.swift b/CommonKit/Sources/CommonKit/Services/AdamantAvatarService.swift index db5bcb4e9..22befcbed 100644 --- a/CommonKit/Sources/CommonKit/Services/AdamantAvatarService.swift +++ b/CommonKit/Sources/CommonKit/Services/AdamantAvatarService.swift @@ -6,57 +6,57 @@ // Copyright © 2018 Adamant. All rights reserved. // -import UIKit import CoreGraphics import CryptoSwift +import UIKit public final class AdamantAvatarService: @unchecked Sendable { private let colors: [[UIColor]] = [ [ - UIColor(hex: "#ffffff"), //background - UIColor(hex: "#179cec"), // main - UIColor(hex: "#8bcef6"), // 2dary - UIColor(hex: "#c5e6fa") // 2dary + UIColor(hex: "#ffffff"), //background + UIColor(hex: "#179cec"), // main + UIColor(hex: "#8bcef6"), // 2dary + UIColor(hex: "#c5e6fa") // 2dary ], [ - UIColor(hex: "#ffffff"), //background - UIColor(hex: "#32d296"), // main - UIColor(hex: "#99e9cb"), // 2dary - UIColor(hex: "#ccf4e5") // 2dary + UIColor(hex: "#ffffff"), //background + UIColor(hex: "#32d296"), // main + UIColor(hex: "#99e9cb"), // 2dary + UIColor(hex: "#ccf4e5") // 2dary ], [ - UIColor(hex: "#ffffff"), //background - UIColor(hex: "#faa05a"), // main - UIColor(hex: "#fdd0ad"), // 2dary - UIColor(hex: "#fee7d6") // 2dary + UIColor(hex: "#ffffff"), //background + UIColor(hex: "#faa05a"), // main + UIColor(hex: "#fdd0ad"), // 2dary + UIColor(hex: "#fee7d6") // 2dary ], [ - UIColor(hex: "#ffffff"), //background - UIColor(hex: "#474a5f"), // main - UIColor(hex: "#a3a5af"), // 2dary - UIColor(hex: "#d1d2d7") // 2dary + UIColor(hex: "#ffffff"), //background + UIColor(hex: "#474a5f"), // main + UIColor(hex: "#a3a5af"), // 2dary + UIColor(hex: "#d1d2d7") // 2dary ], [ - UIColor(hex: "#ffffff"), //background - UIColor(hex: "#9497a3"), // main - UIColor(hex: "#cacbd1"), // 2dary - UIColor(hex: "#e4e5e8") // 2dary + UIColor(hex: "#ffffff"), //background + UIColor(hex: "#9497a3"), // main + UIColor(hex: "#cacbd1"), // 2dary + UIColor(hex: "#e4e5e8") // 2dary ] ] - + @Atomic private var cache: [String: UIImage] = [String: UIImage]() - - public func avatar(for key:String, size: Double = 200) -> UIImage { + + public func avatar(for key: String, size: Double = 200) -> UIImage { if let image = cache[key] { return image } - + UIGraphicsBeginImageContextWithOptions(CGSize(squareSize: size), false, 0) Hexa16(key: key, size: size) - + let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() - + if let image = image { cache[key] = image return image @@ -64,33 +64,37 @@ public final class AdamantAvatarService: @unchecked Sendable { return UIImage() } } - + public init() {} } -private extension AdamantAvatarService { - func Hexa16(key: String, size: Double) { +extension AdamantAvatarService { + fileprivate func Hexa16(key: String, size: Double) { let fringeSize = size / 6 let distance = distanceTo3rdPoint(fringeSize) let lines = size / fringeSize let offset = ((fringeSize - distance) * lines) / 2 - + let fillTriangle = triangleColors(0, key, Int(lines)) let transparent = UIColor.clear - + let isLeft: (Int) -> Bool = { (v: Int) -> Bool in return (v % 2) == 0 } let isRight: (Int) -> Bool = { (v: Int) -> Bool in return (v % 2) != 0 } - + let L = Int(lines) let hL = L / 2 - - for xL in 0 ..< hL { - for yL in 0 ..< L { + + for xL in 0.. Double { + + fileprivate func distanceTo3rdPoint(_ AC: Double) -> Double { // distance from center of vector to third point of equilateral triangles // ABC triangle, O is the center of AB vector // OC = SQRT(AC^2 - AO^2) - return ceil(sqrt((AC * AC) - (AC/2 * AC/2))) + return ceil(sqrt((AC * AC) - (AC / 2 * AC / 2))) } - + // right1stTriangle computes a right oriented triangle '>' - func right1stTriangle(_ xL: Double, _ yL: Double, _ fringeSize: Double, _ distance: Double) -> (x1: Double, y1: Double, x2: Double, y2: Double, x3: Double, y3: Double) { + fileprivate func right1stTriangle( + _ xL: Double, + _ yL: Double, + _ fringeSize: Double, + _ distance: Double + ) -> (x1: Double, y1: Double, x2: Double, y2: Double, x3: Double, y3: Double) { let x1 = xL * distance let x2 = xL * distance + distance let x3 = x1 @@ -209,9 +222,14 @@ private extension AdamantAvatarService { let y3 = yL * fringeSize + fringeSize return (x1, y1, x2, y2, x3, y3) } - + // left1stTriangle computes the coordinates of a left oriented triangle '<' - func left1stTriangle(_ xL: Double, _ yL: Double, _ fringeSize: Double, _ distance: Double) -> (x1: Double, y1: Double, x2: Double, y2: Double, x3: Double, y3: Double) { + fileprivate func left1stTriangle( + _ xL: Double, + _ yL: Double, + _ fringeSize: Double, + _ distance: Double + ) -> (x1: Double, y1: Double, x2: Double, y2: Double, x3: Double, y3: Double) { let x1 = xL * distance + distance let x2 = xL * distance let x3 = x1 @@ -220,9 +238,14 @@ private extension AdamantAvatarService { let y3 = yL * fringeSize + fringeSize return (x1, y1, x2, y2, x3, y3) } - + // left2ndTriangle computes the coordinates of a left oriented triangle '<' - func left2ndTriangle(_ xL: Double, _ yL: Double, _ fringeSize: Double, _ distance: Double) -> (x1: Double, y1: Double, x2: Double, y2: Double, x3: Double, y3: Double) { + fileprivate func left2ndTriangle( + _ xL: Double, + _ yL: Double, + _ fringeSize: Double, + _ distance: Double + ) -> (x1: Double, y1: Double, x2: Double, y2: Double, x3: Double, y3: Double) { let x1 = xL * distance + distance let x2 = xL * distance let x3 = x1 @@ -231,9 +254,14 @@ private extension AdamantAvatarService { let y3 = yL * fringeSize + fringeSize + fringeSize / 2 return (x1, y1, x2, y2, x3, y3) } - + // right2ndTriangle computes the coordinates of a right oriented triangle '>' - func right2ndTriangle(_ xL: Double, _ yL: Double, _ fringeSize: Double, _ distance: Double) -> (x1: Double, y1: Double, x2: Double, y2: Double, x3: Double, y3: Double) { + fileprivate func right2ndTriangle( + _ xL: Double, + _ yL: Double, + _ fringeSize: Double, + _ distance: Double + ) -> (x1: Double, y1: Double, x2: Double, y2: Double, x3: Double, y3: Double) { let x1 = xL * distance let x2 = xL * distance + distance let x3 = x1 @@ -242,137 +270,137 @@ private extension AdamantAvatarService { let y3 = yL * fringeSize + fringeSize / 2 + fringeSize return (x1, y1, x2, y2, x3, y3) } - - func mirrorCoordinates(_ xs: [Double], _ lines: Double, _ fringeSize: Double, _ offset: Double) -> [Double] { + + fileprivate func mirrorCoordinates(_ xs: [Double], _ lines: Double, _ fringeSize: Double, _ offset: Double) -> [Double] { var xsMirror = [Double]() for (_, v) in xs.enumerated() { xsMirror.append((lines * fringeSize) - v + offset) } return xsMirror } - - func triangleColors(_ id: Int, _ key: String, _ lines: Int) -> [UIColor] { + + fileprivate func triangleColors(_ id: Int, _ key: String, _ lines: Int) -> [UIColor] { let keyHash = key.md5() guard keyHash.count == 32 else { fatalError("AdamantAvatarService: Wrong md5 hash") } - + var tColors = [UIColor]() - + var rawKeyArray = [Int]() for u in keyHash.unicodeScalars { rawKeyArray.append(Int(u.value)) } - let seed = scramble(rawKeyArray.reduce(0, +)) // sum of all values - + let seed = scramble(rawKeyArray.reduce(0, +)) // sum of all values + // process hash values to number array with 10 values. 1 - avatar color set (merge first 5), 2-10 - values for triange colors (merged by 3 values) var keyArray = [Int]() - keyArray.append(rawKeyArray[0..<5].reduce(0, +)) // merge first 5 + keyArray.append(rawKeyArray[0..<5].reduce(0, +)) // merge first 5 for i in stride(from: 5, to: 32, by: 3) { - keyArray.append(rawKeyArray[i..<(i+3)].reduce(0, +)) // merge rest by 3 + keyArray.append(rawKeyArray[i..<(i + 3)].reduce(0, +)) // merge rest by 3 } - + let setId = seed % getValue(from: keyHash, by: keyArray[0]) let colorsSet = self.colors[setId % self.colors.count] - - for (i,t) in Triangle.triangles[id].enumerated() { + + for (i, t) in Triangle.triangles[id].enumerated() { let x = t.x let y = t.y - let index = x + 3 * y + lines + seed % getValue(from: keyHash, by: keyArray[i+1]) + let index = x + 3 * y + lines + seed % getValue(from: keyHash, by: keyArray[i + 1]) let color = PickColor(keyHash, colorsSet, index: index) tColors.append(color) } return tColors } - - func scramble( _ seed : Int ) -> Int { - let multiplier : Int64 = 0x5DEEC - let mask : Int64 = (1 << 30) - 1 - + + fileprivate func scramble(_ seed: Int) -> Int { + let multiplier: Int64 = 0x5DEEC + let mask: Int64 = (1 << 30) - 1 + return Int((Int64(seed) ^ multiplier) & mask) } - - func getValue(from string: String, by index: Int) -> Int { + + fileprivate func getValue(from string: String, by index: Int) -> Int { let index = string.index(string.startIndex, offsetBy: index % string.count) let s = String(string[index]) return Int([UInt8](s.utf8).first ?? 0) } - - func isOutsideHexagon(_ xL: Int, _ yL: Int, _ lines: Int) -> Bool { + + fileprivate func isOutsideHexagon(_ xL: Int, _ yL: Int, _ lines: Int) -> Bool { return !isFill1InHexagon(xL, yL, lines) && !isFill2InHexagon(xL, yL, lines) } - - func isFill1InHexagon(_ xL: Int, _ yL: Int, _ lines: Int) -> Bool { + + fileprivate func isFill1InHexagon(_ xL: Int, _ yL: Int, _ lines: Int) -> Bool { let half = lines / 2 let start = half / 2 - if xL < start+1 { - if yL > start-1 && yL < start+half+1 { + if xL < start + 1 { + if yL > start - 1 && yL < start + half + 1 { return true } } - if xL == half-1 { - if yL > start-1-1 && yL < start+half+1+1 { + if xL == half - 1 { + if yL > start - 1 - 1 && yL < start + half + 1 + 1 { return true } } return false } - - func isFill2InHexagon(_ xL: Int, _ yL: Int, _ lines: Int) -> Bool { + + fileprivate func isFill2InHexagon(_ xL: Int, _ yL: Int, _ lines: Int) -> Bool { let half = lines / 2 let start = half / 2 - + if xL < start { - if yL > start-1 && yL < start+half { + if yL > start - 1 && yL < start + half { return true } } if xL == 1 { - if yL > start-1-1 && yL < start+half+1 { + if yL > start - 1 - 1 && yL < start + half + 1 { return true } } - if xL == half-1 { - if yL > start-1-1 && yL < start+half+1 { + if xL == half - 1 { + if yL > start - 1 - 1 && yL < start + half + 1 { return true } } return false } - + // PickColor returns a color given a key string, an array of colors and an index. // key: should be a md5 hash string. // index: is an index from the key string. // Algorithm: PickColor converts the key[index] value to a decimal value. // We pick the ith colors that respects the equality value%numberOfColors == i. - func PickColor(_ key: String, _ colors: [UIColor], index: Int) -> UIColor { + fileprivate func PickColor(_ key: String, _ colors: [UIColor], index: Int) -> UIColor { let n = colors.count let i = PickIndex(key, n, index) return colors[i] } - + // PickIndex returns an index of given a key string, the size of an array of colors // and an index. // key: should be a md5 hash string. // index: is an index from the key string. // Algorithm: PickIndex converts the key[index] value to a decimal value. // We pick the ith index that respects the equality value%sizeOfArray == i. - func PickIndex(_ key: String, _ n: Int, _ index: Int) -> Int { + fileprivate func PickIndex(_ key: String, _ n: Int, _ index: Int) -> Int { let r = getValue(from: key, by: index) - for i in 0 ..< n { - if r%n == i { + for i in 0.. Bool, _ isRight: (Int) -> Bool) -> UIColor? { + fileprivate func canFill(_ x: Int, _ y: Int, _ fills: [UIColor], _ isLeft: (Int) -> Bool, _ isRight: (Int) -> Bool) -> UIColor? { let l = Triangle(x, y, .left) let r = Triangle(x, y, .right) - + if isLeft(x) && l.isInTriangle() { let rid = l.rotationID() return fills[rid] @@ -388,7 +416,7 @@ private struct Triangle { let x: Int let y: Int let direction: Direction - + // triangles in an array of array triangle positions. // each array correspond to a triangle, there are 6 of them, // indexes from 0 to 5, they form an hexagon. @@ -462,17 +490,17 @@ private struct Triangle { Triangle(2, 5, .left) ] ] - + init(_ x: Int, _ y: Int, _ direction: Direction) { self.x = x self.y = y self.direction = direction } - + func isInTriangle() -> Bool { return self.triangleID() != -1 } - + // triangleID returns the triangle id (from 0 to 5) // that has a match with the position given as param. // returns -1 if a match is not found. @@ -486,7 +514,7 @@ private struct Triangle { } return -1 } - + // subTriangleID returns the sub triangle id (from 0 to 8) // that has a match with the position given as param. // returns -1 if a match is not found. @@ -500,9 +528,9 @@ private struct Triangle { } return -1 } - + func subTriangleRotations(_ lookforSubTriangleID: Int) -> [Int]? { - let m: [Int:[Int]] = [ + let m: [Int: [Int]] = [ 0: [0, 6, 8, 8, 2, 0], 1: [1, 2, 5, 7, 6, 3], 2: [2, 0, 0, 6, 8, 8], @@ -512,17 +540,17 @@ private struct Triangle { 6: [6, 3, 1, 2, 5, 7], 7: [7, 5, 4, 1, 3, 4], 8: [8, 8, 2, 0, 0, 6] - ] + ] return m[lookforSubTriangleID] } - + // rotationId returns the original sub triangle id // if the current triangle was rotated to position 0. func rotationID() -> Int { let currentTID = self.triangleID() let currentSTID = self.subTriangleID() let numberOfSubTriangles = 9 - for i in 0 ..< numberOfSubTriangles { + for i in 0.. ApiServiceResult { - guard let keypair = adamantCore.createKeypairFor(passphrase: passphrase) else { + guard let keypair = adamantCore.createKeypairFor(passphrase: passphrase, password: .empty) else { return .failure(.accountNotFound) } - + return await getAccount(byPublicKey: keypair.publicKey) } - + /// Get account by publicKey public func getAccount(byPublicKey publicKey: String) async -> ApiServiceResult { switch await request({ apiCore, origin in @@ -37,7 +37,7 @@ extension AdamantApiService { parameters: ["publicKey": publicKey], encoding: .url ) - + return response.flatMap { $0.resolved() } }) { case let .success(value): @@ -54,16 +54,17 @@ extension AdamantApiService { public func getAccount(byAddress address: String) async -> ApiServiceResult { await request { apiCore, origin in - let response: ApiServiceResult< - ServerModelResponse - > = await apiCore.sendRequestJsonResponse( - origin: origin, - path: ApiCommands.Accounts.root, - method: .get, - parameters: ["address": address], - encoding: .url - ) - + let response: + ApiServiceResult< + ServerModelResponse + > = await apiCore.sendRequestJsonResponse( + origin: origin, + path: ApiCommands.Accounts.root, + method: .get, + parameters: ["address": address], + encoding: .url + ) + return response.flatMap { $0.resolved() } } } diff --git a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+AdamantApiTask.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+AdamantApiTask.swift new file mode 100644 index 000000000..24702e0f2 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+AdamantApiTask.swift @@ -0,0 +1,38 @@ +// +// AdamantApi+AdamantApiTask.swift +// CommonKit +// +// Created by Sergei Veretennikov on 04.03.2025. +// + +import Foundation + +final class AdamantApiTask: CancellableTask { + private let task: Task, Never> + private let id = UUID() + var value: Result { + get async { + await task.value + } + } + + init(task: Task, Never>) { + self.task = task + } + + func cancel() { + task.cancel() + } + + func storeIn(taskStorage: inout [UUID: CancellableTask]) { + taskStorage[id] = self + } + + func removeFrom(taskStorage: inout [UUID: CancellableTask]) { + taskStorage[id] = nil + } +} + +protocol CancellableTask { + func cancel() +} diff --git a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Chats.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Chats.swift index cffb1982b..8751081e4 100644 --- a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Chats.swift +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Chats.swift @@ -9,8 +9,8 @@ import Foundation import UIKit -public extension ApiCommands { - static let Chats = ( +extension ApiCommands { + public static let Chats = ( root: "/api/chats", get: "/api/chats/get", normalizeTransaction: "/api/chats/normalize", @@ -30,15 +30,15 @@ extension AdamantApiService { "isIn": address, "orderBy": "timestamp:desc" ] - + if let height = height, height > .zero { parameters["fromHeight"] = String(height) } - + if let offset = offset { parameters["offset"] = String(offset) } - + let response: ApiServiceResult> response = await request(waitsForConnectivity: waitsForConnectivity) { [parameters] service, origin in @@ -50,10 +50,10 @@ extension AdamantApiService { encoding: .url ) } - + return response.flatMap { $0.resolved() } } - + public func sendMessageTransaction( transaction: UnregisteredTransaction ) async -> ApiServiceResult { @@ -62,18 +62,18 @@ extension AdamantApiService { transaction: transaction ) } - + public func getChatRooms( address: String, offset: Int?, waitsForConnectivity: Bool ) async -> ApiServiceResult { var parameters = ["limit": "20"] - + if let offset = offset { parameters["offset"] = String(offset) } - + return await request(waitsForConnectivity: waitsForConnectivity) { [parameters] service, origin in await service.sendRequestJsonResponse( @@ -85,7 +85,7 @@ extension AdamantApiService { ) } } - + public func getChatMessages( address: String, addressRecipient: String, @@ -93,15 +93,15 @@ extension AdamantApiService { limit: Int? ) async -> ApiServiceResult { var parameters: [String: String] = [:] - + if let offset = offset { parameters["offset"] = String(offset) } - + if let limit = limit { parameters["limit"] = String(limit) } - + return await request { [parameters] service, origin in await service.sendRequestJsonResponse( origin: origin, diff --git a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Delegates.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Delegates.swift index a466aef71..dbbe12562 100644 --- a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Delegates.swift +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Delegates.swift @@ -9,8 +9,8 @@ import Foundation import UIKit -public extension ApiCommands { - static let Delegates = ( +extension ApiCommands { + public static let Delegates = ( root: "/api/delegates", getDelegates: "/api/delegates", votes: "/api/accounts/delegates", @@ -25,7 +25,7 @@ extension AdamantApiService { public func getDelegates(limit: Int) async -> ApiServiceResult<[Delegate]> { await getDelegates(limit: limit, offset: .zero, currentDelegates: [Delegate]()) } - + public func getDelegates( limit: Int, offset: Int, @@ -41,11 +41,11 @@ extension AdamantApiService { encoding: .url ) } - + let result = response.flatMap { $0.resolved() } guard let delegates = try? result.get() else { return result } let currentDelegates = currentDelegates + delegates - + if delegates.count < limit { return .success(currentDelegates) } else { @@ -56,30 +56,30 @@ extension AdamantApiService { ) } } - + public func getDelegatesWithVotes(for address: String, limit: Int) async -> ApiServiceResult<[Delegate]> { let response = await getVotes(for: address) - + switch response { case let .success(delegates): let votes = delegates.map { $0.address } let delegatesResponse = await getDelegates(limit: limit) - + return delegatesResponse.map { delegates in var delegatesWithVotes = [Delegate]() - + delegates.forEach { delegate in delegate.voted = votes.contains(delegate.address) delegatesWithVotes.append(delegate) } - + return delegatesWithVotes } case let .failure(error): return .failure(error) } } - + public func getForgedByAccount(publicKey: String) async -> ApiServiceResult { await request { core, origin in await core.sendRequestJsonResponse( @@ -91,7 +91,7 @@ extension AdamantApiService { ) } } - + public func getForgingTime(for delegate: Delegate) async -> ApiServiceResult { await getNextForgers().map { nextForgers in var forgingTime = -1 @@ -101,7 +101,7 @@ extension AdamantApiService { return forgingTime } } - + private func getDelegatesCount() async -> ApiServiceResult { await request { core, origin in await core.sendRequestJsonResponse( @@ -110,7 +110,7 @@ extension AdamantApiService { ) } } - + private func getNextForgers() async -> ApiServiceResult { await request { core, origin in await core.sendRequestJsonResponse( @@ -122,7 +122,7 @@ extension AdamantApiService { ) } } - + public func getVotes(for address: String) async -> ApiServiceResult<[Delegate]> { let response: ApiServiceResult> response = await request { core, origin in @@ -134,14 +134,15 @@ extension AdamantApiService { encoding: .url ) } - + return response.map { $0.collection ?? .init() } } - + public func voteForDelegates( from address: String, keypair: Keypair, - votes: [DelegateVote] + votes: [DelegateVote], + date: Date ) async -> ApiServiceResult { // MARK: 0. Prepare var votesOrdered = votes @@ -151,36 +152,38 @@ extension AdamantApiService { case .downvote: return true } } - + let votesAsset = VotesAsset(votes: votesOrdered) - + // MARK: 1. Create and sign transaction let transaction = NormalizedTransaction( type: .vote, amount: .zero, senderPublicKey: keypair.publicKey, requesterPublicKey: nil, - date: .now, + date: date, recipientId: address, asset: TransactionAsset(votes: votesAsset) ) - - guard let transaction = adamantCore.makeSignedTransaction( - transaction: transaction, - senderId: address, - keypair: keypair - ) else { + + guard + let transaction = adamantCore.makeSignedTransaction( + transaction: transaction, + senderId: address, + keypair: keypair + ) + else { return .failure(.internalError(error: InternalAPIError.signTransactionFailed)) } - + return await sendDelegateVoteTransaction( path: ApiCommands.Delegates.votes, transaction: transaction ) } - + // MARK: - Private methods - + private func getBlocks() async -> ApiServiceResult<[Block]> { let response: ApiServiceResult> = await request { core, origin in await core.sendRequestJsonResponse( @@ -191,10 +194,10 @@ extension AdamantApiService { encoding: .url ) } - + return response.flatMap { $0.resolved() } } - + private func getRoundDelegates(delegates: [String], height: UInt64) -> [String] { let currentRound = round(height) return delegates.filter({ (delegate) -> Bool in @@ -204,7 +207,7 @@ extension AdamantApiService { return false }) } - + private func round(_ height: UInt64?) -> UInt { if let height = height { return UInt(floor(Double(height) / 101) + (Double(height).truncatingRemainder(dividingBy: 101) > 0 ? 1 : 0)) diff --git a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Keys.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Keys.swift index 2e6f2ca6f..0f113d83b 100644 --- a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Keys.swift +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Keys.swift @@ -19,7 +19,7 @@ extension AdamantApiService { encoding: .url ) } - + return response.flatMap { $0.resolved() } } } diff --git a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+States.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+States.swift index d68340d7d..da22b2b1b 100644 --- a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+States.swift +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+States.swift @@ -9,8 +9,8 @@ import Foundation import UIKit -public extension ApiCommands { - static let States = ( +extension ApiCommands { + public static let States = ( root: "/api/states", get: "/api/states/get", store: "/api/states/store" @@ -19,40 +19,44 @@ public extension ApiCommands { extension AdamantApiService { public static let KvsFee: Decimal = 0.001 - - public func store(_ model: KVSValueModel) async -> ApiServiceResult { + + public func store(_ model: KVSValueModel, date: Date) async -> ApiServiceResult { let transaction = NormalizedTransaction( type: .state, amount: .zero, senderPublicKey: model.keypair.publicKey, requesterPublicKey: nil, - date: .now, + date: date, recipientId: nil, - asset: TransactionAsset(state: StateAsset( - key: model.key, - value: model.value, - type: .keyValue - )) + asset: TransactionAsset( + state: StateAsset( + key: model.key, + value: model.value, + type: .keyValue + ) + ) ) - - guard let transaction = adamantCore.makeSignedTransaction( - transaction: transaction, - senderId: AdamantUtilities.generateAddress( - publicKey: model.keypair.publicKey - ), - keypair: model.keypair - ) else { + + guard + let transaction = adamantCore.makeSignedTransaction( + transaction: transaction, + senderId: AdamantUtilities.generateAddress( + publicKey: model.keypair.publicKey + ), + keypair: model.keypair + ) + else { return .failure(.internalError(error: InternalAPIError.signTransactionFailed)) } - + // MARK: Send - + return await sendTransaction( path: ApiCommands.States.store, transaction: transaction ) } - + public func get(key: String, sender: String) async -> ApiServiceResult { // MARK: 1. Prepare let parameters = [ @@ -60,7 +64,7 @@ extension AdamantApiService { "orderBy": "timestamp:desc", "key": key ] - + let response: ApiServiceResult> response = await request { [parameters] core, origin in await core.sendRequestJsonResponse( @@ -71,8 +75,9 @@ extension AdamantApiService { encoding: .url ) } - - return response + + return + response .flatMap { $0.resolved() } .map { $0.first?.asset.state?.value } } diff --git a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Transactions.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Transactions.swift index bf6052ac8..004e3ccbf 100644 --- a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Transactions.swift +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Transactions.swift @@ -8,8 +8,8 @@ import Foundation -public extension ApiCommands { - static let Transactions = ( +extension ApiCommands { + public static let Transactions = ( root: "/api/transactions", getTransaction: "/api/transactions/get", normalizeTransaction: "/api/transactions/normalize", @@ -31,10 +31,10 @@ extension AdamantApiService { encoding: .json ) } - + return response.flatMap { $0.resolved() } } - + public func sendDelegateVoteTransaction( path: String, transaction: UnregisteredTransaction @@ -48,17 +48,17 @@ extension AdamantApiService { encoding: .json ) } - + return response.flatMap { guard let error = $0.error else { return .success($0.success) } return .failure(.serverError(error: error)) } } - + public func getTransaction(id: UInt64) async -> ApiServiceResult { await getTransaction(id: id, withAsset: false) } - + public func getTransaction(id: UInt64, withAsset: Bool) async -> ApiServiceResult { let response: ApiServiceResult> response = await request { core, origin in @@ -73,10 +73,10 @@ extension AdamantApiService { encoding: .url ) } - + return response.flatMap { $0.resolved() } } - + public func getTransactions( forAccount account: String, type: TransactionType, @@ -95,7 +95,7 @@ extension AdamantApiService { waitsForConnectivity: waitsForConnectivity ) } - + public func getTransactions( forAccount account: String, type: TransactionType, @@ -106,30 +106,30 @@ extension AdamantApiService { waitsForConnectivity: Bool ) async -> ApiServiceResult<[Transaction]> { var queryItems = [URLQueryItem(name: "inId", value: account)] - + if type == .send { // transfers can be of type 0 and 8 so we can filter by min amount queryItems.append(URLQueryItem(name: "and:minAmount", value: "1")) } else { queryItems.append(URLQueryItem(name: "and:type", value: String(type.rawValue))) } - + if let limit = limit { queryItems.append(URLQueryItem(name: "limit", value: String(limit))) } - + if let offset = offset { queryItems.append(URLQueryItem(name: "offset", value: String(offset))) } - + if let fromHeight = fromHeight, fromHeight > 0 { queryItems.append(URLQueryItem(name: "and:fromHeight", value: String(fromHeight))) } - + if let orderByTime = orderByTime, orderByTime { queryItems.append(URLQueryItem(name: "orderBy", value: "timestamp:desc")) } - + let response: ApiServiceResult> response = await request(waitsForConnectivity: waitsForConnectivity) { [queryItems] core, origin in @@ -141,7 +141,7 @@ extension AdamantApiService { encoding: .forceQueryItems(queryItems) ) } - + return response.flatMap { $0.resolved() } } } diff --git a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Transfers.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Transfers.swift index 700f0ed3c..afc0c79c7 100644 --- a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Transfers.swift +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApi+Transfers.swift @@ -6,9 +6,9 @@ // Copyright © 2018 Adamant. All rights reserved. // -import Foundation -import CryptoSwift import BigInt +import CryptoSwift +import Foundation extension AdamantApiService { public func transferFunds(transaction: UnregisteredTransaction) async -> ApiServiceResult { @@ -22,26 +22,29 @@ extension AdamantApiService { sender: String, recipient: String, amount: Decimal, - keypair: Keypair + keypair: Keypair, + date: Date ) async -> ApiServiceResult { let normalizedTransaction = NormalizedTransaction( type: .send, amount: amount, senderPublicKey: keypair.publicKey, requesterPublicKey: nil, - date: .now, + date: date, recipientId: recipient, asset: .init() ) - - guard let transaction = adamantCore.makeSignedTransaction( - transaction: normalizedTransaction, - senderId: sender, - keypair: keypair - ) else { + + guard + let transaction = adamantCore.makeSignedTransaction( + transaction: normalizedTransaction, + senderId: sender, + keypair: keypair + ) + else { return .failure(.internalError(error: InternalAPIError.signTransactionFailed)) } - + return await transferFunds(transaction: transaction) } } diff --git a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiCore.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiCore.swift index 777c5f02b..faca02fe0 100644 --- a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiCore.swift +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiCore.swift @@ -6,21 +6,21 @@ // Copyright © 2023 Adamant. All rights reserved. // -import Foundation import Alamofire +import Foundation -public extension ApiCommands { - static let status = "/api/node/status" - static let version = "/api/peers/version" +extension ApiCommands { + public static let status = "/api/node/status" + public static let version = "/api/peers/version" } public final class AdamantApiCore: Sendable { public let apiCore: APICoreProtocol - + public init(apiCore: APICoreProtocol) { self.apiCore = apiCore } - + public func getNodeStatus( origin: NodeOrigin ) async -> ApiServiceResult { @@ -38,7 +38,7 @@ extension AdamantApiCore: BlockchainHealthCheckableService { let startTimestamp = Date.now.timeIntervalSince1970 let statusResponse = await getNodeStatus(origin: origin) let ping = Date.now.timeIntervalSince1970 - startTimestamp - + return statusResponse.map { statusDto in .init( ping: ping, diff --git a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiService.swift b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiService.swift index 7a3a8b481..bf44184e1 100644 --- a/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiService.swift +++ b/CommonKit/Sources/CommonKit/Services/ApiService/AdamantApiService.swift @@ -8,10 +8,12 @@ import Foundation -public final class AdamantApiService { +public final class AdamantApiService: @unchecked Sendable { + @Atomic private var adamantApiTaskStorage: [UUID: CancellableTask] = [:] + public let adamantCore: AdamantCore public let service: BlockchainHealthCheckWrapper - + public init( healthCheckWrapper: BlockchainHealthCheckWrapper, adamantCore: AdamantCore @@ -19,15 +21,34 @@ public final class AdamantApiService { service = healthCheckWrapper self.adamantCore = adamantCore } - + public func request( waitsForConnectivity: Bool = false, - _ request: @Sendable (APICoreProtocol, NodeOrigin) async -> ApiServiceResult + _ request: @Sendable @escaping (APICoreProtocol, NodeOrigin) async -> ApiServiceResult ) async -> ApiServiceResult { - await service.request( - waitsForConnectivity: waitsForConnectivity - ) { admApiCore, origin in - await request(admApiCore.apiCore, origin) + let task = AdamantApiTask( + task: Task { + await service.request( + waitsForConnectivity: waitsForConnectivity + ) { admApiCore, origin in + let result = await request(admApiCore.apiCore, origin) + do { + try Task.checkCancellation() + } catch { + return .failure(.requestCancelled) + } + return result + } + } + ) + task.storeIn(taskStorage: &adamantApiTaskStorage) + defer { task.removeFrom(taskStorage: &adamantApiTaskStorage) } + return await task.value + } + + public func cancelCurrentTasks() { + adamantApiTaskStorage.forEach { _, task in + task.cancel() } } } @@ -35,9 +56,9 @@ public final class AdamantApiService { extension AdamantApiService: AdamantApiServiceProtocol { @MainActor public var nodesInfoPublisher: AnyObservable { service.nodesInfoPublisher } - + @MainActor public var nodesInfo: NodesListInfo { service.nodesInfo } - + public func healthCheck() { service.healthCheck() } } diff --git a/CommonKit/Sources/CommonKit/Services/HealthCheck/BlockchainHealthCheckWrapper.swift b/CommonKit/Sources/CommonKit/Services/HealthCheck/BlockchainHealthCheckWrapper.swift index cea132917..1d968de5b 100644 --- a/CommonKit/Sources/CommonKit/Services/HealthCheck/BlockchainHealthCheckWrapper.swift +++ b/CommonKit/Sources/CommonKit/Services/HealthCheck/BlockchainHealthCheckWrapper.swift @@ -10,7 +10,7 @@ import Foundation public protocol BlockchainHealthCheckableService { associatedtype Error: HealthCheckableError - + func getStatusInfo(origin: NodeOrigin) async -> Result } @@ -21,7 +21,7 @@ public final class BlockchainHealthCheckWrapper< private let nodesStorage: NodesStorageProtocol private let params: BlockchainHealthCheckParams private var currentRequests = Set() - + nonisolated public init( service: Service, nodesStorage: NodesStorageProtocol, @@ -32,7 +32,7 @@ public final class BlockchainHealthCheckWrapper< ) { self.nodesStorage = nodesStorage self.params = params - + super.init( service: service, isActive: isActive, @@ -42,52 +42,52 @@ public final class BlockchainHealthCheckWrapper< connection: connection, nodes: nodesStorage.getNodesPublisher(group: params.group) ) - + Task { @HealthCheckActor [self] in configure(nodesAdditionalParamsStorage: nodesAdditionalParamsStorage) } } - + public override func healthCheckInternal() async { await super.healthCheckInternal() updateNodesAvailability(update: nil) - + try? await withThrowingTaskGroup(of: Void.self, returning: Void.self) { group in nodes.filter { $0.isEnabled }.forEach { node in group.addTask { @HealthCheckActor [weak self] in guard let self, !currentRequests.contains(node.id) else { return } - + currentRequests.insert(node.id) defer { currentRequests.remove(node.id) } - + let update = await updateNodeStatusInfo(node: node) try Task.checkCancellation() updateNodesAvailability(update: update) } } - + try await group.waitForAll() healthCheckPostProcessing() } } } -private extension BlockchainHealthCheckWrapper { - struct NodeUpdate { +extension BlockchainHealthCheckWrapper { + fileprivate struct NodeUpdate { let id: UUID let info: NodeStatusInfo? let preferMainOrigin: Bool? } - - func configure(nodesAdditionalParamsStorage: NodesAdditionalParamsStorageProtocol) { + + fileprivate func configure(nodesAdditionalParamsStorage: NodesAdditionalParamsStorageProtocol) { nodesAdditionalParamsStorage .fastestNodeMode(group: params.group) .values .sink { [weak self] in await self?.setFastestMode($0) } .store(in: &subscriptions) } - - func updateNodeStatusInfo(node: Node) async -> NodeUpdate { + + fileprivate func updateNodeStatusInfo(node: Node) async -> NodeUpdate { guard node.preferMainOrigin == nil, let altOrigin = node.altOrigin @@ -98,7 +98,7 @@ private extension BlockchainHealthCheckWrapper { preferMainOrigin: nil ) } - + switch await service.getStatusInfo(origin: node.mainOrigin) { case let .success(info): return .init( @@ -123,90 +123,93 @@ private extension BlockchainHealthCheckWrapper { } } } - - func applyUpdate(update: NodeUpdate) { + + fileprivate func applyUpdate(update: NodeUpdate) { updateNode(id: update.id) { node in if let preferMainOrigin = update.preferMainOrigin { node.preferMainOrigin = preferMainOrigin } - - guard let info = update.info else { return node.connectionStatus = .offline } + + guard let info = update.info else { + node.connectionStatus = .offline + return + } node.wsEnabled = info.wsEnabled node.updateWsPort(info.wsPort) node.version = info.version node.height = info.height node.ping = info.ping - + guard let version = info.version, let minNodeVersion = params.minNodeVersion, version < minNodeVersion else { return } - + node.connectionStatus = .notAllowed(.outdatedApiVersion) } } - - func updateNodesAvailability(update: NodeUpdate?) { + + fileprivate func updateNodesAvailability(update: NodeUpdate?) { let forceIncludeId = update?.info != nil ? update?.id : nil - + if let update = update { applyUpdate(update: update) } - + let workingNodes = nodes.filter { $0.isEnabled && ($0.isWorkingStatus) || $0.id == forceIncludeId } - + let actualHeightsRange = getActualNodeHeightsRange( heights: workingNodes.compactMap { $0.height }, group: params.group, nodeHeightEpsilon: params.nodeHeightEpsilon ) - + workingNodes.forEach { node in var status: NodeConnectionStatus? - - if - let version = node.version, + + if let version = node.version, let minNodeVersion = params.minNodeVersion, version < minNodeVersion { status = .notAllowed(.outdatedApiVersion) } else { - status = node.height.map { height in - actualHeightsRange?.contains(height) ?? false - ? .allowed - : .synchronizing(isFinal: !node.connectionStatus.notFinalSync) - } ?? .none + status = + node.height.map { height in + actualHeightsRange?.contains(height) ?? false + ? .allowed + : .synchronizing(isFinal: !node.connectionStatus.notFinalSync) + } ?? .none } - + updateNode(id: node.id) { $0.connectionStatus = status } } } - - func updateNode(id: UUID, mutate: (inout Node) -> Void) { + + fileprivate func updateNode(id: UUID, mutate: (inout Node) -> Void) { nodesStorage.updateNode( id: id, group: params.group, mutate: mutate ) } - - func healthCheckPostProcessing() { + + fileprivate func healthCheckPostProcessing() { nodes.forEach { node in guard case let .synchronizing(isFinal) = node.connectionStatus, !isFinal else { return } - + updateNode(id: node.id) { $0.connectionStatus = .synchronizing(isFinal: true) } } } } -private extension Node { - var isWorkingStatus: Bool { +extension Node { + fileprivate var isWorkingStatus: Bool { switch connectionStatus { case .allowed, .synchronizing, .none: return isEnabled @@ -228,28 +231,28 @@ private func getActualNodeHeightsRange( ) -> ClosedRange? { let heights = heights.sorted() var bestInterval: NodeHeightsInterval? - + for i in heights.indices { var currentInterval = NodeHeightsInterval( - range: heights[i] ... heights[i] + nodeHeightEpsilon - 1, + range: heights[i]...heights[i] + nodeHeightEpsilon - 1, count: 1 ) - - for j in i + 1 ..< heights.endIndex { + + for j in i + 1..= bestInterval?.count ?? .zero { bestInterval = currentInterval } } - + return bestInterval?.range } -private extension Optional where Wrapped == NodeConnectionStatus { - var notFinalSync: Bool { +extension Optional where Wrapped == NodeConnectionStatus { + fileprivate var notFinalSync: Bool { switch self { case .offline, .notAllowed, .none: false diff --git a/CommonKit/Sources/CommonKit/Services/HealthCheck/HealthCheckWrapper.swift b/CommonKit/Sources/CommonKit/Services/HealthCheck/HealthCheckWrapper.swift index fefbffad6..6fb448ada 100644 --- a/CommonKit/Sources/CommonKit/Services/HealthCheck/HealthCheckWrapper.swift +++ b/CommonKit/Sources/CommonKit/Services/HealthCheck/HealthCheckWrapper.swift @@ -6,14 +6,14 @@ // Copyright © 2023 Adamant. All rights reserved. // -import Foundation +import AsyncAlgorithms import Combine +import Foundation import UIKit -import AsyncAlgorithms public protocol HealthCheckableError: Error { var isNetworkError: Bool { get } - + static var noNetworkError: Self { get } static func noEndpointsError(nodeGroupName: String) -> Self } @@ -22,23 +22,23 @@ public protocol HealthCheckableError: Error { open class HealthCheckWrapper: Sendable { @ObservableValue private(set) var nodes: [Node] = .init() @ObservableValue private var sortedAllowedNodes: [Node] = .init() - + @MainActor private var _nodesInfo: ObservableValue = .init(.default) - + @MainActor public var nodesInfoPublisher: AnyObservable { _nodesInfo.removeDuplicates().eraseToAnyPublisher() } - + @MainActor public var nodesInfo: NodesListInfo { _nodesInfo.value } - + let name: String var subscriptions: Set = .init() - + public let service: Service public private(set) var fastestNodeMode = true private let normalUpdateInterval: TimeInterval @@ -49,7 +49,7 @@ open class HealthCheckWrapper: S private var appState: AppState = .active private var performHealthCheckWhenBecomeActive = false private var lastUpdateTime: Date? - + public nonisolated init( service: Service, isActive: Bool, @@ -64,12 +64,12 @@ open class HealthCheckWrapper: S self.name = name self.normalUpdateInterval = normalUpdateInterval self.crucialUpdateInterval = crucialUpdateInterval - + Task { @HealthCheckActor [self] in configure(nodes: nodes, connection: connection) } } - + public func request( waitsForConnectivity: Bool, _ requestAction: @Sendable (Service, NodeOrigin) async -> Result @@ -77,15 +77,15 @@ open class HealthCheckWrapper: S defer { updateSortedNodes() } var usedNodesIds: Set = .init() var lastConnectionError: Error? - + while true { let node = await nodesForRequest(waitsForConnectivity: waitsForConnectivity) .first { !usedNodesIds.contains($0.id) } - + guard let node else { break } usedNodesIds.insert(node.id) let response = await requestAction(service, node.preferredOrigin) - + switch response { case .success: return response @@ -94,30 +94,31 @@ open class HealthCheckWrapper: S lastConnectionError = error } } - + healthCheck() - - lastConnectionError = lastConnectionError + + lastConnectionError = + lastConnectionError ?? (nodes.contains { $0.isEnabled } ? .noNetworkError : .noEndpointsError(nodeGroupName: name)) - + return await waitsForConnectivity ? request(waitsForConnectivity: waitsForConnectivity, requestAction) : .failure(lastConnectionError ?? .noEndpointsError(nodeGroupName: name)) } - + public func setFastestMode(_ isOn: Bool) { fastestNodeMode = isOn updateSortedNodes() } - + nonisolated public func healthCheck() { Task { @HealthCheckActor in guard canPerformHealthCheck else { return } lastUpdateTime = .now updateHealthCheckTimerSubscription() - + Task { await healthCheckInternal() guard Task.isCancelled else { return } @@ -125,19 +126,19 @@ open class HealthCheckWrapper: S }.store(in: &healthCheckSubscriptions) } } - + open func healthCheckInternal() async {} } -private extension HealthCheckWrapper { +extension HealthCheckWrapper { private enum AppState { case active case background } - - var canPerformHealthCheck: Bool { + + fileprivate var canPerformHealthCheck: Bool { guard isActive else { return false } - + switch appState { case .active: return true @@ -145,12 +146,13 @@ private extension HealthCheckWrapper { return false } } - - func configure(nodes: AnyObservable<[Node]>, connection: AnyObservable) { - let connection = connection + + fileprivate func configure(nodes: AnyObservable<[Node]>, connection: AnyObservable) { + let connection = + connection .removeDuplicates() .filter { $0 } - + nodes .removeDuplicates() .handleEvents(receiveOutput: { [weak self] in self?.updateNodes($0) }) @@ -158,31 +160,31 @@ private extension HealthCheckWrapper { .combineLatest(connection) .sink { [weak self] _ in self?.healthCheck() } .store(in: &subscriptions) - + $sortedAllowedNodes .map { $0.isEmpty } .removeDuplicates() .sink { [weak self] _ in self?.updateHealthCheckTimerSubscription() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: UIApplication.didBecomeActiveNotification, object: nil) .sink { @HealthCheckActor [weak self] _ in self?.didBecomeActiveAction() } .store(in: &subscriptions) - + NotificationCenter.default .notifications(named: UIApplication.willResignActiveNotification, object: nil) .sink { @HealthCheckActor [weak self] _ in self?.willResignActiveAction() } .store(in: &subscriptions) } - - func nodesForRequest(waitsForConnectivity: Bool) async -> [Node] { + + fileprivate func nodesForRequest(waitsForConnectivity: Bool) async -> [Node] { await $sortedAllowedNodes.values.first { !waitsForConnectivity || !$0.isEmpty } ?? .init() } - - func updateHealthCheckTimerSubscription() { + + fileprivate func updateHealthCheckTimerSubscription() { healthCheckTimerSubscription = Timer.publish( every: sortedAllowedNodes.isEmpty ? crucialUpdateInterval @@ -193,42 +195,43 @@ private extension HealthCheckWrapper { self?.healthCheck() } } - - func didBecomeActiveAction() { + + fileprivate func didBecomeActiveAction() { guard appState != .active else { return } appState = .active - - let timeToUpdate = lastUpdateTime?.addingTimeInterval(normalUpdateInterval / 3) + + let timeToUpdate = + lastUpdateTime?.addingTimeInterval(normalUpdateInterval / 3) ?? .adamantNullDate - + guard performHealthCheckWhenBecomeActive || Date.now >= timeToUpdate else { return } performHealthCheckWhenBecomeActive = false healthCheck() } - - func willResignActiveAction() { + + fileprivate func willResignActiveAction() { appState = .background healthCheckSubscriptions = .init() } - - func updateNodes(_ newNodes: [Node]) { + + fileprivate func updateNodes(_ newNodes: [Node]) { nodes = newNodes updateSortedNodes() } - - func updateSortedNodes() { + + fileprivate func updateSortedNodes() { sortedAllowedNodes = nodes.getAllowedNodes( sortedBySpeedDescending: fastestNodeMode, needWS: false ) - + let newInfo = NodesListInfo(nodes: nodes, chosenNodeId: sortedAllowedNodes.first?.id) Task { @MainActor in _nodesInfo.send(newInfo) } } } -private extension Sequence where Element == Node { - func doesNeedHealthCheck( +extension Sequence where Element == Node { + fileprivate func doesNeedHealthCheck( _ nodes: Nodes ) -> Bool where Nodes.Element == Self.Element { Set(self.map { NodeComparisonInfo(node: $0) }) @@ -241,7 +244,7 @@ private struct NodeComparisonInfo: Hashable { let mainOrigin: NodeOriginComparisonInfo let altOrigin: NodeOriginComparisonInfo? let isEnabled: Bool - + init(node: Node) { id = node.id mainOrigin = .init(origin: node.mainOrigin) @@ -254,7 +257,7 @@ private struct NodeOriginComparisonInfo: Hashable { let scheme: NodeOrigin.URLScheme let host: String let port: Int? - + init(origin: NodeOrigin) { scheme = origin.scheme host = origin.host diff --git a/CommonKit/Sources/CommonKit/Services/KeychainStore.swift b/CommonKit/Sources/CommonKit/Services/KeychainStore.swift index da8daa8c8..fca5844ac 100644 --- a/CommonKit/Sources/CommonKit/Services/KeychainStore.swift +++ b/CommonKit/Sources/CommonKit/Services/KeychainStore.swift @@ -6,136 +6,139 @@ // Copyright © 2018 Adamant. All rights reserved. // +import CryptoKit import Foundation @preconcurrency import KeychainAccess import RNCryptor -import CryptoKit -public final class KeychainStore: SecuredStore, @unchecked Sendable { +public final class KeychainStore: SecureStore, @unchecked Sendable { // MARK: - Properties private static let keychain = Keychain(service: "\(AdamantSecret.appIdentifierPrefix).im.adamant.messenger") - + private let secureStorage: SecureStorageProtocol - + private let keychainStoreIdAlias = "com.adamant.messenger.id" private var keychainPassword: String? - + private let oldKeychainService = "im.adamant" private let migrationKey = "migrated" private let migrationValue = "2" private lazy var userDefaults = UserDefaults(suiteName: sharedGroup) - + public init(secureStorage: SecureStorageProtocol) { self.secureStorage = secureStorage - + migrateUserDefaultsIfNeeded() clearIfNeeded() configure() migrateIfNeeded() } - - // MARK: - SecuredStore - + + // MARK: - SecureStore + public func get(_ key: String) -> T? { guard let data = getValue(key) else { return nil } - + guard !(T.self == String.self) else { return String(data: data, encoding: .utf8) as? T } - + return try? JSONDecoder().decode(T.self, from: data) } - + public func set(_ value: T, for key: String) { if let string = value as? String, - let data = string.data(using: .utf8) { + let data = string.data(using: .utf8) + { setValue(data, for: key) return } - + guard let data = try? JSONEncoder().encode(value) else { return } setValue(data, for: key) } - + public func remove(_ key: String) { try? KeychainStore.keychain.remove(key) } } -private extension KeychainStore { - func configure() { +extension KeychainStore { + fileprivate func configure() { guard let privateKey = secureStorage.getPrivateKey(), - let publicKey = secureStorage.getPublicKey(privateKey: privateKey) + let publicKey = secureStorage.getPublicKey(privateKey: privateKey) else { return } - + if let savedKey = getData(for: keychainStoreIdAlias) { let decryptedData = secureStorage.decrypt( data: savedKey, privateKey: privateKey ) - + keychainPassword = decryptedData?.base64EncodedString() return } - + let keychainRandomKeyData = SymmetricKey(size: .bits256) .withUnsafeBytes { Data($0) } let keychainRandomKey = keychainRandomKeyData.base64EncodedString() - - guard let encryptedData = secureStorage.encrypt( - data: keychainRandomKeyData, - publicKey: publicKey - ) else { return } - + + guard + let encryptedData = secureStorage.encrypt( + data: keychainRandomKeyData, + publicKey: publicKey + ) + else { return } + keychainPassword = keychainRandomKey setData(encryptedData, for: keychainStoreIdAlias) } - - func clearIfNeeded() { + + fileprivate func clearIfNeeded() { guard let userDefaults = userDefaults else { return } - + let isFirstRun = !userDefaults.bool(forKey: firstRun) - + guard isFirstRun else { return } - + userDefaults.set(true, forKey: firstRun) - + purgeStore() } - - func getValue(_ key: String) -> Data? { + + fileprivate func getValue(_ key: String) -> Data? { guard let keychainPassword = keychainPassword, - let data = getData(for: key) - else { return nil} - + let data = getData(for: key) + else { return nil } + return decrypt( data: data, password: keychainPassword ) } - - func setValue(_ value: Data, for key: String) { + + fileprivate func setValue(_ value: Data, for key: String) { guard let keychainPassword = keychainPassword else { return } - + let encryptedValue = encrypt( data: value, password: keychainPassword ) - + setData(encryptedValue, for: key) } - - func getData(for key: String) -> Data? { + + fileprivate func getData(for key: String) -> Data? { try? KeychainStore.keychain.getData(key) } - - func setData(_ value: Data, for key: String) { + + fileprivate func setData(_ value: Data, for key: String) { try? KeychainStore.keychain.set(value, key: key) } - - func encrypt( + + fileprivate func encrypt( data: Data, password: String ) -> Data { @@ -144,15 +147,15 @@ private extension KeychainStore { withPassword: password ) } - - func decrypt( + + fileprivate func decrypt( data: Data, password: String ) -> Data? { try? RNCryptor.decrypt(data: data, withPassword: password) } - - func decryptOld( + + fileprivate func decryptOld( string: String, password: String ) -> Data? { @@ -161,65 +164,65 @@ private extension KeychainStore { } return try? RNCryptor.decrypt(data: encryptedData, withPassword: password) } - - func purgeStore() { + + fileprivate func purgeStore() { try? KeychainStore.keychain.removeAll() - NotificationCenter.default.post(name: Notification.Name.SecuredStore.securedStorePurged, object: self) + NotificationCenter.default.post(name: Notification.Name.SecureStore.SecureStorePurged, object: self) } } -private extension KeychainStore { +extension KeychainStore { // MARK: - Migration - + /* * Long time ago, we didn't use shared keychain. Now we do. We need to move all items from old keychain to new. And drop old one. */ - - func migrateIfNeeded() { + + fileprivate func migrateIfNeeded() { let migrated = KeychainStore.keychain[migrationKey] - + guard keychainPassword != nil, - migrated != migrationValue + migrated != migrationValue else { return } - + let oldKeychain = Keychain(service: oldKeychainService) - + migrate( keychain: oldKeychain, oldPassword: AdamantSecret.oldKeychainPass ) - + migrate( keychain: KeychainStore.keychain, oldPassword: AdamantSecret.keychainValuePassword ) - + try? KeychainStore.keychain.set(migrationValue, key: migrationKey) try? oldKeychain.removeAll() } - - func migrate( + + fileprivate func migrate( keychain: Keychain, oldPassword: String ) { for key in keychain.allKeys() { guard key != keychainStoreIdAlias, - let oldEncryptedValue = keychain[key], - let value = decryptOld( + let oldEncryptedValue = keychain[key], + let value = decryptOld( string: oldEncryptedValue, password: oldPassword - ) + ) else { continue } - + try? KeychainStore.keychain.remove(key) setValue(value, for: key) } } - - func migrateUserDefaultsIfNeeded() { + + fileprivate func migrateUserDefaultsIfNeeded() { let migrated = KeychainStore.keychain[migrationKey] guard migrated != migrationValue else { return } - + let value = UserDefaults.standard.bool(forKey: firstRun) userDefaults?.set(value, forKey: firstRun) } diff --git a/CommonKit/Sources/CommonKit/Services/NodesAdditionalParamsStorage.swift b/CommonKit/Sources/CommonKit/Services/NodesAdditionalParamsStorage.swift index 6da72bbeb..623838c86 100644 --- a/CommonKit/Sources/CommonKit/Services/NodesAdditionalParamsStorage.swift +++ b/CommonKit/Sources/CommonKit/Services/NodesAdditionalParamsStorage.swift @@ -6,26 +6,26 @@ // Copyright © 2023 Adamant. All rights reserved. // -import Foundation import Combine +import Foundation public final class NodesAdditionalParamsStorage: NodesAdditionalParamsStorageProtocol { @Atomic private var fastestNodeModeValues: ObservableValue<[NodeGroup: Bool]> - - private let securedStore: SecuredStore + + private let SecureStore: SecureStore private var subscription: AnyCancellable? - + public func isFastestNodeMode(group: NodeGroup) -> Bool { fastestNodeModeValues.wrappedValue[group] ?? group.defaultFastestNodeMode } - + public func fastestNodeMode(group: NodeGroup) -> AnyObservable { fastestNodeModeValues .map { $0[group] ?? group.defaultFastestNodeMode } .removeDuplicates() .eraseToAnyPublisher() } - + public func setFastestNodeMode(groups: Set, value: Bool) { $fastestNodeModeValues.mutate { dict in groups.forEach { @@ -33,20 +33,22 @@ public final class NodesAdditionalParamsStorage: NodesAdditionalParamsStoragePro } } } - + public func setFastestNodeMode(group: NodeGroup, value: Bool) { fastestNodeModeValues.wrappedValue[group] = value } - - public init(securedStore: SecuredStore) { - self.securedStore = securedStore - - _fastestNodeModeValues = .init(wrappedValue: .init( - wrappedValue: securedStore.get( - StoreKey.NodesAdditionalParamsStorage.fastestNodeMode - ) ?? [:] - )) - + + public init(SecureStore: SecureStore) { + self.SecureStore = SecureStore + + _fastestNodeModeValues = .init( + wrappedValue: .init( + wrappedValue: SecureStore.get( + StoreKey.NodesAdditionalParamsStorage.fastestNodeMode + ) ?? [:] + ) + ) + subscription = fastestNodeModeValues.removeDuplicates().sink { [weak self] in guard let self = self, subscription != nil else { return } saveFastestNodeMode($0) @@ -54,8 +56,8 @@ public final class NodesAdditionalParamsStorage: NodesAdditionalParamsStoragePro } } -private extension NodesAdditionalParamsStorage { - func saveFastestNodeMode(_ dict: [NodeGroup: Bool]) { - securedStore.set(dict, for: StoreKey.NodesAdditionalParamsStorage.fastestNodeMode) +extension NodesAdditionalParamsStorage { + fileprivate func saveFastestNodeMode(_ dict: [NodeGroup: Bool]) { + SecureStore.set(dict, for: StoreKey.NodesAdditionalParamsStorage.fastestNodeMode) } } diff --git a/CommonKit/Sources/CommonKit/Services/NodesMergingService.swift b/CommonKit/Sources/CommonKit/Services/NodesMergingService.swift index 24aa3a00a..abded1c45 100644 --- a/CommonKit/Sources/CommonKit/Services/NodesMergingService.swift +++ b/CommonKit/Sources/CommonKit/Services/NodesMergingService.swift @@ -12,35 +12,35 @@ public struct NodesMergingService: NodesMergingServiceProtocol { defaultNodes: [NodeGroup: [Node]] ) -> [NodeGroup: [Node]] { var resultNodes = savedNodes - + defaultNodes.keys.forEach { group in guard resultNodes[group] == nil else { return } resultNodes[group] = .init() } - + resultNodes.forEach { group, nodes in guard let defaultNodes = defaultNodes[group] else { return } resultNodes[group] = merge(savedNodes: nodes, defaultNodes: defaultNodes) } - + return resultNodes } - + public init() {} } -private extension NodesMergingService { - func merge(savedNodes: [Node], defaultNodes: [Node]) -> [Node] { +extension NodesMergingService { + fileprivate func merge(savedNodes: [Node], defaultNodes: [Node]) -> [Node] { var resultNodes = savedNodes var defaultNodes = defaultNodes var removedNodesIndexes: [Int] = .init() - + // Merging default nodes resultNodes.enumerated().forEach { index, node in switch node.type { case .default: let defaultNodeIndex = defaultNodes.firstIndex { $0.isSame(node) } - + if let defaultNodeIndex = defaultNodeIndex { resultNodes[index].merge(defaultNodes[defaultNodeIndex]) defaultNodes.remove(at: defaultNodeIndex) @@ -53,39 +53,40 @@ private extension NodesMergingService { break } } - + removedNodesIndexes.reversed().forEach { resultNodes.remove(at: $0) } - + // We are filtering default nodes to avoid duplications. // Maybe a new default node is a user's old custom node - return resultNodes + defaultNodes.filter { defaultNode in - !resultNodes.contains { $0.isSame(defaultNode) } - } + return resultNodes + + defaultNodes.filter { defaultNode in + !resultNodes.contains { $0.isSame(defaultNode) } + } } } -private extension Node { - mutating func merge(_ node: Node) { +extension Node { + fileprivate mutating func merge(_ node: Node) { mainOrigin.merge(node.mainOrigin) - + guard let mergedAltOrigin = node.altOrigin else { altOrigin = nil return } - + guard altOrigin != nil else { altOrigin = mergedAltOrigin return } - + altOrigin?.merge(mergedAltOrigin) } } -private extension NodeOrigin { - mutating func merge(_ origin: NodeOrigin) { +extension NodeOrigin { + fileprivate mutating func merge(_ origin: NodeOrigin) { scheme = origin.scheme host = origin.host port = origin.port diff --git a/CommonKit/Sources/CommonKit/Services/NodesStorage.swift b/CommonKit/Sources/CommonKit/Services/NodesStorage.swift index 5830445e0..5ad3fe482 100644 --- a/CommonKit/Sources/CommonKit/Services/NodesStorage.swift +++ b/CommonKit/Sources/CommonKit/Services/NodesStorage.swift @@ -6,32 +6,32 @@ // Copyright © 2023 Adamant. All rights reserved. // -import Foundation import Combine +import Foundation public final class NodesStorage: NodesStorageProtocol, @unchecked Sendable { public typealias DefaultNodesGetter = @Sendable (Set) -> [NodeGroup: [Node]] - + @Atomic private var items: ObservableValue<[NodeGroup: [Node]]> - + public var nodesPublisher: AnyObservable<[NodeGroup: [Node]]> { items .map { $0.mapValues { $0.filter { !$0.isHidden } } } .removeDuplicates() .eraseToAnyPublisher() } - + private var subscription: AnyCancellable? - private let securedStore: SecuredStore + private let SecureStore: SecureStore private let defaultNodes: DefaultNodesGetter - + public func getNodesPublisher(group: NodeGroup) -> AnyObservable<[Node]> { nodesPublisher .map { $0[group] ?? .init() } .removeDuplicates() .eraseToAnyPublisher() } - + public func addNode(_ node: Node, group: NodeGroup) { $items.mutate { items in if items.wrappedValue[group] == nil { @@ -41,13 +41,13 @@ public final class NodesStorage: NodesStorageProtocol, @unchecked Sendable { } } } - + public func removeNode(id: UUID, group: NodeGroup) { $items.mutate { items in guard let index = items.wrappedValue[group]?.firstIndex(where: { $0.id == id }) else { return } - + switch items.wrappedValue[group]?[safe: index]?.type { case .default: items.wrappedValue[group]?[index].type = .default(isHidden: true) @@ -58,23 +58,23 @@ public final class NodesStorage: NodesStorageProtocol, @unchecked Sendable { } } } - + public func updateNode(id: UUID, group: NodeGroup, mutate: (inout Node) -> Void) { $items.mutate { items in guard let index = items.wrappedValue[group]?.firstIndex(where: { $0.id == id }), var node = items.wrappedValue[group]?[safe: index] else { return } - + let previousValue = node mutate(&node) - + if !node.isEnabled { node.connectionStatus = nil node.height = nil node.ping = nil } - + switch node.type { case .default: guard !previousValue.isSame(node) else { break } @@ -82,46 +82,51 @@ public final class NodesStorage: NodesStorageProtocol, @unchecked Sendable { case .custom: break } - + guard node != previousValue else { return } items.wrappedValue[group]?[index] = node } } - + public func resetNodes(_ groups: Set) { let defaultNodes = defaultNodes(groups) - + $items.mutate { items in for group in groups { items.wrappedValue[group] = defaultNodes[group] ?? .init() } } } - + public init( - securedStore: SecuredStore, + SecureStore: SecureStore, nodesMergingService: NodesMergingServiceProtocol, defaultNodes: @escaping DefaultNodesGetter ) { - self.securedStore = securedStore + self.SecureStore = SecureStore self.defaultNodes = defaultNodes - - let dto: NodesKeychainDTO? = securedStore.get(StoreKey.NodesStorage.nodes) - - let savedNodes = dto?.data.values.mapValues { $0.map { $0.mapToModel() } } - ?? migrateOldNodesData(securedStore: securedStore) + + let dto: NodesKeychainDTO? = SecureStore.get(StoreKey.NodesStorage.nodes) + + let savedNodes = + dto?.data.values.mapValues { $0.map { $0.mapToModel() } } + ?? migrateOldNodesData(SecureStore: SecureStore) ?? .init() - - _items = .init(.init(wrappedValue: nodesMergingService.merge( - savedNodes: savedNodes, - defaultNodes: defaultNodes(.init(NodeGroup.allCases)) - ))) - + + _items = .init( + .init( + wrappedValue: nodesMergingService.merge( + savedNodes: savedNodes, + defaultNodes: defaultNodes(.init(NodeGroup.allCases)) + ) + ) + ) + subscription = items.removeDuplicates().sink { [weak self] in guard let self = self else { return } saveNodes(nodes: $0) } - + // Applying empty mutations, so post-mutation code in `func updateNode` will be executed for (group, nodes) in items.wrappedValue { for node in nodes { @@ -131,31 +136,31 @@ public final class NodesStorage: NodesStorageProtocol, @unchecked Sendable { } } -private extension NodesStorage { - func saveNodes(nodes: [NodeGroup: [Node]]) { +extension NodesStorage { + fileprivate func saveNodes(nodes: [NodeGroup: [Node]]) { let nodesDto = NodesKeychainDTO(nodes.mapValues { $0.map { $0.mapToDto() } }) - securedStore.set(nodesDto, for: StoreKey.NodesStorage.nodes) + SecureStore.set(nodesDto, for: StoreKey.NodesStorage.nodes) } } -private func migrateOldNodesData(securedStore: SecuredStore) -> [NodeGroup: [Node]]? { - let dto: SafeDecodingArray? = securedStore.get(StoreKey.NodesStorage.nodes) +private func migrateOldNodesData(SecureStore: SecureStore) -> [NodeGroup: [Node]]? { + let dto: SafeDecodingArray? = SecureStore.get(StoreKey.NodesStorage.nodes) guard let dto = dto else { return nil } var result: [NodeGroup: [Node]] = [:] - + dto.forEach { if result[$0.group] == nil { result[$0.group] = [] } - + result[$0.group]?.append($0.node.mapToModernDto(group: $0.group).mapToModel()) } - + return result } -private extension Node { - var isHidden: Bool { +extension Node { + fileprivate var isHidden: Bool { switch type { case let .default(isHidden): return isHidden diff --git a/CommonKit/Sources/CommonKit/View/NotificationWarningView.swift b/CommonKit/Sources/CommonKit/View/NotificationWarningView.swift index d60b3f78d..131c90973 100644 --- a/CommonKit/Sources/CommonKit/View/NotificationWarningView.swift +++ b/CommonKit/Sources/CommonKit/View/NotificationWarningView.swift @@ -5,21 +5,21 @@ // Created by Andrey Golubenko on 19.07.2023. // -import UIKit import SnapKit +import UIKit public final class NotificationWarningView: UIView { public var emoji: Emoji? = .allCases.randomElement() { didSet { update() } } - + public var message: String = .empty { didSet { update() } } - + private lazy var emojiLabel = UILabel(font: .systemFont(ofSize: 75)) private lazy var messageLabel = UILabel() - + private lazy var verticalStack: UIStackView = { let view = UIStackView(arrangedSubviews: [emojiLabel, messageLabel]) view.axis = .vertical @@ -27,33 +27,33 @@ public final class NotificationWarningView: UIView { view.alignment = .center return view }() - + override public init(frame: CGRect) { super.init(frame: frame) setup() } - + required init?(coder: NSCoder) { super.init(coder: coder) setup() } } -public extension NotificationWarningView { - enum Emoji: String, CaseIterable { - case 😔 ,😟 ,😭 ,😰 ,😨 ,🤭 ,😯 ,😣 ,😖 ,🤕 +extension NotificationWarningView { + public enum Emoji: String, CaseIterable { + case 😔, 😟, 😭, 😰, 😨, 🤭, 😯, 😣, 😖, 🤕 } } -private extension NotificationWarningView { - func setup() { +extension NotificationWarningView { + fileprivate func setup() { addSubview(verticalStack) verticalStack.snp.makeConstraints { $0.directionalEdges.equalToSuperview().inset(20) } } - - func update() { + + fileprivate func update() { emojiLabel.text = emoji?.rawValue messageLabel.text = message } diff --git a/CommonKit/Sources/CommonKit/Models/ERC20Token.swift b/CommonKit/Sources/CommonKit/Wallets/ERC20Token.swift similarity index 94% rename from CommonKit/Sources/CommonKit/Models/ERC20Token.swift rename to CommonKit/Sources/CommonKit/Wallets/ERC20Token.swift index acca39b29..0596934d2 100644 --- a/CommonKit/Sources/CommonKit/Models/ERC20Token.swift +++ b/CommonKit/Sources/CommonKit/Wallets/ERC20Token.swift @@ -18,11 +18,12 @@ public struct ERC20Token: Sendable { public let defaultOrdinalLevel: Int? public let reliabilityGasPricePercent: Int public let reliabilityGasLimitPercent: Int + public let increasedGasPricePercent: Decimal public let defaultGasPriceGwei: Int public let defaultGasLimit: Int public let warningGasPriceGwei: Int public let transferDecimals: Int - + public var logo: UIImage { .asset(named: "\(symbol.lowercased())_wallet") ?? .asset(named: "ethereum_wallet") diff --git a/CommonKit/Sources/CommonKit/Wallets/ERC20TokenAssembler.swift b/CommonKit/Sources/CommonKit/Wallets/ERC20TokenAssembler.swift new file mode 100644 index 000000000..7a25b235c --- /dev/null +++ b/CommonKit/Sources/CommonKit/Wallets/ERC20TokenAssembler.swift @@ -0,0 +1,36 @@ +// +// ERC20TokenComparer.swift +// CommonKit +// +// Created by Владимир Клевцов on 17.1.25.. +// +import AdamantWalletsKit +import Foundation + +public enum ERC20TokenAssembly { + public static func getERC20Tokens(tokensStorage: AnyTokensStorage) -> [ERC20Token] { + guard let erc20Tokens = tokensStorage[.declared(.ethereum)] else { return [] } + return erc20Tokens.map { _, info in + erc20TokenWith(coinInfo: info) + } + } + + private static func erc20TokenWith(coinInfo info: CoinInfoDTO) -> ERC20Token { + ERC20Token( + symbol: info.symbol, + name: info.name, + contractAddress: info.contractId ?? "", + decimals: info.decimals, + naturalUnits: info.decimals, + defaultVisibility: info.defaultVisibility ?? false, + defaultOrdinalLevel: info.defaultOrdinalLevel, + reliabilityGasPricePercent: info.reliabilityGasPricePercent ?? .zero, + reliabilityGasLimitPercent: info.reliabilityGasLimitPercent ?? .zero, + increasedGasPricePercent: Decimal(info.increasedGasPricePercent ?? .zero), + defaultGasPriceGwei: info.defaultGasPriceGwei ?? .zero, + defaultGasLimit: info.defaultGasLimit ?? .zero, + warningGasPriceGwei: info.warningGasPriceGwei ?? .zero, + transferDecimals: info.cryptoTransferDecimals + ) + } +} diff --git a/CommonKit/Sources/CommonKit/Wallets/Providers/CoinInfoProvider.swift b/CommonKit/Sources/CommonKit/Wallets/Providers/CoinInfoProvider.swift new file mode 100644 index 000000000..74f600858 --- /dev/null +++ b/CommonKit/Sources/CommonKit/Wallets/Providers/CoinInfoProvider.swift @@ -0,0 +1,12 @@ +// +// CoinInfoProvider.swift +// CommonKit +// +// Created by Владимир Клевцов on 17.1.25.. +// +import AdamantWalletsKit +import Foundation + +public class CoinInfoProvider { + public static var storage: AnyTokensStorage? +} diff --git a/CommonKit/Sources/CommonKit/Wallets/ethereumTokensList.swift b/CommonKit/Sources/CommonKit/Wallets/ethereumTokensList.swift new file mode 100644 index 000000000..320ceafcf --- /dev/null +++ b/CommonKit/Sources/CommonKit/Wallets/ethereumTokensList.swift @@ -0,0 +1,6 @@ +import AdamantWalletsKit +import Foundation + +extension ERC20Token { + public static var supportedTokens: [ERC20Token] = [] +} diff --git a/CommonKit/Tests/CommonKitTests/CommonKitTests.swift b/CommonKit/Tests/CommonKitTests/CommonKitTests.swift index 404e870be..02976d8ba 100644 --- a/CommonKit/Tests/CommonKitTests/CommonKitTests.swift +++ b/CommonKit/Tests/CommonKitTests/CommonKitTests.swift @@ -1,4 +1,5 @@ import XCTest + @testable import CommonKit final class CommonKitTests: XCTestCase { diff --git a/CommonKit/Tests/CommonKitTests/HexBytesTests.swift b/CommonKit/Tests/CommonKitTests/HexBytesTests.swift new file mode 100644 index 000000000..d927ff361 --- /dev/null +++ b/CommonKit/Tests/CommonKitTests/HexBytesTests.swift @@ -0,0 +1,49 @@ +import XCTest + +@testable import CommonKit + +final class HexBytesTests: XCTestCase { + func test_hexBytes_smallStringWithLettersAndDigits() { + let input = "48656c6c6f" + let expectedOutput: [UInt8] = [0x48, 0x65, 0x6c, 0x6c, 0x6f] + XCTAssertEqual(input.hexBytes(), expectedOutput) + } + + func test_hexBytes_bigStringWithAllLettersAndAllDigits() { + let input = "1234567890abcdef" + let expectedOutput: [UInt8] = [0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef] + XCTAssertEqual(input.hexBytes(), expectedOutput) + } + + func test_hexBytes_emptyString() { + let input = "" + let expectedOutput: [UInt8] = [] + XCTAssertEqual(input.hexBytes(), expectedOutput) + } + + func test_hexBytes_withEdgeHexValues() { + let input = "00ff" + let expectedOutput: [UInt8] = [0x00, 0xff] + XCTAssertEqual(input.hexBytes(), expectedOutput) + } + + func test_hexBytes_withUppercaseLetters() { + let input = "ABCDEF" + let expectedOutput: [UInt8] = [0xAB, 0xCD, 0xEF] + XCTAssertEqual(input.hexBytes(), expectedOutput) + } + + func test_hexBytes_replacesInvalidHexesWithZeros() { + let input = "AXBXCDEF" + let expectedOutput: [UInt8] = [0x00, 0x00, 0xCD, 0xEF] + XCTAssertEqual(input.hexBytes(), expectedOutput) + } + + func test_hexBytes_performance() { + let input = String.init(repeating: "0123456789abcdef", count: 20000) + + measure { + _ = input.hexBytes() + } + } +} diff --git a/FilesPickerKit/Package.swift b/FilesPickerKit/Package.swift index a6e60c84c..5dfc74389 100644 --- a/FilesPickerKit/Package.swift +++ b/FilesPickerKit/Package.swift @@ -12,7 +12,8 @@ let package = Package( // Products define the executables and libraries a package produces, making them visible to other packages. .library( name: "FilesPickerKit", - targets: ["FilesPickerKit"]), + targets: ["FilesPickerKit"] + ) ], dependencies: [ .package(path: "../CommonKit"), @@ -27,6 +28,7 @@ let package = Package( ), .testTarget( name: "FilesPickerKitTests", - dependencies: ["FilesPickerKit"]), + dependencies: ["FilesPickerKit"] + ) ] ) diff --git a/FilesPickerKit/Sources/FilesPickerKit/Helpers/FilesPickerKit.swift b/FilesPickerKit/Sources/FilesPickerKit/Helpers/FilesPickerKit.swift index cdc556dab..7f23137b5 100644 --- a/FilesPickerKit/Sources/FilesPickerKit/Helpers/FilesPickerKit.swift +++ b/FilesPickerKit/Sources/FilesPickerKit/Helpers/FilesPickerKit.swift @@ -1,88 +1,89 @@ // The Swift Programming Language // https://docs.swift.org/swift-book -import CommonKit -import UIKit -import SwiftUI import AVFoundation -import QuickLook +import CommonKit import FilesStorageKit +import QuickLook +import SwiftUI +import UIKit public final class FilesPickerKit: FilesPickerProtocol { private let storageKit: FilesStorageProtocol public var previewExtension: String { "jpeg" } - + public init(storageKit: FilesStorageProtocol) { self.storageKit = storageKit } - + public func getFileSize(from url: URL) throws -> Int64 { try storageKit.getFileSize(from: url).get() } - + public func getUrl(for image: UIImage?, name: String) throws -> URL { try storageKit.getTempUrl(for: image, name: name) } - + public func validateFiles(_ files: [FileResult]) throws { guard files.count <= FilesConstants.maxFilesCount else { throw FileValidationError.tooManyFiles } - + for file in files { guard file.size <= FilesConstants.maxFileSize else { throw FileValidationError.fileSizeExceedsLimit } } } - + public func resizeImage(image: UIImage, targetSize: CGSize) -> UIImage { let newSize = getPreviewSize( from: image.size, previewSize: FilesConstants.previewSize ) - + return image.imageResized(to: newSize) } - + public func getOriginalSize(for url: URL) -> CGSize? { - guard let track = AVURLAsset(url: url).tracks( - withMediaType: AVMediaType.video - ).first + guard + let track = AVURLAsset(url: url).tracks( + withMediaType: AVMediaType.video + ).first else { return nil } - + let naturalSize = track.naturalSize.applying(track.preferredTransform) - + return .init(width: abs(naturalSize.width), height: abs(naturalSize.height)) } - + public func getThumbnailImage( forUrl url: URL, originalSize: CGSize? ) async throws -> UIImage? { var thumbnailSize: CGSize? - + if let size = originalSize { thumbnailSize = getPreviewSize( from: size, previewSize: FilesConstants.previewVideoSize ) } - + let request = QLThumbnailGenerator.Request( fileAt: url, size: thumbnailSize ?? FilesConstants.previewVideoSize, scale: 1.0, representationTypes: .thumbnail ) - + let image = try await QLThumbnailGenerator.shared.generateBestRepresentation( for: request ).uiImage - + return image } - + public func getFileResult(for url: URL) throws -> FileResult { try createFileResult( from: url, @@ -93,16 +94,16 @@ public final class FilesPickerKit: FilesPickerProtocol { public func getFileResult(for image: UIImage) throws -> FileResult { let fileName = "\(imagePrefix)\(String.random(length: 4))" - + let newUrl = try storageKit.getTempUrl(for: image, name: fileName) - + return try createFileResult( from: newUrl, name: fileName, extension: previewExtension ) } - + public func getUrlConforms( to type: UTType, for itemProvider: NSItemProvider @@ -111,17 +112,17 @@ public final class FilesPickerKit: FilesPickerProtocol { guard let utType = UTType(identifier), utType.conforms(to: type) else { continue } - + do { return try await getFileURL(by: identifier, itemProvider: itemProvider) } catch { continue } } - + throw FilePickersError.cantSelectFile(itemProvider.suggestedName ?? .empty) } - + public func getUrl(for itemProvider: NSItemProvider) async throws -> URL { for type in itemProvider.registeredTypeIdentifiers { do { @@ -130,10 +131,10 @@ public final class FilesPickerKit: FilesPickerProtocol { continue } } - + throw FileValidationError.fileNotFound } - + public func getFileURL( by type: String, itemProvider: NSItemProvider @@ -154,32 +155,32 @@ public final class FilesPickerKit: FilesPickerProtocol { } } } - + public func getVideoDuration(from url: URL) -> Float64? { guard isFileType(format: .movie, atURL: url) else { return nil } - + let asset = AVAsset(url: url) - + let duration = asset.duration let durationTime = CMTimeGetSeconds(duration) - + return durationTime } - + public func getMimeType(for url: URL) -> String? { var mimeType: String? - + let pathExtension = url.pathExtension if let type = UTType(filenameExtension: pathExtension) { mimeType = type.preferredMIMEType } - + return mimeType } } -private extension FilesPickerKit { - func createFileResult( +extension FilesPickerKit { + fileprivate func createFileResult( from url: URL, name: String, extension: String @@ -189,11 +190,12 @@ private extension FilesPickerKit { let fileSize = try storageKit.getFileSize(from: newUrl).get() let duration = getVideoDuration(from: newUrl) let mimeType = getMimeType(for: newUrl) - + let fileType = detectFileType(for: newUrl) + return FileResult( assetId: url.absoluteString, url: newUrl, - type: .other, + type: fileType, preview: preview.image, previewUrl: preview.url, previewExtension: previewExtension, @@ -205,21 +207,21 @@ private extension FilesPickerKit { mimeType: mimeType ) } - - func getPreviewSize( + + fileprivate func getPreviewSize( from originalSize: CGSize?, previewSize: CGSize ) -> CGSize { guard let size = originalSize else { return FilesConstants.previewSize } - + let width = abs(size.width) let height = abs(size.height) - - let widthRatio = previewSize.width / width + + let widthRatio = previewSize.width / width let heightRatio = previewSize.height / height - + var newSize: CGSize - if(widthRatio > heightRatio) { + if widthRatio > heightRatio { newSize = CGSize( width: width * heightRatio, height: height * heightRatio @@ -230,37 +232,37 @@ private extension FilesPickerKit { height: height * widthRatio ) } - + return newSize } - - func isFileType(format: UTType, atURL fileURL: URL) -> Bool { + + fileprivate func isFileType(format: UTType, atURL fileURL: URL) -> Bool { guard let mimeType = getMimeType(for: fileURL) else { return false } - + return UTType(mimeType: mimeType)?.conforms(to: format) ?? false } - - func getPreview(for url: URL) -> (image: UIImage?, url: URL?, resolution: CGSize?) { + + fileprivate func getPreview(for url: URL) -> (image: UIImage?, url: URL?, resolution: CGSize?) { defer { url.stopAccessingSecurityScopedResource() } - + _ = url.startAccessingSecurityScopedResource() - + var image: UIImage? - + if isFileType(format: .image, atURL: url) { image = UIImage(contentsOfFile: url.path) } - + if isFileType(format: .movie, atURL: url) { image = getThumbnailImage(forUrl: url) } - + guard let image = image else { return (image: nil, url: nil, resolution: nil) } - + let resizedImage = resizeImage( image: image, targetSize: FilesConstants.previewSize @@ -269,23 +271,37 @@ private extension FilesPickerKit { for: resizedImage, name: FilesConstants.previewTag + url.lastPathComponent ) - + return (image: resizedImage, url: imageURL, resolution: image.size) } - - func getThumbnailImage(forUrl url: URL) -> UIImage? { + + fileprivate func getThumbnailImage(forUrl url: URL) -> UIImage? { let asset: AVAsset = AVAsset(url: url) let imageGenerator = AVAssetImageGenerator(asset: asset) do { let thumbnailImage = try imageGenerator.copyCGImage(at: .zero, actualTime: nil) - + let image = UIImage(cgImage: thumbnailImage) return image } catch { return nil } } + + fileprivate func detectFileType(for url: URL) -> FileType { + guard let typeIdentifier = try? url.resourceValues(forKeys: [.contentTypeKey]).contentType else { + return .other + } + + if typeIdentifier.conforms(to: .image) { + return .image + } else if typeIdentifier.conforms(to: .movie) { + return .video + } else { + return .other + } + } } private let imagePrefix = "image" diff --git a/FilesPickerKit/Sources/FilesPickerKit/Pickers/DocumentInteractionService.swift b/FilesPickerKit/Sources/FilesPickerKit/Pickers/DocumentInteractionService.swift index aab279879..cc5eee139 100644 --- a/FilesPickerKit/Sources/FilesPickerKit/Pickers/DocumentInteractionService.swift +++ b/FilesPickerKit/Sources/FilesPickerKit/Pickers/DocumentInteractionService.swift @@ -5,41 +5,41 @@ // Created by Stanislav Jelezoglo on 14.03.2024. // -import UIKit import CommonKit +import QuickLook import SwiftUI +import UIKit import WebKit -import QuickLook public final class DocumentInteractionService: NSObject { private var items: ItemsList = .default private var itemsToBeRemoved: ItemsList = .default - + public func openFile(files: [FileResult]) { let urls = files.map { file in let name = file.name ?? "Unknown" let ext = file.extenstion - + let fullName = [name, ext] .compactMap { $0 } .joined(separator: ".") - + var copyURL = URL(fileURLWithPath: file.url.deletingLastPathComponent().path) copyURL.appendPathComponent(fullName) - + if FileManager.default.fileExists(atPath: copyURL.path) { try? FileManager.default.removeItem(at: copyURL) } - + if let data = file.data { try? data.write(to: copyURL, options: [.atomic, .completeFileProtection]) } else { try? FileManager.default.copyItem(at: file.url, to: copyURL) } - + return copyURL } - + items = .init(urls: urls) } } @@ -48,45 +48,46 @@ extension DocumentInteractionService: QLPreviewControllerDelegate, QLPreviewCont public func numberOfPreviewItems(in controller: QLPreviewController) -> Int { items.urls.count } - + public func previewController( _ controller: QLPreviewController, previewItemAt index: Int ) -> QLPreviewItem { QLPreviewItemEq(url: items.urls[safe: index]) } - + public func previewController( _: QLPreviewController, transitionViewFor _: QLPreviewItem ) -> UIView? { .init() } - + public func previewControllerWillDismiss(_: QLPreviewController) { itemsToBeRemoved = items } - + public func previewControllerDidDismiss(_: QLPreviewController) { // if new items presented before dismissing the previous ones: do not delete everything // because some items could be presenting again - let urlToDelete = itemsToBeRemoved.id == items.id + let urlToDelete = + itemsToBeRemoved.id == items.id ? items.urls : Set(itemsToBeRemoved.urls).subtracting(.init(items.urls)).map { $0 } - + urlToDelete.forEach { try? FileManager.default.removeItem(at: $0) } } } -private extension DocumentInteractionService { - struct ItemsList { +extension DocumentInteractionService { + fileprivate struct ItemsList { let id: UUID = .init() let urls: [URL] - + static let `default` = Self(urls: .init()) } - - final class QLPreviewItemEq: NSObject, QLPreviewItem { + + fileprivate final class QLPreviewItemEq: NSObject, QLPreviewItem { let previewItemURL: URL? - + init(url: URL?) { previewItemURL = url } diff --git a/FilesPickerKit/Sources/FilesPickerKit/Pickers/DocumentPickerService.swift b/FilesPickerKit/Sources/FilesPickerKit/Pickers/DocumentPickerService.swift index afca35821..6e4ba6669 100644 --- a/FilesPickerKit/Sources/FilesPickerKit/Pickers/DocumentPickerService.swift +++ b/FilesPickerKit/Sources/FilesPickerKit/Pickers/DocumentPickerService.swift @@ -1,21 +1,21 @@ // // File.swift -// +// // // Created by Stanislav Jelezoglo on 21.02.2024. // -import UIKit +import AVFoundation import CommonKit import MobileCoreServices -import AVFoundation +import UIKit public final class DocumentPickerService: NSObject, FilePickerServiceProtocol { private var helper: FilesPickerProtocol public var onPreparedDataCallback: ((Result<[FileResult], Error>) -> Void)? public var onPreparingDataCallback: (() -> Void)? - + public init(helper: FilesPickerProtocol) { self.helper = helper super.init() @@ -30,7 +30,7 @@ extension DocumentPickerService: UIDocumentPickerDelegate { let files = urls.compactMap { try? helper.getFileResult(for: $0) } - + do { try helper.validateFiles(files) onPreparedDataCallback?(.success(files)) diff --git a/FilesPickerKit/Sources/FilesPickerKit/Pickers/DropInteractionService.swift b/FilesPickerKit/Sources/FilesPickerKit/Pickers/DropInteractionService.swift index b7485e10b..ecc9dddbc 100644 --- a/FilesPickerKit/Sources/FilesPickerKit/Pickers/DropInteractionService.swift +++ b/FilesPickerKit/Sources/FilesPickerKit/Pickers/DropInteractionService.swift @@ -1,12 +1,12 @@ // // DropInteractionService.swift -// +// // // Created by Stanislav Jelezoglo on 27.03.2024. // -import Foundation import CommonKit +import Foundation import UIKit import UniformTypeIdentifiers @@ -31,34 +31,34 @@ extension DropInteractionService: UIDropInteractionDelegate { ) -> Bool { true } - + public func dropInteraction( _ interaction: UIDropInteraction, sessionDidEnter session: UIDropSession ) { onSessionCallback?(true) } - + public func dropInteraction( _ interaction: UIDropInteraction, sessionDidExit session: UIDropSession ) { onSessionCallback?(false) } - + public func dropInteraction( _ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession ) -> UIDropProposal { UIDropProposal(operation: .copy) } - + public func dropInteraction( _ interaction: UIDropInteraction, performDrop session: UIDropSession ) { onPreparingDataCallback?() - + let providers = session.items.map { $0.itemProvider } Task { await process(itemProviders: providers) @@ -66,20 +66,20 @@ extension DropInteractionService: UIDropInteractionDelegate { } } -private extension DropInteractionService { - func process(itemProviders: [NSItemProvider]) async { +extension DropInteractionService { + fileprivate func process(itemProviders: [NSItemProvider]) async { var files: [FileResult] = [] - + for itemProvider in itemProviders { guard let url = try? await helper.getUrl(for: itemProvider), - let file = try? helper.getFileResult(for: url) + let file = try? helper.getFileResult(for: url) else { continue } - + files.append(file) } - + do { try helper.validateFiles(files) onPreparedDataCallback?(.success(files)) diff --git a/FilesPickerKit/Sources/FilesPickerKit/Pickers/MediaPickerService.swift b/FilesPickerKit/Sources/FilesPickerKit/Pickers/MediaPickerService.swift index 73f30ab1d..4dadbc896 100644 --- a/FilesPickerKit/Sources/FilesPickerKit/Pickers/MediaPickerService.swift +++ b/FilesPickerKit/Sources/FilesPickerKit/Pickers/MediaPickerService.swift @@ -1,23 +1,23 @@ // // File.swift -// +// // // Created by Stanislav Jelezoglo on 11.02.2024. // import CommonKit -import UIKit import Photos import PhotosUI +import UIKit @MainActor public final class MediaPickerService: NSObject, FilePickerServiceProtocol { private var helper: FilesPickerProtocol - + public var onPreparedDataCallback: ((Result<[FileResult], Error>) -> Void)? public var onPreparingDataCallback: (() -> Void)? public var preSelectedFiles: [FileResult] = [] - + public init(helper: FilesPickerProtocol) { self.helper = helper super.init() @@ -29,21 +29,24 @@ extension MediaPickerService: PHPickerViewControllerDelegate { _ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult] ) { - picker.dismiss(animated: true, completion: { [weak self] in - self?.onPreparingDataCallback?() - - Task { - await self?.processResults(results) + picker.dismiss( + animated: true, + completion: { [weak self] in + self?.onPreparingDataCallback?() + + Task { + await self?.processResults(results) + } } - }) + ) } } -private extension MediaPickerService { - func processResults(_ results: [PHPickerResult]) async { +extension MediaPickerService { + fileprivate func processResults(_ results: [PHPickerResult]) async { do { var dataArray: [FileResult] = [] - + for result in results { let itemProvider = result.itemProvider if isConforms(to: .image, itemProvider.registeredTypeIdentifiers) { @@ -51,19 +54,19 @@ private extension MediaPickerService { to: .image, for: itemProvider ) - + let preview = try getPhoto( from: url, name: itemProvider.suggestedName ?? .empty ) - + let fileSize = try helper.getFileSize(from: url) - + let resizedPreview = helper.resizeImage( image: preview, targetSize: FilesConstants.previewSize ) - + let previewUrl = try? helper.getUrl( for: resizedPreview, name: FilesConstants.previewTag + url.lastPathComponent @@ -76,7 +79,7 @@ private extension MediaPickerService { url: url, type: .image, preview: resizedPreview, - previewUrl: previewUrl, + previewUrl: previewUrl, previewExtension: helper.previewExtension, size: fileSize, name: itemProvider.suggestedName, @@ -90,22 +93,22 @@ private extension MediaPickerService { to: .movie, for: itemProvider ) - + let fileSize = try helper.getFileSize(from: url) let originalSize = helper.getOriginalSize(for: url) let duration = helper.getVideoDuration(from: url) let mimeType = helper.getMimeType(for: url) - + let thumbnailImage = try? await helper.getThumbnailImage( forUrl: url, originalSize: originalSize ) - + let previewUrl = try? helper.getUrl( for: thumbnailImage, name: FilesConstants.previewTag + url.lastPathComponent ) - + dataArray.append( .init( assetId: result.assetIdentifier, @@ -132,35 +135,35 @@ private extension MediaPickerService { } } } - + try helper.validateFiles(dataArray) onPreparedDataCallback?(.success(dataArray)) } catch { onPreparedDataCallback?(.failure(error)) } - + preSelectedFiles.removeAll() } - - func getPhoto(from url: URL, name: String) throws -> UIImage { + + fileprivate func getPhoto(from url: URL, name: String) throws -> UIImage { guard let image = UIImage(contentsOfFile: url.path) else { throw FilePickersError.cantSelectFile(name) } - + return image } - - func isConforms(to type: UTType, _ registeredTypeIdentifiers: [String]) -> Bool { + + fileprivate func isConforms(to type: UTType, _ registeredTypeIdentifiers: [String]) -> Bool { for identifier in registeredTypeIdentifiers { guard !identifier.contains("private") else { continue } - + if let uiType = UTType(identifier), uiType.conforms(to: type) { return true } } - + return false } } diff --git a/FilesPickerKit/Sources/FilesPickerKit/Protocols/FilePickerServiceProtocol.swift b/FilesPickerKit/Sources/FilesPickerKit/Protocols/FilePickerServiceProtocol.swift index 5c59b2b7e..830cffa24 100644 --- a/FilesPickerKit/Sources/FilesPickerKit/Protocols/FilePickerServiceProtocol.swift +++ b/FilesPickerKit/Sources/FilesPickerKit/Protocols/FilePickerServiceProtocol.swift @@ -1,12 +1,12 @@ // // File.swift -// +// // // Created by Stanislav Jelezoglo on 11.02.2024. // -import UIKit import CommonKit +import UIKit @MainActor protocol FilePickerServiceProtocol { diff --git a/FilesPickerKit/Sources/FilesPickerKit/Protocols/FilesPickerProtocol.swift b/FilesPickerKit/Sources/FilesPickerKit/Protocols/FilesPickerProtocol.swift index c44e68118..6b9af7a67 100644 --- a/FilesPickerKit/Sources/FilesPickerKit/Protocols/FilesPickerProtocol.swift +++ b/FilesPickerKit/Sources/FilesPickerKit/Protocols/FilesPickerProtocol.swift @@ -5,16 +5,16 @@ // Created by Stanislav Jelezoglo on 20.05.2024. // -import UIKit import CommonKit import QuickLook +import UIKit @MainActor public protocol FilesPickerProtocol { var previewExtension: String { get } - + func getFileSize(from url: URL) throws -> Int64 func getUrl(for image: UIImage?, name: String) throws -> URL func validateFiles(_ files: [FileResult]) throws @@ -25,20 +25,20 @@ public protocol FilesPickerProtocol { originalSize: CGSize? ) async throws -> UIImage? func getFileResult(for url: URL) throws -> FileResult - + func getUrlConforms( to type: UTType, for itemProvider: NSItemProvider ) async throws -> URL - + func getUrl(for itemProvider: NSItemProvider) async throws -> URL - + func getFileURL( by type: String, itemProvider: NSItemProvider ) async throws -> URL - + func getFileResult(for image: UIImage) throws -> FileResult func getVideoDuration(from url: URL) -> Float64? - func getMimeType(for url: URL) -> String? + func getMimeType(for url: URL) -> String? } diff --git a/FilesPickerKit/Tests/FilesPickerKitTests/FilesPickerKitTests.swift b/FilesPickerKit/Tests/FilesPickerKitTests/FilesPickerKitTests.swift index 586837470..c57b47bdc 100644 --- a/FilesPickerKit/Tests/FilesPickerKitTests/FilesPickerKitTests.swift +++ b/FilesPickerKit/Tests/FilesPickerKitTests/FilesPickerKitTests.swift @@ -1,4 +1,5 @@ import XCTest + @testable import FilesPickerKit final class FilesPickerKitTests: XCTestCase { diff --git a/FilesStorageKit/Package.swift b/FilesStorageKit/Package.swift index 588daaeb2..f0e36d970 100644 --- a/FilesStorageKit/Package.swift +++ b/FilesStorageKit/Package.swift @@ -12,7 +12,8 @@ let package = Package( // Products define the executables and libraries a package produces, making them visible to other packages. .library( name: "FilesStorageKit", - targets: ["FilesStorageKit"]), + targets: ["FilesStorageKit"] + ) ], dependencies: [ .package(path: "../CommonKit") @@ -22,9 +23,11 @@ let package = Package( // Targets can depend on other targets in this package and products from dependencies. .target( name: "FilesStorageKit", - dependencies: ["CommonKit"]), + dependencies: ["CommonKit"] + ), .testTarget( name: "FilesStorageKitTests", - dependencies: ["FilesStorageKit"]), + dependencies: ["FilesStorageKit"] + ) ] ) diff --git a/FilesStorageKit/Sources/FilesStorageKit/FilesStorageKit.swift b/FilesStorageKit/Sources/FilesStorageKit/FilesStorageKit.swift index bfd90b3be..2bc92604a 100644 --- a/FilesStorageKit/Sources/FilesStorageKit/FilesStorageKit.swift +++ b/FilesStorageKit/Sources/FilesStorageKit/FilesStorageKit.swift @@ -1,9 +1,9 @@ // The Swift Programming Language // https://docs.swift.org/swift-book +import Combine import CommonKit import UIKit -import Combine public typealias FileStorageServiceResult = Result @@ -15,60 +15,60 @@ public final class FilesStorageKit: FilesStorageProtocol, @unchecked Sendable { public let fileType: FileType public let isPreview: Bool } - + @Atomic private var cachedFiles: [String: File] = [:] private let cachedImages: NSCache = NSCache() private let maxCachedFilesToLoad = 100 private let encryptedFileExtension = "encFhj" private let previewFileExtension = "prvAIFE" - + public init() { try? loadCache() } - + public func getPreview(for id: String) -> UIImage? { guard !id.isEmpty else { return nil } - + if let image = cachedImages.object(forKey: id as NSString) { return image } - + return nil } - + public func cacheImageToMemoryIfNeeded(id: String, data: Data) -> UIImage? { guard let image = UIImage(data: data), - cachedImages.object(forKey: id as NSString) == nil + cachedImages.object(forKey: id as NSString) == nil else { return nil } - + cachedImages.setObject(image, forKey: id as NSString) return image } - + public func isCachedInMemory(_ id: String) -> Bool { guard !id.isEmpty else { return false } - + return cachedImages.object(forKey: id as NSString) != nil } - + public func isCachedLocally(_ id: String) -> Bool { cachedFiles[id] != nil } - + public func getFile(with id: String) -> FileStorageServiceResult { guard let file = cachedFiles[id] else { return .failure(.fileNotFound) } - + return .success(file) } - + public func getFileURL(with id: String) -> FileStorageServiceResult { getFile(with: id).flatMap { .success($0.url) } } - + public func cacheFile( id: String, fileExtension: String, @@ -92,15 +92,15 @@ public final class FilesStorageKit: FilesStorageProtocol, @unchecked Sendable { fileType: fileType, isPreview: isPreview ) - + guard fileType == .image, isPreview else { return } cacheFileToMemory(data: decodedData, id: id) - + if let url = url { cacheFileToMemory(data: decodedData, id: url.absoluteString) } } - + public func cacheTemporaryFile( url: URL, isEncrypted: Bool, @@ -114,19 +114,20 @@ public final class FilesStorageKit: FilesStorageProtocol, @unchecked Sendable { isPreview: isPreview ) } - + public func getCacheSize() -> FileStorageServiceResult { - guard let url = try? FileManager.default.url( - for: .cachesDirectory, - in: .userDomainMask, - appropriateFor: nil, - create: true - ).appendingPathComponent(cachePath) + guard + let url = try? FileManager.default.url( + for: .cachesDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true + ).appendingPathComponent(cachePath) else { return .failure(.fileNotFound) } - + return folderSize(at: url) } - + public func clearCache() throws { let cacheUrl = try FileManager.default.url( for: .cachesDirectory, @@ -134,29 +135,31 @@ public final class FilesStorageKit: FilesStorageProtocol, @unchecked Sendable { appropriateFor: nil, create: true ).appendingPathComponent(cachePath) - + if FileManager.default.fileExists( atPath: cacheUrl.path ) { try FileManager.default.removeItem(at: cacheUrl) } - + try clearTempCache() - + cachedImages.removeAllObjects() cachedFiles.removeAll() } - + public func removeTempFiles(at urls: [URL]) { urls.forEach { url in - guard FileManager.default.fileExists( - atPath: url.path - ) else { return } - + guard + FileManager.default.fileExists( + atPath: url.path + ) + else { return } + try? FileManager.default.removeItem(at: url) } } - + public func clearTempCache() throws { let tempCacheUrl = try FileManager.default.url( for: .cachesDirectory, @@ -164,19 +167,21 @@ public final class FilesStorageKit: FilesStorageProtocol, @unchecked Sendable { appropriateFor: nil, create: true ).appendingPathComponent(tempCachePath) - - guard FileManager.default.fileExists( - atPath: tempCacheUrl.path - ) else { return } - + + guard + FileManager.default.fileExists( + atPath: tempCacheUrl.path + ) + else { return } + try FileManager.default.removeItem(at: tempCacheUrl) } - + public func getTempUrl(for image: UIImage?, name: String) throws -> URL { guard let data = image?.jpegData(compressionQuality: FilesConstants.previewCompressQuality) else { throw FileValidationError.fileNotFound } - + let folder = try FileManager.default.url( for: .cachesDirectory, in: .userDomainMask, @@ -189,55 +194,55 @@ public final class FilesStorageKit: FilesStorageProtocol, @unchecked Sendable { let fileURL = folder.appendingPathComponent(name) try data.write(to: fileURL, options: [.atomic, .completeFileProtection]) - + return fileURL } - + public func copyFileToTempCache(from url: URL) throws -> URL { defer { url.stopAccessingSecurityScopedResource() } - + _ = url.startAccessingSecurityScopedResource() - + let folder = try FileManager.default.url( for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true ).appendingPathComponent(tempCachePath) - + try FileManager.default.createDirectory( at: folder, withIntermediateDirectories: true ) - + let targetURL = folder.appendingPathComponent(String.random(length: 6) + url.lastPathComponent) - + guard targetURL != url else { return url } - + if FileManager.default.fileExists(atPath: targetURL.path) { try FileManager.default.removeItem(at: targetURL) } - + try FileManager.default.copyItem(at: url, to: targetURL) - + return targetURL } - + public func getFileSize(from fileURL: URL) -> FileStorageServiceResult { defer { fileURL.stopAccessingSecurityScopedResource() } - + _ = fileURL.startAccessingSecurityScopedResource() do { let fileAttributes = try FileManager.default.attributesOfItem(atPath: fileURL.path) - + guard let fileSize = fileAttributes[.size] as? Int64 else { throw FileValidationError.fileNotFound } - + return .success(fileSize) } catch { return .failure(.unknownError(error)) @@ -245,8 +250,8 @@ public final class FilesStorageKit: FilesStorageProtocol, @unchecked Sendable { } } -private extension FilesStorageKit { - func loadCache() throws { +extension FilesStorageKit { + fileprivate func loadCache() throws { let folder = try FileManager.default.url( for: .cachesDirectory, in: .userDomainMask, @@ -255,14 +260,14 @@ private extension FilesStorageKit { ).appendingPathComponent(cachePath) let files = getAllFiles(in: folder) - + var previewFiles: [File] = [] - + files.forEach { url in let result = fileNameAndExtension(from: url) let isEncrypted = result.extensions.contains(encryptedFileExtension) let isPreview = result.extensions.contains(previewFileExtension) - + let file = File( id: result.name, isEncrypted: isEncrypted, @@ -271,12 +276,12 @@ private extension FilesStorageKit { isPreview: isPreview ) cachedFiles[result.name] = file - + if isPreview, !isEncrypted { previewFiles.append(file) } } - + previewFiles.prefix(maxCachedFilesToLoad).forEach { file in if let data = UIImage(contentsOfFile: file.url.path) { cachedImages.setObject(data, forKey: file.id as NSString) @@ -284,30 +289,30 @@ private extension FilesStorageKit { } } - func getAllFiles(in directoryURL: URL) -> [URL] { + fileprivate func getAllFiles(in directoryURL: URL) -> [URL] { var fileURLs: [URL] = [] - + let fileManager = FileManager.default let enumerator = fileManager.enumerator(at: directoryURL, includingPropertiesForKeys: nil) - + while let fileURL = enumerator?.nextObject() as? URL { var isDirectory: ObjCBool = false let fileExist = fileManager.fileExists( atPath: fileURL.path, isDirectory: &isDirectory ) - + if fileExist && !isDirectory.boolValue { fileURLs.append(fileURL) } else if fileExist && isDirectory.boolValue { fileURLs.append(contentsOf: getAllFiles(in: fileURL)) } } - + return Array(Set(fileURLs)) } - - func cacheTemporaryFile( + + fileprivate func cacheTemporaryFile( with url: URL, isEncrypted: Bool, fileType: FileType, @@ -321,21 +326,22 @@ private extension FilesStorageKit { isPreview: isPreview ) $cachedFiles.mutate { $0[file.id] = file } - + if fileType == .image, - isPreview, - let uiImage = UIImage(contentsOfFile: url.path) { + isPreview, + let uiImage = UIImage(contentsOfFile: url.path) + { cachedImages.setObject(uiImage, forKey: file.id as NSString) } } - - func cacheFileToMemory(data: Data, id: String) { + + fileprivate func cacheFileToMemory(data: Data, id: String) { guard let uiImage = UIImage(data: data) else { return } - + cachedImages.setObject(uiImage, forKey: id as NSString) } - - func saveFileLocally( + + fileprivate func saveFileLocally( with id: String, fileExtension: String, data: Data? = nil, @@ -355,18 +361,21 @@ private extension FilesStorageKit { try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true) - let mainExtension = !fileExtension.isEmpty - ? ".\(fileExtension)" - : .empty - - let additionalExtension = isPreview - ? ".\(previewFileExtension)" - : .empty - - let fileName = isEncrypted - ? "\(id)\(mainExtension)\(additionalExtension).\(encryptedFileExtension)" - : "\(id)\(mainExtension)\(additionalExtension)" - + let mainExtension = + !fileExtension.isEmpty + ? ".\(fileExtension)" + : .empty + + let additionalExtension = + isPreview + ? ".\(previewFileExtension)" + : .empty + + let fileName = + isEncrypted + ? "\(id)\(mainExtension)\(additionalExtension).\(encryptedFileExtension)" + : "\(id)\(mainExtension)\(additionalExtension)" + let fileURL = folder.appendingPathComponent(fileName) let file = File( id: id, @@ -375,54 +384,60 @@ private extension FilesStorageKit { fileType: fileType, isPreview: isPreview ) - + if let data = data { try data.write(to: fileURL, options: [.atomic, .completeFileProtection]) } - + if let url = localUrl { try FileManager.default.removeItem(at: url) $cachedFiles.mutate { $0[url.absoluteString] = file } } - + $cachedFiles.mutate { $0[id] = file } } - - func folderSize(at url: URL) -> FileStorageServiceResult { + + fileprivate func folderSize(at url: URL) -> FileStorageServiceResult { let fileManager = FileManager.default - + guard fileManager.fileExists(atPath: url.path) else { return .failure(.fileNotFound) } - - guard let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: [.totalFileAllocatedSizeKey], options: [.skipsHiddenFiles, .skipsPackageDescendants]) else { + + guard + let enumerator = fileManager.enumerator( + at: url, + includingPropertiesForKeys: [.totalFileAllocatedSizeKey], + options: [.skipsHiddenFiles, .skipsPackageDescendants] + ) + else { return .failure(.fileNotFound) } - + var folderSize: Int64 = 0 - + for case let fileURL as URL in enumerator { do { let attributes = try fileManager.attributesOfItem(atPath: fileURL.path) if let fileSize = attributes[.size] as? Int64 { folderSize += fileSize } - } catch { } + } catch {} } - + return .success(folderSize) } - - func fileNameAndExtension(from url: URL) -> (name: String, extensions: [String]) { + + fileprivate func fileNameAndExtension(from url: URL) -> (name: String, extensions: [String]) { let filename = url.lastPathComponent let nameComponents = filename.components(separatedBy: ".") - + guard nameComponents.count > 1, - let name = nameComponents.first + let name = nameComponents.first else { return (filename.replacingOccurrences(of: ".", with: ""), []) } - + return (name, Array(nameComponents.dropFirst())) } } diff --git a/FilesStorageKit/Sources/FilesStorageKit/Protocols/FilesStorageProtocol.swift b/FilesStorageKit/Sources/FilesStorageKit/Protocols/FilesStorageProtocol.swift index 3da36b699..d295bdcce 100644 --- a/FilesStorageKit/Sources/FilesStorageKit/Protocols/FilesStorageProtocol.swift +++ b/FilesStorageKit/Sources/FilesStorageKit/Protocols/FilesStorageProtocol.swift @@ -5,29 +5,29 @@ // Created by Stanislav Jelezoglo on 21.05.2024. // -import UIKit import CommonKit +import UIKit public protocol FilesStorageProtocol: Sendable { func cacheImageToMemoryIfNeeded(id: String, data: Data) -> UIImage? - + func getPreview(for id: String) -> UIImage? - + func isCachedLocally(_ id: String) -> Bool - + func isCachedInMemory(_ id: String) -> Bool - + func getFileURL(with id: String) -> FileStorageServiceResult - + func getFile(with id: String) -> FileStorageServiceResult - + func cacheTemporaryFile( url: URL, isEncrypted: Bool, fileType: FileType, isPreview: Bool ) - + func cacheFile( id: String, fileExtension: String, @@ -40,18 +40,18 @@ public protocol FilesStorageProtocol: Sendable { fileType: FileType, isPreview: Bool ) throws - + func getCacheSize() -> FileStorageServiceResult - + func clearCache() throws - + func clearTempCache() throws - + func removeTempFiles(at urls: [URL]) - + func getTempUrl(for image: UIImage?, name: String) throws -> URL - + func copyFileToTempCache(from url: URL) throws -> URL - + func getFileSize(from fileURL: URL) -> FileStorageServiceResult } diff --git a/FilesStorageKit/Tests/FilesStorageKitTests/FilesStorageKitTests.swift b/FilesStorageKit/Tests/FilesStorageKitTests/FilesStorageKitTests.swift index 988a30f98..de5c63256 100644 --- a/FilesStorageKit/Tests/FilesStorageKitTests/FilesStorageKitTests.swift +++ b/FilesStorageKit/Tests/FilesStorageKitTests/FilesStorageKitTests.swift @@ -1,4 +1,5 @@ import XCTest + @testable import FilesStorageKit final class FilesStorageKitTests: XCTestCase { diff --git a/LiskKit/Package.swift b/LiskKit/Package.swift index ea99ca695..8da6ff433 100644 --- a/LiskKit/Package.swift +++ b/LiskKit/Package.swift @@ -12,11 +12,12 @@ let package = Package( products: [ .library( name: "LiskKit", - targets: ["LiskKit"]) + targets: ["LiskKit"] + ) ], dependencies: [ .package(name: "Sodium", url: "https://github.com/jedisct1/swift-sodium.git", from: "0.9.1"), - .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", from: "1.3.0"), + .package(url: "https://github.com/Adamant-im/CryptoSwift.git", from: "1.3.0"), .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.6.0") ], targets: [ @@ -34,6 +35,6 @@ let package = Package( dependencies: ["LiskKit"], path: "Tests" ) - + ] ) diff --git a/LiskKit/Sources/API/Accounts/Accounts.swift b/LiskKit/Sources/API/Accounts/Accounts.swift index dda7a243b..f395af903 100644 --- a/LiskKit/Sources/API/Accounts/Accounts.swift +++ b/LiskKit/Sources/API/Accounts/Accounts.swift @@ -24,7 +24,16 @@ public struct Accounts: APIService { extension Accounts { /// Retrieve accounts - public func legacyAccounts(address: String? = nil, publicKey: String? = nil, secondPublicKey: String? = nil, username: String? = nil, limit: Int? = nil, offset: Int? = nil, sort: APIRequest.Sort? = nil, completionHandler: @escaping (Response) -> Void) { + public func legacyAccounts( + address: String? = nil, + publicKey: String? = nil, + secondPublicKey: String? = nil, + username: String? = nil, + limit: Int? = nil, + offset: Int? = nil, + sort: APIRequest.Sort? = nil, + completionHandler: @escaping (Response) -> Void + ) { var options: RequestOptions = [:] if let value = address { options["address"] = value } if let value = publicKey { options["publicKey"] = value } @@ -42,7 +51,7 @@ extension Accounts { public func accounts(address: String, completionHandler: @escaping (Response) -> Void) { client.get(path: "accounts/\(address)", options: nil, completionHandler: completionHandler) } - + public func balance(address: String) async throws -> Balance? { let balances: BalancesResponse = try await client.request( method: "token_getBalances", @@ -50,7 +59,7 @@ extension Accounts { ) return balances.balances.first(where: { $0.tokenID == Constants.tokenID }) } - + public func nonce(address: String) async throws -> String { let data: AuthAccount = try await client.request( method: "auth_getAuthAccount", @@ -58,14 +67,14 @@ extension Accounts { ) return data.nonce } - + public func lastBlock() async throws -> Block { try await client.request( method: "chain_getLastBlock", params: [:] ) } - + public func getFees() async throws -> ServiceFeeModel { try await client.request(method: "fee_getMinFeePerByte", params: [:]) } diff --git a/LiskKit/Sources/API/Accounts/Balance.swift b/LiskKit/Sources/API/Accounts/Balance.swift index e067db881..5f04b9593 100644 --- a/LiskKit/Sources/API/Accounts/Balance.swift +++ b/LiskKit/Sources/API/Accounts/Balance.swift @@ -1,6 +1,6 @@ // // File.swift -// +// // // Created by Stanislav Jelezoglo on 18.12.2023. // diff --git a/LiskKit/Sources/API/Accounts/Block.swift b/LiskKit/Sources/API/Accounts/Block.swift index a0505c901..c9fb6992d 100644 --- a/LiskKit/Sources/API/Accounts/Block.swift +++ b/LiskKit/Sources/API/Accounts/Block.swift @@ -1,6 +1,6 @@ // // File.swift -// +// // // Created by Stanislav Jelezoglo on 18.12.2023. // diff --git a/LiskKit/Sources/API/Accounts/Models/AccountModel.swift b/LiskKit/Sources/API/Accounts/Models/AccountModel.swift index c67b6e100..7433aaf0d 100644 --- a/LiskKit/Sources/API/Accounts/Models/AccountModel.swift +++ b/LiskKit/Sources/API/Accounts/Models/AccountModel.swift @@ -32,12 +32,12 @@ extension Accounts { public var hashValue: Int { return address.hashValue } - + public func hash(into hasher: inout Hasher) { hasher.combine(address) } } - + public struct AccountModel: APIModel { private struct Sequence: APIModel { diff --git a/LiskKit/Sources/API/Accounts/Responses/AccountsResponse.swift b/LiskKit/Sources/API/Accounts/Responses/AccountsResponse.swift index 3010eb506..f9c62c284 100644 --- a/LiskKit/Sources/API/Accounts/Responses/AccountsResponse.swift +++ b/LiskKit/Sources/API/Accounts/Responses/AccountsResponse.swift @@ -13,7 +13,7 @@ extension Accounts { public let data: [LegacyAccountModel] } - + public struct AccountsResponse: APIResponse { public let data: AccountModel diff --git a/LiskKit/Sources/API/Blocks/Blocks.swift b/LiskKit/Sources/API/Blocks/Blocks.swift index e245d1fe3..62fb80907 100644 --- a/LiskKit/Sources/API/Blocks/Blocks.swift +++ b/LiskKit/Sources/API/Blocks/Blocks.swift @@ -23,7 +23,15 @@ public struct Blocks: APIService { extension Blocks { /// List blocks - public func blocks(id: String? = nil, height: Int? = nil, generatorPublicKey: String? = nil, limit: Int? = nil, offset: Int? = nil, sort: APIRequest.Sort? = nil, completionHandler: @escaping (Response) -> Void) { + public func blocks( + id: String? = nil, + height: Int? = nil, + generatorPublicKey: String? = nil, + limit: Int? = nil, + offset: Int? = nil, + sort: APIRequest.Sort? = nil, + completionHandler: @escaping (Response) -> Void + ) { var options: RequestOptions = [:] if let value = id { options["blockId"] = value } if let value = height { options["height"] = value } diff --git a/LiskKit/Sources/API/Blocks/Models/BlockModel.swift b/LiskKit/Sources/API/Blocks/Models/BlockModel.swift index 3669999dc..e4ecb7ef9 100644 --- a/LiskKit/Sources/API/Blocks/Models/BlockModel.swift +++ b/LiskKit/Sources/API/Blocks/Models/BlockModel.swift @@ -52,7 +52,7 @@ extension Blocks { public var hashValue: Int { return id.hashValue } - + public func hash(into hasher: inout Hasher) { hasher.combine(id) } diff --git a/LiskKit/Sources/API/Dapps/Dapps.swift b/LiskKit/Sources/API/Dapps/Dapps.swift index 2717e7b75..8eea23c69 100644 --- a/LiskKit/Sources/API/Dapps/Dapps.swift +++ b/LiskKit/Sources/API/Dapps/Dapps.swift @@ -23,7 +23,14 @@ public struct Dapps: APIService { extension Dapps { /// List Dapps - public func blocks(transactionId: String? = nil, name: Int? = nil, limit: Int? = nil, offset: Int? = nil, sort: APIRequest.Sort? = nil, completionHandler: @escaping (Response) -> Void) { + public func blocks( + transactionId: String? = nil, + name: Int? = nil, + limit: Int? = nil, + offset: Int? = nil, + sort: APIRequest.Sort? = nil, + completionHandler: @escaping (Response) -> Void + ) { var options: RequestOptions = [:] if let value = transactionId { options["transactionId"] = value } if let value = name { options["name"] = value } diff --git a/LiskKit/Sources/API/Dapps/Models/DappModel.swift b/LiskKit/Sources/API/Dapps/Models/DappModel.swift index 2972a4660..db167cd2c 100644 --- a/LiskKit/Sources/API/Dapps/Models/DappModel.swift +++ b/LiskKit/Sources/API/Dapps/Models/DappModel.swift @@ -36,7 +36,7 @@ extension Dapps { public var hashValue: Int { return name.hashValue } - + public func hash(into hasher: inout Hasher) { hasher.combine(name) } diff --git a/LiskKit/Sources/API/Delegates/Delegates.swift b/LiskKit/Sources/API/Delegates/Delegates.swift index 51bd96edd..72cbd0cb9 100644 --- a/LiskKit/Sources/API/Delegates/Delegates.swift +++ b/LiskKit/Sources/API/Delegates/Delegates.swift @@ -23,7 +23,17 @@ public struct Delegates: APIService { extension Delegates { /// List delegate objects - public func delegates(address: String? = nil, publicKey: String? = nil, secondPublicKey: String? = nil, username: String? = nil, search: String? = nil, limit: UInt? = nil, offset: UInt? = nil, sort: APIRequest.Sort? = nil, completionHandler: @escaping (Response) -> Void) { + public func delegates( + address: String? = nil, + publicKey: String? = nil, + secondPublicKey: String? = nil, + username: String? = nil, + search: String? = nil, + limit: UInt? = nil, + offset: UInt? = nil, + sort: APIRequest.Sort? = nil, + completionHandler: @escaping (Response) -> Void + ) { var options: RequestOptions = [:] if let value = address { options["address"] = value } if let value = publicKey { options["publicKey"] = value } diff --git a/LiskKit/Sources/API/Delegates/Models/DelegateModel.swift b/LiskKit/Sources/API/Delegates/Models/DelegateModel.swift index 6f5497c31..17a867695 100644 --- a/LiskKit/Sources/API/Delegates/Models/DelegateModel.swift +++ b/LiskKit/Sources/API/Delegates/Models/DelegateModel.swift @@ -38,7 +38,7 @@ extension Delegates { public var hashValue: Int { return username.hashValue } - + public func hash(into hasher: inout Hasher) { hasher.combine(username) } diff --git a/LiskKit/Sources/API/Node/Models/NodeStatusModel.swift b/LiskKit/Sources/API/Node/Models/NodeStatusModel.swift index e9bfdc8cd..a88a8af2d 100644 --- a/LiskKit/Sources/API/Node/Models/NodeStatusModel.swift +++ b/LiskKit/Sources/API/Node/Models/NodeStatusModel.swift @@ -38,14 +38,14 @@ extension Node { public let total: Int } - + public struct NodeInfoModel: APIModel { public let version: String public let networkVersion: String - + public let height: Int? } - + } diff --git a/LiskKit/Sources/API/Node/Responses/NodeStatusResponse.swift b/LiskKit/Sources/API/Node/Responses/NodeStatusResponse.swift index df4deeec8..f0fbdb939 100644 --- a/LiskKit/Sources/API/Node/Responses/NodeStatusResponse.swift +++ b/LiskKit/Sources/API/Node/Responses/NodeStatusResponse.swift @@ -13,7 +13,7 @@ extension Node { public let data: NodeStatusModel } - + public struct NodeInfoResponse: APIResponse { public let data: NodeInfoModel diff --git a/LiskKit/Sources/API/Peers/Models/PeerModel.swift b/LiskKit/Sources/API/Peers/Models/PeerModel.swift index 2f0d2b0b0..1cfe97d42 100644 --- a/LiskKit/Sources/API/Peers/Models/PeerModel.swift +++ b/LiskKit/Sources/API/Peers/Models/PeerModel.swift @@ -44,7 +44,7 @@ extension Peers { public var hashValue: Int { return ip.hashValue } - + public func hash(into hasher: inout Hasher) { hasher.combine(ip) } diff --git a/LiskKit/Sources/API/Peers/Peers.swift b/LiskKit/Sources/API/Peers/Peers.swift index 88c4cb670..489773114 100644 --- a/LiskKit/Sources/API/Peers/Peers.swift +++ b/LiskKit/Sources/API/Peers/Peers.swift @@ -23,7 +23,17 @@ public struct Peers: APIService { extension Peers { /// List peers - public func peers(ip: String? = nil, state: PeerModel.State? = nil, version: String? = nil, os: String? = nil, height: Int? = nil, limit: UInt? = nil, offset: UInt? = nil, sort: APIRequest.Sort? = nil, completionHandler: @escaping (Response) -> Void) { + public func peers( + ip: String? = nil, + state: PeerModel.State? = nil, + version: String? = nil, + os: String? = nil, + height: Int? = nil, + limit: UInt? = nil, + offset: UInt? = nil, + sort: APIRequest.Sort? = nil, + completionHandler: @escaping (Response) -> Void + ) { var options: RequestOptions = [:] if let value = ip { options["ip"] = value } if let value = state?.rawValue { options["state"] = value } diff --git a/LiskKit/Sources/API/Service/Models/ServiceFeeModel.swift b/LiskKit/Sources/API/Service/Models/ServiceFeeModel.swift index 77852369a..f4202534c 100644 --- a/LiskKit/Sources/API/Service/Models/ServiceFeeModel.swift +++ b/LiskKit/Sources/API/Service/Models/ServiceFeeModel.swift @@ -1,6 +1,6 @@ // // ServiceFeeModel.swift -// +// // // Created by Anton Boyarkin on 20.08.2021. // @@ -16,7 +16,7 @@ public struct ServiceFeeModel: APIModel { public static func == (lhs: ServiceFeeModel, rhs: ServiceFeeModel) -> Bool { return lhs.minFeePerByte == rhs.minFeePerByte } - + public func hash(into hasher: inout Hasher) { hasher.combine(minFeePerByte) } diff --git a/LiskKit/Sources/API/Service/Models/ServiceInfoModel.swift b/LiskKit/Sources/API/Service/Models/ServiceInfoModel.swift index e41c9245a..defa95b55 100644 --- a/LiskKit/Sources/API/Service/Models/ServiceInfoModel.swift +++ b/LiskKit/Sources/API/Service/Models/ServiceInfoModel.swift @@ -1,6 +1,6 @@ // // File.swift -// +// // // Created by Stanislav Jelezoglo on 26.12.2023. // diff --git a/LiskKit/Sources/API/Service/Models/ServiceMetaFeeModel.swift b/LiskKit/Sources/API/Service/Models/ServiceMetaFeeModel.swift index 0f01f442a..fedceb2a7 100644 --- a/LiskKit/Sources/API/Service/Models/ServiceMetaFeeModel.swift +++ b/LiskKit/Sources/API/Service/Models/ServiceMetaFeeModel.swift @@ -1,6 +1,6 @@ // // File.swift -// +// // // Created by Stanislav Jelezoglo on 14.03.2022. // @@ -16,7 +16,7 @@ public struct ServiceMetaFeeModel: APIModel { public static func == (lhs: ServiceMetaFeeModel, rhs: ServiceMetaFeeModel) -> Bool { return lhs.lastBlockHeight == rhs.lastBlockHeight } - + public func hash(into hasher: inout Hasher) { hasher.combine(lastBlockHeight) } diff --git a/LiskKit/Sources/API/Service/Models/ServiceTransactionModel.swift b/LiskKit/Sources/API/Service/Models/ServiceTransactionModel.swift index a06b98862..4c81f4f7f 100644 --- a/LiskKit/Sources/API/Service/Models/ServiceTransactionModel.swift +++ b/LiskKit/Sources/API/Service/Models/ServiceTransactionModel.swift @@ -1,6 +1,6 @@ // // ServiceTransactionModel.swift -// +// // // Created by Anton Boyarkin on 15.08.2021. // @@ -12,11 +12,12 @@ public enum ExecutionStatus: String, Decodable { case successful case failed case unknown - + public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if let rawValue = try? container.decode(String.self), - let status = ExecutionStatus(rawValue: rawValue) { + let status = ExecutionStatus(rawValue: rawValue) + { self = status } else { self = .unknown @@ -25,62 +26,62 @@ public enum ExecutionStatus: String, Decodable { } public struct ServiceTransactionModel: APIModel { - + public var blockId: String? { return block?.id } - + public var type: UInt8 { return 0 } - + public var timestamp: UInt32? { return block?.timestamp } - + public var senderPublicKey: String { return sender.publicKey } - + public var senderId: String { return sender.address } - + public var recipientId: String { return params.recipientAddress } - + public var recipientPublicKey: String? { return nil } - + public var amount: String { return params.amount } - + public var signature: String { return "" } - + public var confirmations: UInt64 { return 0 } - + public var height: UInt64? { block?.height } - + public struct Block: APIModel { public let id: String public let height: UInt64 public let timestamp: UInt32 } - + public struct Sender: APIModel { public let address: String public let publicKey: String } - + public struct Params: APIModel { public let amount: String public let recipientAddress: String @@ -94,7 +95,7 @@ public struct ServiceTransactionModel: APIModel { public let params: Params public let executionStatus: ExecutionStatus public let nonce: String - + // MARK: - Hashable public static func == (lhs: ServiceTransactionModel, rhs: ServiceTransactionModel) -> Bool { @@ -104,7 +105,7 @@ public struct ServiceTransactionModel: APIModel { public var hashValue: Int { return id.hashValue } - + public func hash(into hasher: inout Hasher) { hasher.combine(id) } diff --git a/LiskKit/Sources/API/Service/Responses/ServiceFeeResponce.swift b/LiskKit/Sources/API/Service/Responses/ServiceFeeResponce.swift index a65e1c670..725b8a18b 100644 --- a/LiskKit/Sources/API/Service/Responses/ServiceFeeResponce.swift +++ b/LiskKit/Sources/API/Service/Responses/ServiceFeeResponce.swift @@ -1,6 +1,6 @@ // // ServiceFeeResponse.swift -// +// // // Created by Anton Boyarkin on 20.08.2021. // diff --git a/LiskKit/Sources/API/Service/Responses/ServiceTransactionsResponse.swift b/LiskKit/Sources/API/Service/Responses/ServiceTransactionsResponse.swift index d8502b04d..8b3c504b5 100644 --- a/LiskKit/Sources/API/Service/Responses/ServiceTransactionsResponse.swift +++ b/LiskKit/Sources/API/Service/Responses/ServiceTransactionsResponse.swift @@ -1,6 +1,6 @@ // // ServiceTransactionsResponse.swift -// +// // // Created by Anton Boyarkin on 15.08.2021. // diff --git a/LiskKit/Sources/API/Service/Service.swift b/LiskKit/Sources/API/Service/Service.swift index 27f2d835d..33614f32b 100644 --- a/LiskKit/Sources/API/Service/Service.swift +++ b/LiskKit/Sources/API/Service/Service.swift @@ -8,7 +8,7 @@ import Foundation public struct Service: APIService { - + public enum Version: String { case v1 = "v1" case v2 = "v2" @@ -24,7 +24,7 @@ public struct Service: APIService { self.init(client: client) self.version = version } - + public init(client: APIClient = .shared) { self.client = client } @@ -37,7 +37,7 @@ extension Service { public func getFees(completionHandler: @escaping (Response) -> Void) { client.get(path: "\(Version.v3.rawValue)/fees", completionHandler: completionHandler) } - + public func fees() async throws -> ServiceFeeResponse { try await client.request( .get, @@ -45,7 +45,7 @@ extension Service { options: nil ) } - + public func info() async throws -> ServiceInfoModelDTO { try await client.request( .get, @@ -61,7 +61,7 @@ extension Service { completionHandler: completionHandler ) } - + /// List transaction objects public func transactions( ownerAddress: String?, @@ -103,7 +103,7 @@ extension Service { fee: $0.fee, signature: $0.signature, confirmations: $0.confirmations, - isOutgoing: $0.senderId.lowercased() == ownerAddress?.lowercased(), + isOutgoing: $0.senderId.lowercased() == ownerAddress?.lowercased(), nonce: $0.nonce, executionStatus: $0.executionStatus, txData: $0.params.data @@ -116,7 +116,7 @@ extension Service { } } } - + private func transactionsV3( id: String? = nil, block: String? = nil, diff --git a/LiskKit/Sources/API/Transactions/LocalTransaction.swift b/LiskKit/Sources/API/Transactions/LocalTransaction.swift index 173e4468d..1f85f0977 100644 --- a/LiskKit/Sources/API/Transactions/LocalTransaction.swift +++ b/LiskKit/Sources/API/Transactions/LocalTransaction.swift @@ -161,14 +161,7 @@ extension LocalTransaction { var bytes: [UInt8] { return - typeBytes + - timestampBytes + - senderPublicKeyBytes + - recipientIdBytes + - amountBytes + - assetBytes + - signatureBytes + - signSignatureBytes + typeBytes + timestampBytes + senderPublicKeyBytes + recipientIdBytes + amountBytes + assetBytes + signatureBytes + signSignatureBytes } var typeBytes: [UInt8] { @@ -186,7 +179,8 @@ extension LocalTransaction { var recipientIdBytes: [UInt8] { guard let value = recipientId?.replacingOccurrences(of: "L", with: ""), - let number = UInt64(value) else { return [UInt8](repeating: 0, count: 8) } + let number = UInt64(value) + else { return [UInt8](repeating: 0, count: 8) } return BytePacker.pack(number, byteOrder: .bigEndian) } @@ -207,7 +201,7 @@ extension LocalTransaction { let data = asset as? [String: [String: String]], let signature = data["signature"], let publicKey = signature["publicKey"] - else { return [] } + else { return [] } return publicKey.hexBytes() } } @@ -228,20 +222,20 @@ extension LocalTransaction { "asset": asset ?? Asset(), "signature": signature ?? NSNull() ] - + if let value = signSignature { options["signSignature"] = value } - + return options } } protocol BinaryConvertible { - static func +(lhs: Data, rhs: Self) -> Data - static func +=(lhs: inout Data, rhs: Self) + static func + (lhs: Data, rhs: Self) -> Data + static func += (lhs: inout Data, rhs: Self) } extension BinaryConvertible { - static func +(lhs: Data, rhs: Self) -> Data { + static func + (lhs: Data, rhs: Self) -> Data { var value = rhs let data = withUnsafePointer(to: &value) { ptr -> Data in return Data(buffer: UnsafeBufferPointer(start: ptr, count: 1)) @@ -249,7 +243,7 @@ extension BinaryConvertible { return lhs + data } - static func +=(lhs: inout Data, rhs: Self) { + static func += (lhs: inout Data, rhs: Self) { lhs = lhs + rhs } } @@ -265,20 +259,20 @@ extension Int64: BinaryConvertible {} extension Int: BinaryConvertible {} extension Bool: BinaryConvertible { - static func +(lhs: Data, rhs: Bool) -> Data { + static func + (lhs: Data, rhs: Bool) -> Data { return lhs + (rhs ? UInt8(0x01) : UInt8(0x00)).littleEndian } } extension String: BinaryConvertible { - static func +(lhs: Data, rhs: String) -> Data { + static func + (lhs: Data, rhs: String) -> Data { guard let data = rhs.data(using: .utf8) else { return lhs } return lhs + data } } extension Data: BinaryConvertible { - static func +(lhs: Data, rhs: Data) -> Data { + static func + (lhs: Data, rhs: Data) -> Data { var data = Data() data.append(lhs) data.append(rhs) diff --git a/LiskKit/Sources/API/Transactions/Models/TransactionModel.swift b/LiskKit/Sources/API/Transactions/Models/TransactionModel.swift index 01708120a..19fa89321 100644 --- a/LiskKit/Sources/API/Transactions/Models/TransactionModel.swift +++ b/LiskKit/Sources/API/Transactions/Models/TransactionModel.swift @@ -8,7 +8,7 @@ import Foundation extension Transactions { - + public struct TransactionSubmitModel: APIModel { public let transactionId: String @@ -50,15 +50,15 @@ extension Transactions { public let signature: String public var confirmations: UInt64? - + public var isOutgoing: Bool = false - + public var nonce: String - + public var executionStatus: ExecutionStatus public var txData: String? - + // MARK: - Hashable public static func == (lhs: TransactionModel, rhs: TransactionModel) -> Bool { @@ -68,11 +68,11 @@ extension Transactions { public var hashValue: Int { return id.hashValue } - + public func hash(into hasher: inout Hasher) { hasher.combine(id) } - + public mutating func updateConfirmations(value: UInt64) { confirmations = value } diff --git a/LiskKit/Sources/API/Transactions/Responses/TransactionBroadcastResponse.swift b/LiskKit/Sources/API/Transactions/Responses/TransactionBroadcastResponse.swift index 7178e17ea..011662631 100644 --- a/LiskKit/Sources/API/Transactions/Responses/TransactionBroadcastResponse.swift +++ b/LiskKit/Sources/API/Transactions/Responses/TransactionBroadcastResponse.swift @@ -13,9 +13,9 @@ extension Transactions { public let data: APIMessageModel } - + public struct TransactionSubmitResponse: APIResponse { - + public let data: TransactionSubmitModel } } diff --git a/LiskKit/Sources/API/Transactions/TransactionEntity+Extension.swift b/LiskKit/Sources/API/Transactions/TransactionEntity+Extension.swift index 09b5e728f..9b57b08e5 100644 --- a/LiskKit/Sources/API/Transactions/TransactionEntity+Extension.swift +++ b/LiskKit/Sources/API/Transactions/TransactionEntity+Extension.swift @@ -7,24 +7,24 @@ import Foundation -public extension TransactionEntity { - var id: String { +extension TransactionEntity { + public var id: String { getTxID() ?? "" } - - var recipientAddressBase32: String { + + public var recipientAddressBase32: String { let bytes = [UInt8](params.recipientAddressBinary) let binary = bytes.hexString() return Crypto.getBase32Address(binaryAddress: binary) } - - var senderAddress: String { + + public var senderAddress: String { let bytes = [UInt8](senderPublicKey) let senderPublicKey = bytes.hexString() return Crypto.getBase32Address(from: senderPublicKey) } - - func createTx( + + public func createTx( amount: Decimal, fee: Decimal, nonce: UInt64, @@ -34,7 +34,7 @@ public extension TransactionEntity { ) -> TransactionEntity { let amount = Crypto.fixedPoint(amount: amount) let fee = Crypto.fixedPoint(amount: fee) - + return TransactionEntity.with { $0.command = Constants.command $0.module = Constants.module @@ -50,10 +50,10 @@ public extension TransactionEntity { $0.signatures = [] } } - - func sign(with keyPair: KeyPair, for chainID: String) -> TransactionEntity { + + public func sign(with keyPair: KeyPair, for chainID: String) -> TransactionEntity { let signature = signature(with: keyPair, for: chainID) - + return TransactionEntity.with { $0.command = command $0.module = module @@ -64,41 +64,42 @@ public extension TransactionEntity { $0.signatures = [Data(signature.allHexBytes())] } } - - func getTxHash() -> String? { + + public func getTxHash() -> String? { let bytes = try? serializedData() return bytes?.hexString() } - - func getTxID() -> String? { + + public func getTxID() -> String? { let bytes = try? serializedData() return bytes?.sha256().hexString() } - - func getFee(with minFeePerByte: UInt64) -> UInt64 { + + public func getFee(with minFeePerByte: UInt64) -> UInt64 { let bytesCount = (try? serializedData().count) ?? .zero return UInt64(bytesCount) * minFeePerByte } } -private extension TransactionEntity { - func signature(with keyPair: KeyPair, for chainID: String) -> String { +extension TransactionEntity { + fileprivate func signature(with keyPair: KeyPair, for chainID: String) -> String { let unsignedBytes = (try? serializedData()) ?? Data() - + guard !unsignedBytes.isEmpty else { return "" } - + let tagBytes: [UInt8] = Array("KLY_TX_".utf8) let chainBytes: [UInt8] = chainID.allHexBytes() - let allBytes = tagBytes - + chainBytes - + unsignedBytes - + let allBytes = + tagBytes + + chainBytes + + unsignedBytes + let sha = allBytes.sha256() let signBytes = Ed25519.sign(message: sha, privateKey: keyPair.privateKey) let sign = signBytes.hexString() - + return sign } } diff --git a/LiskKit/Sources/API/Transactions/Transactions.swift b/LiskKit/Sources/API/Transactions/Transactions.swift index 90adeac00..158b12c57 100644 --- a/LiskKit/Sources/API/Transactions/Transactions.swift +++ b/LiskKit/Sources/API/Transactions/Transactions.swift @@ -44,16 +44,16 @@ extension Transactions { client.post(path: "transactions", options: signedTransaction.requestOptions, completionHandler: completionHandler) } - + public func submit(signedTransaction: RequestOptions, completionHandler: @escaping (Response) -> Void) { client.post(path: "transactions", options: signedTransaction, completionHandler: completionHandler) } - + public func submit(transaction: TransactionEntity) async throws -> TransactionSubmitModel { guard let hash = transaction.getTxHash() else { throw APIError.unexpected(code: 0) } - + return try await client.request( method: "txpool_postTransaction", params: ["transaction": hash] @@ -66,7 +66,13 @@ extension Transactions { extension Transactions { /// Transfer LSK to a Lisk address using Local Signing - public func transfer(lsk: Double, to recipient: String, passphrase: String, secondPassphrase: String? = nil, completionHandler: @escaping (Response) -> Void) { + public func transfer( + lsk: Double, + to recipient: String, + passphrase: String, + secondPassphrase: String? = nil, + completionHandler: @escaping (Response) -> Void + ) { do { let transaction = LocalTransaction(.transfer, lsk: lsk, recipientId: recipient) let signedTransaction = try transaction.signed(passphrase: passphrase, secondPassphrase: secondPassphrase) @@ -76,7 +82,7 @@ extension Transactions { completionHandler(.error(response: response)) } } - + /// Transfer LSK to a Lisk address using Local Signing with KeyPair public func transfer(lsk: Double, to recipient: String, keyPair: KeyPair, completionHandler: @escaping (Response) -> Void) { do { @@ -95,7 +101,11 @@ extension Transactions { extension Transactions { /// Register a second passphrase - public func registerSecondPassphrase(_ secondPassphrase: String, passphrase: String, completionHandler: @escaping (Response) -> Void) { + public func registerSecondPassphrase( + _ secondPassphrase: String, + passphrase: String, + completionHandler: @escaping (Response) -> Void + ) { do { let (publicKey, _) = try Crypto.keys(fromPassphrase: secondPassphrase) let asset = ["signature": ["publicKey": publicKey]] @@ -114,7 +124,17 @@ extension Transactions { extension Transactions { /// List transaction objects - public func transactions(id: String? = nil, block: String? = nil, sender: String? = nil, recipient: String? = nil, senderIdOrRecipientId: String? = nil, limit: UInt? = nil, offset: UInt? = nil, sort: APIRequest.Sort? = nil, completionHandler: @escaping (Response) -> Void) { + public func transactions( + id: String? = nil, + block: String? = nil, + sender: String? = nil, + recipient: String? = nil, + senderIdOrRecipientId: String? = nil, + limit: UInt? = nil, + offset: UInt? = nil, + sort: APIRequest.Sort? = nil, + completionHandler: @escaping (Response) -> Void + ) { var options: RequestOptions = [:] if let value = id { options["id"] = value } if let value = block { options["blockId"] = value } diff --git a/LiskKit/Sources/Constants.swift b/LiskKit/Sources/Constants.swift index ce0684387..232c5941b 100644 --- a/LiskKit/Sources/Constants.swift +++ b/LiskKit/Sources/Constants.swift @@ -29,7 +29,7 @@ public struct Constants { } public struct Time { - public static let epochMilliseconds: Double = 1464109200000 + public static let epochMilliseconds: Double = 1_464_109_200_000 public static let epochSeconds: TimeInterval = epochMilliseconds / 1000 public static let epoch: Date = Date(timeIntervalSince1970: epochSeconds) } @@ -39,7 +39,7 @@ public struct Constants { public static let test = "15f0dacc1060e91818224a94286b13aa04279c640bd5d6f193182031d133df7c" public static let beta = "ef3844327d1fd0fc5785291806150c937797bdb34a748c9cd932b7e859e9ca0c" } - + public static let chainID = "00000000" public static let tokenID = "0000000000000000" public static let command = "transfer" diff --git a/LiskKit/Sources/Core/APIClient.swift b/LiskKit/Sources/Core/APIClient.swift index ec0400d30..1c35067e2 100644 --- a/LiskKit/Sources/Core/APIClient.swift +++ b/LiskKit/Sources/Core/APIClient.swift @@ -42,7 +42,7 @@ public struct APIClient { /// Client that connects to Betanet public static let betanet = APIClient(options: .betanet) - + public struct Service { public static let mainnet = APIClient(options: .Service.mainnet) public static let testnet = APIClient(options: .Service.testnet) @@ -53,7 +53,7 @@ public struct APIClient { let jsonrpc: String let result: R } - + // MARK: - Init public init(options: APIOptions = .mainnet) { @@ -99,7 +99,12 @@ public struct APIClient { /// Perform request @discardableResult - public func request(_ httpMethod: HTTPMethod, path: String, options: Any?, completionHandler: @escaping (Response) -> Void) -> (URLRequest, URLSessionDataTask) { + public func request( + _ httpMethod: HTTPMethod, + path: String, + options: Any?, + completionHandler: @escaping (Response) -> Void + ) -> (URLRequest, URLSessionDataTask) { let request = urlRequest(httpMethod, path: path, options: options) let task = dataTask(request, completionHandler: completionHandler) return (request, task) @@ -114,7 +119,7 @@ public struct APIClient { let response: R = try await dataTask(request) return response } - + public func request( method: String, params: [String: Any] @@ -123,7 +128,7 @@ public struct APIClient { let response: R = try await dataTask(request) return response } - + // MARK: - Private /// Base url of all requests @@ -143,7 +148,7 @@ public struct APIClient { let response: R = try processRequestCompletion(data.0, response: data.1) return response } - + /// Create a json data task private func dataTask(_ request: URLRequest, completionHandler: @escaping (Response) -> Void) -> URLSessionDataTask { let task = urlSession.dataTask(with: request) { data, response, _ in @@ -188,17 +193,17 @@ public struct APIClient { ) throws -> URLRequest { let url = baseURL.appendingPathComponent(rpcPath) var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData) - + request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") - + let requestBody: [String: Any] = [ "jsonrpc": "2.0", "id": "1", "method": method, "params": params ] - + let jsonData = try JSONSerialization.data(withJSONObject: requestBody) request.httpBody = jsonData return request @@ -213,7 +218,7 @@ public struct APIClient { guard let safeKey = key.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), let safeValue = "\(value)".addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) - else { return nil } + else { return nil } return "\(safeKey)=\(safeValue)" } @@ -226,7 +231,7 @@ public struct APIClient { response: URLResponse? ) -> Response { let code = (response as? HTTPURLResponse)?.statusCode - + guard let data = data else { return .error(response: .unexpected(code: code)) } @@ -235,8 +240,7 @@ public struct APIClient { if var error = try? JSONDecoder().decode(APIError.self, from: data) { error.code = code return .error(response: error) - } else if - let error = try? JSONDecoder().decode(APIErrors.self, from: data), + } else if let error = try? JSONDecoder().decode(APIErrors.self, from: data), var first = error.errors.first { first.code = code @@ -247,35 +251,35 @@ public struct APIClient { return .success(response: result) } - + /// Process a response private func processRequestCompletion( _ data: Data?, response: URLResponse? ) throws -> R { let code = (response as? HTTPURLResponse)?.statusCode - + guard let data = data else { throw APIError.unexpected(code: code) } do { - if let jsonResponse = try? JSONDecoder().decode(JSONResponse.self, from: data) { - return jsonResponse.result - } else if let result = try? JSONDecoder().decode(R.self, from: data) { - return result - } else if var error = try? JSONDecoder().decode(APIError.self, from: data) { - error.code = code - throw error - } else if let error = try? JSONDecoder().decode(APIErrors.self, from: data), var first = error.errors.first { - first.code = code - throw first - } else { - throw APIError.unknown(code: code) - } - } catch { - throw error - } - + if let jsonResponse = try? JSONDecoder().decode(JSONResponse.self, from: data) { + return jsonResponse.result + } else if let result = try? JSONDecoder().decode(R.self, from: data) { + return result + } else if var error = try? JSONDecoder().decode(APIError.self, from: data) { + error.code = code + throw error + } else if let error = try? JSONDecoder().decode(APIErrors.self, from: data), var first = error.errors.first { + first.code = code + throw first + } else { + throw APIError.unknown(code: code) + } + } catch { + throw error + } + } } diff --git a/LiskKit/Sources/Core/APIError.swift b/LiskKit/Sources/Core/APIError.swift index e9daf2ba8..524a5e59c 100644 --- a/LiskKit/Sources/Core/APIError.swift +++ b/LiskKit/Sources/Core/APIError.swift @@ -18,9 +18,9 @@ public struct APIErrors: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: Keys.self) - self.errors = try container.decode([APIError].self, forKey: .errors) + self.errors = try container.decode([APIError].self, forKey: .errors) } - + public init(errors: [APIError]) { self.errors = errors } @@ -30,7 +30,7 @@ public struct APIErrors: Decodable { public struct APIError: LocalizedError, Equatable { public let message: String public var code: Int? - + public var errorDescription: String? { message } public init(message: String, code: Int?) { @@ -55,9 +55,9 @@ extension APIError: Decodable { } } -public extension APIError { +extension APIError { public static let noNetwork = Self.unexpected(code: nil) - + /// Describes an unexpected error public static func unexpected(code: Int?) -> Self { .init(message: "Unexpected Error", code: code) diff --git a/LiskKit/Sources/Core/APIModel.swift b/LiskKit/Sources/Core/APIModel.swift index 542051e3a..7172b55d6 100644 --- a/LiskKit/Sources/Core/APIModel.swift +++ b/LiskKit/Sources/Core/APIModel.swift @@ -9,7 +9,7 @@ import Foundation /// API model public protocol APIModel: Decodable, Hashable { - + } /// Common model for single message response diff --git a/LiskKit/Sources/Core/APINethash.swift b/LiskKit/Sources/Core/APINethash.swift index d28b3a920..987116dda 100644 --- a/LiskKit/Sources/Core/APINethash.swift +++ b/LiskKit/Sources/Core/APINethash.swift @@ -43,17 +43,17 @@ public struct APINethash { let osName: String = { #if os(iOS) - return "iOS" + return "iOS" #elseif os(watchOS) - return "watchOS" + return "watchOS" #elseif os(tvOS) - return "tvOS" + return "tvOS" #elseif os(macOS) - return "OS X" + return "OS X" #elseif os(Linux) - return "Linux" + return "Linux" #else - return "Unknown" + return "Unknown" #endif }() diff --git a/LiskKit/Sources/Core/APINode.swift b/LiskKit/Sources/Core/APINode.swift index 4fdda31c8..ade9220a4 100644 --- a/LiskKit/Sources/Core/APINode.swift +++ b/LiskKit/Sources/Core/APINode.swift @@ -51,7 +51,7 @@ extension Array where Element == APINode { .init(origin: "https://testnet-service.lisk.io") ] } - + } extension Array where Element == APINode { diff --git a/LiskKit/Sources/Core/APIOptions.swift b/LiskKit/Sources/Core/APIOptions.swift index 71a369cab..a422d55bc 100644 --- a/LiskKit/Sources/Core/APIOptions.swift +++ b/LiskKit/Sources/Core/APIOptions.swift @@ -23,7 +23,7 @@ public struct APIOptions { public var node: APINode { return nodes.select(random: randomNode) } - + public init(nodes: [APINode], nethash: APINethash, randomNode: Bool) { self.nodes = nodes self.nethash = nethash @@ -41,7 +41,7 @@ extension APIOptions { /// Betanet options public static let betanet: APIOptions = .init(nodes: .betanet, nethash: .mainnet, randomNode: true) - + public struct Service { /// Mainnet options public static let mainnet: APIOptions = .init(nodes: .Service.mainnet, nethash: .mainnet, randomNode: true) diff --git a/LiskKit/Sources/Core/APIRequest.swift b/LiskKit/Sources/Core/APIRequest.swift index b6f75feda..a3fe69611 100644 --- a/LiskKit/Sources/Core/APIRequest.swift +++ b/LiskKit/Sources/Core/APIRequest.swift @@ -24,7 +24,7 @@ public struct APIRequest { public var value: String { return "\(column):\(direction.rawValue)" } - + public init(_ column: String, direction: Direction = .ascending) { self.column = column self.direction = direction diff --git a/LiskKit/Sources/Crypto/BytePacker.swift b/LiskKit/Sources/Crypto/BytePacker.swift index 7b0daa714..0be063cbd 100644 --- a/LiskKit/Sources/Crypto/BytePacker.swift +++ b/LiskKit/Sources/Crypto/BytePacker.swift @@ -20,10 +20,10 @@ internal struct BytePacker { /// - value: value to pack of type `T` /// - byteOrder: Byte order (wither little or big endian) /// - Returns: Byte array - static func pack( _ value: T, byteOrder: ByteOrder) -> [UInt8] { - var value = value // inout works only for var not let types + static func pack(_ value: T, byteOrder: ByteOrder) -> [UInt8] { + var value = value // inout works only for var not let types let valueByteArray = withUnsafePointer(to: &value) { - Array(UnsafeBufferPointer(start: $0.withMemoryRebound(to: UInt8.self, capacity: 1) {$0}, count: MemoryLayout.size)) + Array(UnsafeBufferPointer(start: $0.withMemoryRebound(to: UInt8.self, capacity: 1) { $0 }, count: MemoryLayout.size)) } return (byteOrder == .littleEndian) ? valueByteArray : valueByteArray.reversed() } diff --git a/LiskKit/Sources/Crypto/Crypto.swift b/LiskKit/Sources/Crypto/Crypto.swift index 3cbbccc7e..13f830e1a 100644 --- a/LiskKit/Sources/Crypto/Crypto.swift +++ b/LiskKit/Sources/Crypto/Crypto.swift @@ -5,9 +5,9 @@ // Created by Andrew Barba on 1/1/18. // -import Foundation import Clibsodium import CryptoSwift +import Foundation public struct Crypto { @@ -22,13 +22,13 @@ public struct Crypto { let bytes = SHA256(passphrase).digest() return try KeyPair(seed: bytes) } - + /// Generate key pair from a given secret and salt public static func keyPair(fromPassphrase passphrase: String, salt: String) throws -> KeyPair { let bytes = try Crypto.seed(passphrase: passphrase, salt: salt) return try KeyPair(seed: bytes) } - + private static func seed(passphrase: String, salt: String = "mnemonic") throws -> [UInt8] { let password = passphrase.decomposedStringWithCompatibilityMapping let salt = salt.decomposedStringWithCompatibilityMapping @@ -44,7 +44,7 @@ public struct Crypto { static let PREFIX_KLY = "kly" static let CHARSET = Array("zxvcpmbn3465o978uyrtkqew2adsjhfg") - static let GENERATOR: [UInt] = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] + static let GENERATOR: [UInt] = [0x3b6a_57b2, 0x2650_8e6d, 0x1ea1_19fa, 0x3d42_33dd, 0x2a14_62b3] public static func getAddress(from publicKey: String) -> String { return getBinaryAddress(from: publicKey).hexString() @@ -65,7 +65,7 @@ public struct Crypto { let identifier = convertUInt5ToBase32(uint5Address + uint5Checksum) return "\(PREFIX_KLY)\(identifier)" } - + public static func getBinaryAddressFromBase32(_ base32Address: String) -> String? { guard isValidBase32(address: base32Address) else { return nil } @@ -89,7 +89,7 @@ public struct Crypto { return true } - + internal static func getBinaryAddress(from publicKey: String) -> [UInt8] { let bytes = SHA256(publicKey.hexBytes()).digest()[0..<20] return Array(bytes) @@ -103,7 +103,7 @@ public struct Crypto { for byte in array { accumulator = (accumulator << from) | byte bits += from - while (bits >= to) { + while bits >= to { bits -= to result.append((accumulator >> bits) & maxValue) } @@ -127,7 +127,7 @@ public struct Crypto { let top: UInt = chk >> 25 chk = ((chk & 0x1ffffff) << 5) ^ value for i: UInt8 in 0..<6 { - if (((top >> i) & 1) != 0) { + if ((top >> i) & 1) != 0 { chk ^= GENERATOR[Int(i)] } } @@ -138,8 +138,8 @@ public struct Crypto { internal static func convertUInt5ToBase32(_ array: [UInt]) -> String { return array.map { String(CHARSET[Int($0)]) }.joined() } - - internal static func covertBase32toUInt5(_ value:String) -> [UInt] { + + internal static func covertBase32toUInt5(_ value: String) -> [UInt] { return value.enumerated().map { UInt(CHARSET.firstIndex(of: Character(String($0.element))) ?? 0) } } @@ -155,17 +155,21 @@ public struct Crypto { guard signature.count == 64 else { throw CryptoError.invalidSignatureLength } - + let signature = signature.hexBytes() let message = message.hexBytes() let publicKey = publicKey.hexBytes() - - guard .SUCCESS == crypto_sign_verify_detached( - signature, - message, - UInt64(message.count), - publicKey).exitCode else { throw CryptoError.invalidSignature } - + + guard + .SUCCESS + == crypto_sign_verify_detached( + signature, + message, + UInt64(message.count), + publicKey + ).exitCode + else { throw CryptoError.invalidSignature } + return true } @@ -173,13 +177,17 @@ public struct Crypto { guard signature.count == 64 else { throw CryptoError.invalidSignatureLength } - - guard .SUCCESS == crypto_sign_verify_detached( - signature, - message, - UInt64(message.count), - publicKey).exitCode else { throw CryptoError.invalidSignature } - + + guard + .SUCCESS + == crypto_sign_verify_detached( + signature, + message, + UInt64(message.count), + publicKey + ).exitCode + else { throw CryptoError.invalidSignature } + return true } @@ -224,19 +232,41 @@ extension KeyPair { extension String { internal func hexBytes() -> [UInt8] { - return (0.. [UInt8] { - return (0.. UInt8? { + switch char { + case 48...57: // '0'-'9' + return UInt8(char - 48) + case 65...70: // 'A'-'F' + return UInt8(char - 55) + case 97...102: // 'a'-'f' + return UInt8(char - 87) + default: + return nil } } } @@ -251,17 +281,17 @@ extension Sequence where Self.Element == UInt8 { private enum ExitCode { case SUCCESS case FAILURE - - init (from int: Int32) { + + init(from int: Int32) { switch int { - case 0: self = .SUCCESS + case 0: self = .SUCCESS default: self = .FAILURE } } } -private extension Int32 { - var exitCode: ExitCode { return ExitCode(from: self) } +extension Int32 { + fileprivate var exitCode: ExitCode { return ExitCode(from: self) } } public enum CryptoError: Error { @@ -277,41 +307,48 @@ public enum CryptoError: Error { } public class KeyPair { - + public let publicKey: [UInt8] public let privateKey: [UInt8] - + public init(publicKey: [UInt8], privateKey: [UInt8]) { self.publicKey = publicKey self.privateKey = privateKey } - + public convenience init(seed: [UInt8]) throws { var publicKey = [UInt8](repeating: 0, count: Int(crypto_sign_publickeybytes())) var privateKey = [UInt8](repeating: 0, count: Int(crypto_sign_secretkeybytes())) - - guard .SUCCESS == crypto_sign_seed_keypair( - &publicKey, - &privateKey, - seed - ).exitCode else { throw CryptoError.keysGenerationFailed } - + + guard + .SUCCESS + == crypto_sign_seed_keypair( + &publicKey, + &privateKey, + seed + ).exitCode + else { throw CryptoError.keysGenerationFailed } + self.init(publicKey: publicKey, privateKey: privateKey) } - + public func sign(_ message: [UInt8]) -> [UInt8] { var signature = [UInt8](repeating: 0, count: Int(crypto_sign_bytes())) - - guard .SUCCESS == crypto_sign_detached( - &signature, - nil, - message, UInt64(message.count), - privateKey - ).exitCode else { - return [UInt8]() + + guard + .SUCCESS + == crypto_sign_detached( + &signature, + nil, + message, + UInt64(message.count), + privateKey + ).exitCode + else { + return [UInt8]() } - + return signature } - + } diff --git a/LiskKit/Sources/Crypto/ED25519/Ed25519.swift b/LiskKit/Sources/Crypto/ED25519/Ed25519.swift index 4f38453ce..513b0dd02 100644 --- a/LiskKit/Sources/Crypto/ED25519/Ed25519.swift +++ b/LiskKit/Sources/Crypto/ED25519/Ed25519.swift @@ -1,6 +1,6 @@ // // Ed25519.swift -// +// // // Created by Stanislav Jelezoglo on 18.12.2023. // @@ -15,14 +15,14 @@ public struct Ed25519 { static func sha512(_ s: [UInt8]) -> [UInt8] { #if NO_USE_CryptoSwift - let data = Data(s) - var digest = [UInt8](repeating: 0, count: Int(CC_SHA512_DIGEST_LENGTH)) - data.withUnsafeBytes { (p: UnsafeRawBufferPointer) -> Void in - CC_SHA512(p.baseAddress, CC_LONG(data.count), &digest) - } - return digest + let data = Data(s) + var digest = [UInt8](repeating: 0, count: Int(CC_SHA512_DIGEST_LENGTH)) + data.withUnsafeBytes { (p: UnsafeRawBufferPointer) -> Void in + CC_SHA512(p.baseAddress, CC_LONG(data.count), &digest) + } + return digest #else - return s.sha512() + return s.sha512() #endif } private static func randombytes(_ r: inout [UInt8], len: Int) { @@ -69,9 +69,9 @@ public struct Ed25519 { // sha512 of sk crypto_hash_sha512(&az, secretKey, len: 32) // calc public key - az[0] &= 248 // clear lowest 3bit - az[31] &= 127 // clear highest bit - az[31] |= 64 // set second highest bit + az[0] &= 248 // clear lowest 3bit + az[31] &= 127 // clear highest bit + az[31] |= 64 // set second highest bit sc.sc25519_from32bytes(&sc_sk, az) @@ -124,21 +124,21 @@ public struct Ed25519 { /* pk: 32-byte public key A */ let pk = calcPublicKey(secretKey: secretKey) crypto_hash_sha512(&az, secretKey, len: 32) - az[0] &= 248 // clear lowest 3bit - az[31] &= 127 // clear highest bit - az[31] |= 64 // set second highest bit + az[0] &= 248 // clear lowest 3bit + az[31] &= 127 // clear highest bit + az[31] |= 64 // set second highest bit - var sm = [UInt8](repeating: 0, count: mlen+64) + var sm = [UInt8](repeating: 0, count: mlen + 64) for i in 0.. UInt32 /* 16-bit inputs */ { if a < b { @@ -91,7 +96,7 @@ struct sc { } // no borrow: mask = 0xffffffff -> r = r - m // borrow : mask = 0x0 -> r = r - let mask = UInt32(bitPattern: Int32(borrow)-1) + let mask = UInt32(bitPattern: Int32(borrow) - 1) for i in 0..= k-1 { - q2[i+j] += x[j+k-1] * mu[i] + if i + j >= k - 1 { + q2[i + j] += x[j + k - 1] * mu[i] } } } @@ -123,30 +128,30 @@ struct sc { // q3 = (... + b^(k+1) * q2[k+1] + b^(k+2) * q2[k+2] + ... + b^(2k) * q2[2k] + b^(2k+1) * q2[2k+1]) // = q2[k+1] + b^1 * q2[k+1] + ... + b^(k-1) * q2[2k] + b^k * q2[2k+1] // Since q2[2k] has carry q2[2k+1] is zero. - let carry1 = q2[k-1] >> 8 + let carry1 = q2[k - 1] >> 8 q2[k] += carry1 let carry2 = q2[k] >> 8 - q2[k+1] += carry2 + q2[k + 1] += carry2 // STEP2,3 // r1 = x (mod b^(k+1)) - var r1 = [UInt32](repeating: 0, count: k+1) + var r1 = [UInt32](repeating: 0, count: k + 1) for i in 0...k { r1[i] = x[i] } // r2 = q3 * m (mod b^(k+1)) - var r2 = [UInt32](repeating: 0, count: k+1) - for i in 0...k-1 { + var r2 = [UInt32](repeating: 0, count: k + 1) + for i in 0...k - 1 { for j in 0...k { - if i+j < k+1 { - r2[i+j] += q2[j+k+1] * m[i] + if i + j < k + 1 { + r2[i + j] += q2[j + k + 1] * m[i] } } } - for i in 0...k-1 { + for i in 0...k - 1 { let carry = r2[i] >> 8 - r2[i+1] += carry + r2[i + 1] += carry r2[i] &= 0xff } r2[k] &= 0xff @@ -158,7 +163,7 @@ struct sc { // so r can represented for b^0 y\_0 + b^1 y\_1 + ... + b^(k-1) y\_(k-1) // it means r[v] is zero var val: UInt32 = 0 - for i in 0...k-1 { + for i in 0...k - 1 { val += r2[i] let borrow = lt(r1[i], val) let vv = Int64(r1[i]) - Int64(val) + Int64(borrow << 8) @@ -192,11 +197,11 @@ struct sc { static func sc25519_from32bytes(_ r: inout sc, _ x: [UInt8] /* 32 */) { assert(x.count >= k) - var t = [UInt32](repeating: 0, count: k*2) + var t = [UInt32](repeating: 0, count: k * 2) for i in 0..> 8 - r.v[i+1] += carry + r.v[i + 1] += carry r.v[i] &= 0xff } sc.reduce_add_sub(&r) @@ -290,18 +295,18 @@ struct sc { } static func sc25519_mul(_ r: inout sc, _ x: sc, _ y: sc) { - var t = [UInt32](repeating: 0, count: k*2) + var t = [UInt32](repeating: 0, count: k * 2) for i in 0..> 8 - t[i+1] += carry + t[i + 1] += carry t[i] &= 0xff } @@ -319,33 +324,33 @@ struct sc { static func sc25519_window3(_ r: inout [Int8] /* 85 */, _ s: sc) { assert(r.count == 85) for i in 0..<10 { - r[8*i+0] = Int8(bitPattern: UInt8(s.v[3*i+0] & 7)) - r[8*i+1] = Int8(bitPattern: UInt8((s.v[3*i+0] >> 3) & 7)) - r[8*i+2] = Int8(bitPattern: UInt8((s.v[3*i+0] >> 6) & 7)) - r[8*i+2] ^= Int8(bitPattern: UInt8((s.v[3*i+1] << 2) & 7)) - r[8*i+3] = Int8(bitPattern: UInt8((s.v[3*i+1] >> 1) & 7)) - r[8*i+4] = Int8(bitPattern: UInt8((s.v[3*i+1] >> 4) & 7)) - r[8*i+5] = Int8(bitPattern: UInt8((s.v[3*i+1] >> 7) & 7)) - r[8*i+5] ^= Int8(bitPattern: UInt8((s.v[3*i+2] << 1) & 7)) - r[8*i+6] = Int8(bitPattern: UInt8((s.v[3*i+2] >> 2) & 7)) - r[8*i+7] = Int8(bitPattern: UInt8((s.v[3*i+2] >> 5) & 7)) + r[8 * i + 0] = Int8(bitPattern: UInt8(s.v[3 * i + 0] & 7)) + r[8 * i + 1] = Int8(bitPattern: UInt8((s.v[3 * i + 0] >> 3) & 7)) + r[8 * i + 2] = Int8(bitPattern: UInt8((s.v[3 * i + 0] >> 6) & 7)) + r[8 * i + 2] ^= Int8(bitPattern: UInt8((s.v[3 * i + 1] << 2) & 7)) + r[8 * i + 3] = Int8(bitPattern: UInt8((s.v[3 * i + 1] >> 1) & 7)) + r[8 * i + 4] = Int8(bitPattern: UInt8((s.v[3 * i + 1] >> 4) & 7)) + r[8 * i + 5] = Int8(bitPattern: UInt8((s.v[3 * i + 1] >> 7) & 7)) + r[8 * i + 5] ^= Int8(bitPattern: UInt8((s.v[3 * i + 2] << 1) & 7)) + r[8 * i + 6] = Int8(bitPattern: UInt8((s.v[3 * i + 2] >> 2) & 7)) + r[8 * i + 7] = Int8(bitPattern: UInt8((s.v[3 * i + 2] >> 5) & 7)) } let i = 10 - r[8*i+0] = Int8(bitPattern: UInt8(s.v[3*i+0] & 7)) - r[8*i+1] = Int8(bitPattern: UInt8((s.v[3*i+0] >> 3) & 7)) - r[8*i+2] = Int8(bitPattern: UInt8((s.v[3*i+0] >> 6) & 7)) - r[8*i+2] ^= Int8(bitPattern: UInt8((s.v[3*i+1] << 2) & 7)) - r[8*i+3] = Int8(bitPattern: UInt8((s.v[3*i+1] >> 1) & 7)) - r[8*i+4] = Int8(bitPattern: UInt8((s.v[3*i+1] >> 4) & 7)) + r[8 * i + 0] = Int8(bitPattern: UInt8(s.v[3 * i + 0] & 7)) + r[8 * i + 1] = Int8(bitPattern: UInt8((s.v[3 * i + 0] >> 3) & 7)) + r[8 * i + 2] = Int8(bitPattern: UInt8((s.v[3 * i + 0] >> 6) & 7)) + r[8 * i + 2] ^= Int8(bitPattern: UInt8((s.v[3 * i + 1] << 2) & 7)) + r[8 * i + 3] = Int8(bitPattern: UInt8((s.v[3 * i + 1] >> 1) & 7)) + r[8 * i + 4] = Int8(bitPattern: UInt8((s.v[3 * i + 1] >> 4) & 7)) /* Making it signed */ var carry: Int8 = 0 for i in 0..<84 { r[i] += carry - r[i+1] += (r[i] >> 3) + r[i + 1] += (r[i] >> 3) r[i] &= 7 carry = r[i] >> 2 - let vv: Int16 = Int16(r[i]) - Int16(carry<<3) + let vv: Int16 = Int16(r[i]) - Int16(carry << 3) assert(vv >= -128 && vv <= 127) r[i] = Int8(vv) } @@ -360,10 +365,10 @@ struct sc { let a2 = UInt8(s2.v[i] & 0xff) // 8bits = 2bits * 4 // s2 s1 - r[4*i] = ((a1 >> 0) & 3) ^ (((a2 >> 0) & 3) << 2) - r[4*i+1] = ((a1 >> 2) & 3) ^ (((a2 >> 2) & 3) << 2) - r[4*i+2] = ((a1 >> 4) & 3) ^ (((a2 >> 4) & 3) << 2) - r[4*i+3] = ((a1 >> 6) & 3) ^ (((a2 >> 6) & 3) << 2) + r[4 * i] = ((a1 >> 0) & 3) ^ (((a2 >> 0) & 3) << 2) + r[4 * i + 1] = ((a1 >> 2) & 3) ^ (((a2 >> 2) & 3) << 2) + r[4 * i + 2] = ((a1 >> 4) & 3) ^ (((a2 >> 4) & 3) << 2) + r[4 * i + 3] = ((a1 >> 6) & 3) ^ (((a2 >> 6) & 3) << 2) } let b1 = UInt8(s1.v[31] & 0xff) @@ -383,10 +388,10 @@ struct fe: CustomDebugStringConvertible { // + 2^(2*8) * v[2] // + 2^(1*8) * v[1] // + 2^(0*8) * v[0] - public var v: [UInt32] // size:32 + public var v: [UInt32] // size:32 public var debugDescription: String { - return v.map({ String(format: "%d ", $0)}).joined() + return v.map({ String(format: "%d ", $0) }).joined() } public init() { @@ -437,7 +442,7 @@ struct fe: CustomDebugStringConvertible { // move up for i in 0..<31 { s = r.v[i] >> 8 - r.v[i+1] += s + r.v[i + 1] += s r.v[i] &= 0xff } } @@ -446,18 +451,23 @@ struct fe: CustomDebugStringConvertible { static func reduce_mul(_ r: inout fe) { var t: UInt32 var s: UInt32 - for _ in 0..<2 { + var i = 0 + while i < 2 { // use q = 2^(31*8)*(2^7) - 19 t = r.v[31] >> 7 r.v[31] &= 0x7f t = times19(t) r.v[0] += t // move up - for i in 0..<31 { - s = r.v[i] >> 8 - r.v[i+1] += s - r.v[i] &= 0xff + var j = 0 + while j < 31 { + s = r.v[j] >> 8 + r.v[j + 1] += s + r.v[j] &= 0xff + j += 1 } + + i += 1 } } @@ -476,19 +486,19 @@ struct fe: CustomDebugStringConvertible { m = UInt32(bitPattern: Int32(m) * -1) // m is 0xffffffff or 0x0 - r.v[31] -= (m&127) + r.v[31] -= (m & 127) for i in stride(from: 30, to: 0, by: -1) { - r.v[i] -= m&255 + r.v[i] -= m & 255 } - r.v[0] -= m&237 + r.v[0] -= m & 237 } - static func fe25519_unpack(_ r: inout fe, _ x: [UInt8]/* 32 */) { + static func fe25519_unpack(_ r: inout fe, _ x: [UInt8] /* 32 */) { assert(x.count == 32) for i in 0..<32 { r.v[i] = UInt32(x[i]) } - r.v[31] &= 127 // remove parity + r.v[31] &= 127 // remove parity } /// Assumes input x being reduced mod 2^255 @@ -569,8 +579,10 @@ struct fe: CustomDebugStringConvertible { /// r = x + y static func fe25519_add(_ r: inout fe, _ x: fe, _ y: fe) { - for i in 0..<32 { + var i = 0 + while i < 32 { r.v[i] = x.v[i] + y.v[i] + i += 1 } fe.reduce_add_sub(&r) } @@ -586,29 +598,43 @@ struct fe: CustomDebugStringConvertible { static func fe25519_sub(_ r: inout fe, _ x: fe, _ y: fe) { // t = 2 * q + x var t = [UInt32](repeating: 0, count: 32) - t[0] = x.v[0] + 0x1da // LSB - for i in 1..<31 { t[i] = x.v[i] + 0x1fe } - t[31] = x.v[31] + 0xfe // MSB + t[0] = x.v[0] + 0x1da // LSB + var i = 1 + while i < 31 { + t[i] = x.v[i] + 0x1fe + i += 1 + } + t[31] = x.v[31] + 0xfe // MSB // r = t - y - for i in 0..<32 { r.v[i] = t[i] - y.v[i] } + i = 0 + while i < 32 { + r.v[i] = t[i] - y.v[i] + i += 1 + } fe.reduce_add_sub(&r) } /// r = x * y static func fe25519_mul(_ r: inout fe, _ x: fe, _ y: fe) { var t = [UInt32](repeating: 0, count: 63) - - for i in 0..<32 { - for j in 0..<32 { - t[i+j] += x.v[i] * y.v[j] + var i = 0 + while i < 32 { + var j = 0 + while j < 32 { + t[i + j] += x.v[i] * y.v[j] + j += 1 } + i += 1 } // 2q = 2^256 - 2*19 // so 2^256 = 2*19 - for i in 32..<63 { - r.v[i-32] = t[i-32] + fe.times38(t[i]) + i = 32 + while i < 63 { + r.v[i - 32] = t[i - 32] + fe.times38(t[i]) + i += 1 } + r.v[31] = t[31] /* result now in r[0]...r[31] */ fe.reduce_mul(&r) @@ -652,32 +678,50 @@ struct fe: CustomDebugStringConvertible { /* 2^11 - 2^1 */ fe25519_square(&t0, z2_10_0) /* 2^12 - 2^2 */ fe25519_square(&t1, t0) - /* 2^20 - 2^10 */ for _ in stride(from: 2, to: 10, by: 2) { fe25519_square(&t0, t1); fe25519_square(&t1, t0) } + /* 2^20 - 2^10 */ for _ in stride(from: 2, to: 10, by: 2) { + fe25519_square(&t0, t1) + fe25519_square(&t1, t0) + } /* 2^20 - 2^0 */ fe25519_mul(&z2_20_0, t1, z2_10_0) /* 2^21 - 2^1 */ fe25519_square(&t0, z2_20_0) /* 2^22 - 2^2 */ fe25519_square(&t1, t0) - /* 2^40 - 2^20 */ for _ in stride(from: 2, to: 20, by: 2) { fe25519_square(&t0, t1); fe25519_square(&t1, t0) } + /* 2^40 - 2^20 */ for _ in stride(from: 2, to: 20, by: 2) { + fe25519_square(&t0, t1) + fe25519_square(&t1, t0) + } /* 2^40 - 2^0 */ fe25519_mul(&t0, t1, z2_20_0) /* 2^41 - 2^1 */ fe25519_square(&t1, t0) /* 2^42 - 2^2 */ fe25519_square(&t0, t1) - /* 2^50 - 2^10 */ for _ in stride(from: 2, to: 10, by: 2) { fe25519_square(&t1, t0); fe25519_square(&t0, t1) } + /* 2^50 - 2^10 */ for _ in stride(from: 2, to: 10, by: 2) { + fe25519_square(&t1, t0) + fe25519_square(&t0, t1) + } /* 2^50 - 2^0 */ fe25519_mul(&z2_50_0, t0, z2_10_0) /* 2^51 - 2^1 */ fe25519_square(&t0, z2_50_0) /* 2^52 - 2^2 */ fe25519_square(&t1, t0) - /* 2^100 - 2^50 */ for _ in stride(from: 2, to: 50, by: 2) { fe25519_square(&t0, t1); fe25519_square(&t1, t0) } + /* 2^100 - 2^50 */ for _ in stride(from: 2, to: 50, by: 2) { + fe25519_square(&t0, t1) + fe25519_square(&t1, t0) + } /* 2^100 - 2^0 */ fe25519_mul(&z2_100_0, t1, z2_50_0) /* 2^101 - 2^1 */ fe25519_square(&t1, z2_100_0) /* 2^102 - 2^2 */ fe25519_square(&t0, t1) - /* 2^200 - 2^100 */ for _ in stride(from: 2, to: 100, by: 2) { fe25519_square(&t1, t0); fe25519_square(&t0, t1) } + /* 2^200 - 2^100 */ for _ in stride(from: 2, to: 100, by: 2) { + fe25519_square(&t1, t0) + fe25519_square(&t0, t1) + } /* 2^200 - 2^0 */ fe25519_mul(&t1, t0, z2_100_0) /* 2^201 - 2^1 */ fe25519_square(&t0, t1) /* 2^202 - 2^2 */ fe25519_square(&t1, t0) - /* 2^250 - 2^50 */ for _ in stride(from: 2, to: 50, by: 2) { fe25519_square(&t0, t1); fe25519_square(&t1, t0) } + /* 2^250 - 2^50 */ for _ in stride(from: 2, to: 50, by: 2) { + fe25519_square(&t0, t1) + fe25519_square(&t1, t0) + } /* 2^250 - 2^0 */ fe25519_mul(&t0, t1, z2_50_0) /* 2^251 - 2^1 */ fe25519_square(&t1, t0) diff --git a/LiskKit/Sources/Crypto/ED25519/Ed25519ge.swift b/LiskKit/Sources/Crypto/ED25519/Ed25519ge.swift index 337af81d2..4b60a3db3 100644 --- a/LiskKit/Sources/Crypto/ED25519/Ed25519ge.swift +++ b/LiskKit/Sources/Crypto/ED25519/Ed25519ge.swift @@ -34,10 +34,10 @@ struct ge: CustomDebugStringConvertible { } init() { - x = fe() // zero - y = fe() // zero - z = fe() // zero - t = fe() // zero + x = fe() // zero + y = fe() // zero + z = fe() // zero + t = fe() // zero } init(_ x: fe, _ y: fe, _ z: fe, _ t: fe) { @@ -68,16 +68,22 @@ struct ge: CustomDebugStringConvertible { /* d */ private static let ecd: fe = - fe([0xA3, 0x78, 0x59, 0x13, 0xCA, 0x4D, 0xEB, 0x75, 0xAB, 0xD8, 0x41, 0x41, 0x4D, 0x0A, 0x70, 0x00, - 0x98, 0xE8, 0x79, 0x77, 0x79, 0x40, 0xC7, 0x8C, 0x73, 0xFE, 0x6F, 0x2B, 0xEE, 0x6C, 0x03, 0x52]) + fe([ + 0xA3, 0x78, 0x59, 0x13, 0xCA, 0x4D, 0xEB, 0x75, 0xAB, 0xD8, 0x41, 0x41, 0x4D, 0x0A, 0x70, 0x00, + 0x98, 0xE8, 0x79, 0x77, 0x79, 0x40, 0xC7, 0x8C, 0x73, 0xFE, 0x6F, 0x2B, 0xEE, 0x6C, 0x03, 0x52 + ]) /* 2*d */ private static let ec2d: fe = - fe([0x59, 0xF1, 0xB2, 0x26, 0x94, 0x9B, 0xD6, 0xEB, 0x56, 0xB1, 0x83, 0x82, 0x9A, 0x14, 0xE0, 0x00, - 0x30, 0xD1, 0xF3, 0xEE, 0xF2, 0x80, 0x8E, 0x19, 0xE7, 0xFC, 0xDF, 0x56, 0xDC, 0xD9, 0x06, 0x24]) + fe([ + 0x59, 0xF1, 0xB2, 0x26, 0x94, 0x9B, 0xD6, 0xEB, 0x56, 0xB1, 0x83, 0x82, 0x9A, 0x14, 0xE0, 0x00, + 0x30, 0xD1, 0xF3, 0xEE, 0xF2, 0x80, 0x8E, 0x19, 0xE7, 0xFC, 0xDF, 0x56, 0xDC, 0xD9, 0x06, 0x24 + ]) /* sqrt(-1) = 2^((p-1)/4) */ private static let sqrtm1: fe = - fe([0xB0, 0xA0, 0x0E, 0x4A, 0x27, 0x1B, 0xEE, 0xC4, 0x78, 0xE4, 0x2F, 0xAD, 0x06, 0x18, 0x43, 0x2F, - 0xA7, 0xD7, 0xFB, 0x3D, 0x99, 0x00, 0x4D, 0x2B, 0x0B, 0xDF, 0xC1, 0x4F, 0x80, 0x24, 0x83, 0x2B]) + fe([ + 0xB0, 0xA0, 0x0E, 0x4A, 0x27, 0x1B, 0xEE, 0xC4, 0x78, 0xE4, 0x2F, 0xAD, 0x06, 0x18, 0x43, 0x2F, + 0xA7, 0xD7, 0xFB, 0x3D, 0x99, 0x00, 0x4D, 0x2B, 0x0B, 0xDF, 0xC1, 0x4F, 0x80, 0x24, 0x83, 0x2B + ]) // intermediate point // ge.x = E F @@ -139,14 +145,23 @@ struct ge: CustomDebugStringConvertible { // = 46827403850823179245072216630277197565144205554125654976674165829533817101731 // = 0x67 * 2^31 + 0x87 * 2^30 + ... + 0xa3 * 2^0 static let ge25519_base: ge = ge( - fe([0x1A, 0xD5, 0x25, 0x8F, 0x60, 0x2D, 0x56, 0xC9, 0xB2, 0xA7, 0x25, 0x95, 0x60, 0xC7, 0x2C, 0x69, - 0x5C, 0xDC, 0xD6, 0xFD, 0x31, 0xE2, 0xA4, 0xC0, 0xFE, 0x53, 0x6E, 0xCD, 0xD3, 0x36, 0x69, 0x21]), - fe([0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, - 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0xA3, 0xDD, 0xB7, 0xA5, 0xB3, 0x8A, 0xDE, 0x6D, 0xF5, 0x52, 0x51, 0x77, 0x80, 0x9F, 0xF0, 0x20, - 0x7D, 0xE3, 0xAB, 0x64, 0x8E, 0x4E, 0xEA, 0x66, 0x65, 0x76, 0x8B, 0xD7, 0x0F, 0x5F, 0x87, 0x67])) + fe([ + 0x1A, 0xD5, 0x25, 0x8F, 0x60, 0x2D, 0x56, 0xC9, 0xB2, 0xA7, 0x25, 0x95, 0x60, 0xC7, 0x2C, 0x69, + 0x5C, 0xDC, 0xD6, 0xFD, 0x31, 0xE2, 0xA4, 0xC0, 0xFE, 0x53, 0x6E, 0xCD, 0xD3, 0x36, 0x69, 0x21 + ]), + fe([ + 0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, + 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0xA3, 0xDD, 0xB7, 0xA5, 0xB3, 0x8A, 0xDE, 0x6D, 0xF5, 0x52, 0x51, 0x77, 0x80, 0x9F, 0xF0, 0x20, + 0x7D, 0xE3, 0xAB, 0x64, 0x8E, 0x4E, 0xEA, 0x66, 0x65, 0x76, 0x8B, 0xD7, 0x0F, 0x5F, 0x87, 0x67 + ]) + ) // x = E F // y = H G @@ -214,22 +229,22 @@ struct ge: CustomDebugStringConvertible { fe.fe25519_sub(&a, p.y, p.x) /* a = y1-x1 */ fe.fe25519_sub(&t, q.y, q.x) /* t = y2-x2 */ - fe.fe25519_mul(&a, a, t) /* A = (y1-x1)(y2-x2) */ + fe.fe25519_mul(&a, a, t) /* A = (y1-x1)(y2-x2) */ fe.fe25519_add(&b, p.x, p.y) /* b = x1+y1 */ fe.fe25519_add(&t, q.x, q.y) /* t = x2+y2 */ - fe.fe25519_mul(&b, b, t) /* B = (x1+y1)(x2+y2) */ + fe.fe25519_mul(&b, b, t) /* B = (x1+y1)(x2+y2) */ - fe.fe25519_mul(&c, p.t, q.t) /* c = t1 t2 */ + fe.fe25519_mul(&c, p.t, q.t) /* c = t1 t2 */ fe.fe25519_mul(&c, c, ge.ec2d) /* C = t1 t2 2 d */ - fe.fe25519_mul(&d, p.z, q.z) /* d = z1 z2 */ - fe.fe25519_add(&d, d, d) /* D = 2 z1 z2 */ + fe.fe25519_mul(&d, p.z, q.z) /* d = z1 z2 */ + fe.fe25519_add(&d, d, d) /* D = 2 z1 z2 */ - fe.fe25519_sub(&r.e, b, a) /* E = B-A */ - fe.fe25519_sub(&r.f, d, c) /* F = D-C */ - fe.fe25519_add(&r.g, d, c) /* G = D+C */ - fe.fe25519_add(&r.h, b, a) /* H = B+A */ + fe.fe25519_sub(&r.e, b, a) /* E = B-A */ + fe.fe25519_sub(&r.f, d, c) /* F = D-C */ + fe.fe25519_add(&r.g, d, c) /* G = D+C */ + fe.fe25519_add(&r.h, b, a) /* H = B+A */ } // r = 2 * p @@ -242,16 +257,16 @@ struct ge: CustomDebugStringConvertible { fe.fe25519_square(&a, proj.x) /* A = x^2 */ fe.fe25519_square(&b, proj.y) /* B = y^2 */ fe.fe25519_square(&c, proj.z) /* c = z^2 */ - fe.fe25519_add(&c, c, c) /* C = 2 z^2 */ - fe.fe25519_neg(&d, a) /* D = - x^2 */ + fe.fe25519_add(&c, c, c) /* C = 2 z^2 */ + fe.fe25519_neg(&d, a) /* D = - x^2 */ fe.fe25519_add(&r.e, proj.x, proj.y) /* e = x+y */ - fe.fe25519_square(&r.e, r.e) /* e = (x+y)^2 */ - fe.fe25519_sub(&r.e, r.e, a) /* e = (x+y)^2 - x^2 */ - fe.fe25519_sub(&r.e, r.e, b) /* E = (x+y)^2 - x^2 - y^2 = 2xy */ - fe.fe25519_add(&r.g, d, b) /* G = - x^2 + y^2 */ - fe.fe25519_sub(&r.f, r.g, c) /* F = - x^2 + y^2 - 2 z^2 */ - fe.fe25519_sub(&r.h, d, b) /* H = - x^2 - y^2 */ + fe.fe25519_square(&r.e, r.e) /* e = (x+y)^2 */ + fe.fe25519_sub(&r.e, r.e, a) /* e = (x+y)^2 - x^2 */ + fe.fe25519_sub(&r.e, r.e, b) /* E = (x+y)^2 - x^2 - y^2 = 2xy */ + fe.fe25519_add(&r.g, d, b) /* G = - x^2 + y^2 */ + fe.fe25519_sub(&r.f, r.g, c) /* F = - x^2 + y^2 - 2 z^2 */ + fe.fe25519_sub(&r.h, d, b) /* H = - x^2 - y^2 */ } /* Constant-time version of: if(b) r = p */ @@ -276,11 +291,11 @@ struct ge: CustomDebugStringConvertible { private static func choose_t(_ t: inout aff, _ pos: Int, _ b: Int8) { /* constant time */ var v = fe() - t = ge25519_base_multiples_affine.shared.v[5*pos+0] - cmov_aff(&t, ge25519_base_multiples_affine.shared.v[5*pos+1], equal(b, 1) | equal(b, -1)) - cmov_aff(&t, ge25519_base_multiples_affine.shared.v[5*pos+2], equal(b, 2) | equal(b, -2)) - cmov_aff(&t, ge25519_base_multiples_affine.shared.v[5*pos+3], equal(b, 3) | equal(b, -3)) - cmov_aff(&t, ge25519_base_multiples_affine.shared.v[5*pos+4], equal(b, -4)) + t = ge25519_base_multiples_affine.shared.v[5 * pos + 0] + cmov_aff(&t, ge25519_base_multiples_affine.shared.v[5 * pos + 1], equal(b, 1) | equal(b, -1)) + cmov_aff(&t, ge25519_base_multiples_affine.shared.v[5 * pos + 2], equal(b, 2) | equal(b, -2)) + cmov_aff(&t, ge25519_base_multiples_affine.shared.v[5 * pos + 3], equal(b, 3) | equal(b, -3)) + cmov_aff(&t, ge25519_base_multiples_affine.shared.v[5 * pos + 4], equal(b, -4)) fe.fe25519_neg(&v, t.x) fe.fe25519_cmov(&t.x, v, negative(b)) } @@ -314,7 +329,7 @@ struct ge: CustomDebugStringConvertible { fe.fe25519_setone(&r.z) par = p[31] >> 7 assert(par == 0 || par == 1) - fe.fe25519_unpack(&r.y, p) // parity is omited + fe.fe25519_unpack(&r.y, p) // parity is omited fe.fe25519_square(&u, r.y) /* u = y^2 */ fe.fe25519_mul(&v, u, ge.ecd) /* v = dy^2 */ fe.fe25519_sub(&u, u, r.z) /* u = y^2-1 */ @@ -325,17 +340,17 @@ struct ge: CustomDebugStringConvertible { fe.fe25519_mul(&den6, den4, den2) fe.fe25519_mul(&t, den6, u) fe.fe25519_mul(&t, t, v) - fe.fe25519_pow2523(&t, t) // t = (uv^7)^((q-5)/8) + fe.fe25519_pow2523(&t, t) // t = (uv^7)^((q-5)/8) - fe.fe25519_mul(&t, t, u) // t = u (uv^7)^((q-5)/8) + fe.fe25519_mul(&t, t, u) // t = u (uv^7)^((q-5)/8) fe.fe25519_mul(&t, t, v) fe.fe25519_mul(&t, t, v) - fe.fe25519_mul(&r.x, t, v) // t = uv^3 (uv^7)^((q-5)/8) + fe.fe25519_mul(&r.x, t, v) // t = uv^3 (uv^7)^((q-5)/8) // x^2 = u/v // x^2 v = u - fe.fe25519_square(&chk, r.x) // (r.x)^2 - fe.fe25519_mul(&chk, chk, v) // v (r.x)^2 + fe.fe25519_square(&chk, r.x) // (r.x)^2 + fe.fe25519_mul(&chk, chk, v) // v (r.x)^2 if !fe.fe25519_iseq_vartime(chk, u) { fe.fe25519_mul(&r.x, r.x, ge.sqrtm1) } @@ -348,7 +363,7 @@ struct ge: CustomDebugStringConvertible { } // @warning negated - if fe.fe25519_getparity(r.x) != (1-par) { + if fe.fe25519_getparity(r.x) != (1 - par) { fe.fe25519_neg(&r.x, r.x) } @@ -401,33 +416,46 @@ struct ge: CustomDebugStringConvertible { // 00 01 pre[1] = p1 // 00 10 - ge.dbl_p1p1(&tp1p1, p1.toProj); ge.p1p1_to_ge(&pre[2], tp1p1) + ge.dbl_p1p1(&tp1p1, p1.toProj) + ge.p1p1_to_ge(&pre[2], tp1p1) // 00 11 - ge.add_p1p1(&tp1p1, pre[1], pre[2]); ge.p1p1_to_ge(&pre[3], tp1p1) + ge.add_p1p1(&tp1p1, pre[1], pre[2]) + ge.p1p1_to_ge(&pre[3], tp1p1) // 01 00 pre[4] = p2 // 01 01 - ge.add_p1p1(&tp1p1, pre[1], pre[4]); ge.p1p1_to_ge(&pre[5], tp1p1) + ge.add_p1p1(&tp1p1, pre[1], pre[4]) + ge.p1p1_to_ge(&pre[5], tp1p1) // 01 10 - ge.add_p1p1(&tp1p1, pre[2], pre[4]); ge.p1p1_to_ge(&pre[6], tp1p1) + ge.add_p1p1(&tp1p1, pre[2], pre[4]) + ge.p1p1_to_ge(&pre[6], tp1p1) // 01 11 - ge.add_p1p1(&tp1p1, pre[3], pre[4]); ge.p1p1_to_ge(&pre[7], tp1p1) + ge.add_p1p1(&tp1p1, pre[3], pre[4]) + ge.p1p1_to_ge(&pre[7], tp1p1) // 10 00 - ge.dbl_p1p1(&tp1p1, p2.toProj); ge.p1p1_to_ge(&pre[8], tp1p1) + ge.dbl_p1p1(&tp1p1, p2.toProj) + ge.p1p1_to_ge(&pre[8], tp1p1) // 10 01 - ge.add_p1p1(&tp1p1, pre[1], pre[8]); ge.p1p1_to_ge(&pre[9], tp1p1) + ge.add_p1p1(&tp1p1, pre[1], pre[8]) + ge.p1p1_to_ge(&pre[9], tp1p1) // 10 10 - ge.dbl_p1p1(&tp1p1, pre[5].toProj); ge.p1p1_to_ge(&pre[10], tp1p1) + ge.dbl_p1p1(&tp1p1, pre[5].toProj) + ge.p1p1_to_ge(&pre[10], tp1p1) // 10 11 - ge.add_p1p1(&tp1p1, pre[3], pre[8]); ge.p1p1_to_ge(&pre[11], tp1p1) + ge.add_p1p1(&tp1p1, pre[3], pre[8]) + ge.p1p1_to_ge(&pre[11], tp1p1) // 11 00 - ge.add_p1p1(&tp1p1, pre[4], pre[8]); ge.p1p1_to_ge(&pre[12], tp1p1) + ge.add_p1p1(&tp1p1, pre[4], pre[8]) + ge.p1p1_to_ge(&pre[12], tp1p1) // 11 01 - ge.add_p1p1(&tp1p1, pre[1], pre[12]); ge.p1p1_to_ge(&pre[13], tp1p1) + ge.add_p1p1(&tp1p1, pre[1], pre[12]) + ge.p1p1_to_ge(&pre[13], tp1p1) // 11 10 - ge.add_p1p1(&tp1p1, pre[2], pre[12]); ge.p1p1_to_ge(&pre[14], tp1p1) + ge.add_p1p1(&tp1p1, pre[2], pre[12]) + ge.p1p1_to_ge(&pre[14], tp1p1) // 11 11 - ge.add_p1p1(&tp1p1, pre[3], pre[12]); ge.p1p1_to_ge(&pre[15], tp1p1) + ge.add_p1p1(&tp1p1, pre[3], pre[12]) + ge.p1p1_to_ge(&pre[15], tp1p1) var b = [UInt8](repeating: 0, count: 127) sc.sc25519_2interleave2(&b, s1, s2) @@ -481,2554 +509,5103 @@ struct ge: CustomDebugStringConvertible { // 425 = 85 * 5 private init() { v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x1a, 0xd5, 0x25, 0x8f, 0x60, 0x2d, 0x56, 0xc9, 0xb2, 0xa7, 0x25, 0x95, 0x60, 0xc7, 0x2c, 0x69, - 0x5c, 0xdc, 0xd6, 0xfd, 0x31, 0xe2, 0xa4, 0xc0, 0xfe, 0x53, 0x6e, 0xcd, 0xd3, 0x36, 0x69, 0x21]), - fe([0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, - 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66])) + aff( + fe([ + 0x1a, 0xd5, 0x25, 0x8f, 0x60, 0x2d, 0x56, 0xc9, 0xb2, 0xa7, 0x25, 0x95, 0x60, 0xc7, 0x2c, 0x69, + 0x5c, 0xdc, 0xd6, 0xfd, 0x31, 0xe2, 0xa4, 0xc0, 0xfe, 0x53, 0x6e, 0xcd, 0xd3, 0x36, 0x69, 0x21 + ]), + fe([ + 0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, + 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66 + ]) + ) ) v.append( - aff(fe([0x0e, 0xce, 0x43, 0x28, 0x4e, 0xa1, 0xc5, 0x83, 0x5f, 0xa4, 0xd7, 0x15, 0x45, 0x8e, 0x0d, 0x08, - 0xac, 0xe7, 0x33, 0x18, 0x7d, 0x3b, 0x04, 0x3d, 0x6c, 0x04, 0x5a, 0x9f, 0x4c, 0x38, 0xab, 0x36]), - fe([0xc9, 0xa3, 0xf8, 0x6a, 0xae, 0x46, 0x5f, 0x0e, 0x56, 0x51, 0x38, 0x64, 0x51, 0x0f, 0x39, 0x97, - 0x56, 0x1f, 0xa2, 0xc9, 0xe8, 0x5e, 0xa2, 0x1d, 0xc2, 0x29, 0x23, 0x09, 0xf3, 0xcd, 0x60, 0x22])) + aff( + fe([ + 0x0e, 0xce, 0x43, 0x28, 0x4e, 0xa1, 0xc5, 0x83, 0x5f, 0xa4, 0xd7, 0x15, 0x45, 0x8e, 0x0d, 0x08, + 0xac, 0xe7, 0x33, 0x18, 0x7d, 0x3b, 0x04, 0x3d, 0x6c, 0x04, 0x5a, 0x9f, 0x4c, 0x38, 0xab, 0x36 + ]), + fe([ + 0xc9, 0xa3, 0xf8, 0x6a, 0xae, 0x46, 0x5f, 0x0e, 0x56, 0x51, 0x38, 0x64, 0x51, 0x0f, 0x39, 0x97, + 0x56, 0x1f, 0xa2, 0xc9, 0xe8, 0x5e, 0xa2, 0x1d, 0xc2, 0x29, 0x23, 0x09, 0xf3, 0xcd, 0x60, 0x22 + ]) + ) ) v.append( - aff(fe([0x5c, 0xe2, 0xf8, 0xd3, 0x5f, 0x48, 0x62, 0xac, 0x86, 0x48, 0x62, 0x81, 0x19, 0x98, 0x43, 0x63, - 0x3a, 0xc8, 0xda, 0x3e, 0x74, 0xae, 0xf4, 0x1f, 0x49, 0x8f, 0x92, 0x22, 0x4a, 0x9c, 0xae, 0x67]) , - fe([0xd4, 0xb4, 0xf5, 0x78, 0x48, 0x68, 0xc3, 0x02, 0x04, 0x03, 0x24, 0x67, 0x17, 0xec, 0x16, 0x9f, - 0xf7, 0x9e, 0x26, 0x60, 0x8e, 0xa1, 0x26, 0xa1, 0xab, 0x69, 0xee, 0x77, 0xd1, 0xb1, 0x67, 0x12])) + aff( + fe([ + 0x5c, 0xe2, 0xf8, 0xd3, 0x5f, 0x48, 0x62, 0xac, 0x86, 0x48, 0x62, 0x81, 0x19, 0x98, 0x43, 0x63, + 0x3a, 0xc8, 0xda, 0x3e, 0x74, 0xae, 0xf4, 0x1f, 0x49, 0x8f, 0x92, 0x22, 0x4a, 0x9c, 0xae, 0x67 + ]), + fe([ + 0xd4, 0xb4, 0xf5, 0x78, 0x48, 0x68, 0xc3, 0x02, 0x04, 0x03, 0x24, 0x67, 0x17, 0xec, 0x16, 0x9f, + 0xf7, 0x9e, 0x26, 0x60, 0x8e, 0xa1, 0x26, 0xa1, 0xab, 0x69, 0xee, 0x77, 0xd1, 0xb1, 0x67, 0x12 + ]) + ) ) v.append( - aff(fe([0x70, 0xf8, 0xc9, 0xc4, 0x57, 0xa6, 0x3a, 0x49, 0x47, 0x15, 0xce, 0x93, 0xc1, 0x9e, 0x73, 0x1a, - 0xf9, 0x20, 0x35, 0x7a, 0xb8, 0xd4, 0x25, 0x83, 0x46, 0xf1, 0xcf, 0x56, 0xdb, 0xa8, 0x3d, 0x20]) , - fe([0x2f, 0x11, 0x32, 0xca, 0x61, 0xab, 0x38, 0xdf, 0xf0, 0x0f, 0x2f, 0xea, 0x32, 0x28, 0xf2, 0x4c, - 0x6c, 0x71, 0xd5, 0x80, 0x85, 0xb8, 0x0e, 0x47, 0xe1, 0x95, 0x15, 0xcb, 0x27, 0xe8, 0xd0, 0x47])) + aff( + fe([ + 0x70, 0xf8, 0xc9, 0xc4, 0x57, 0xa6, 0x3a, 0x49, 0x47, 0x15, 0xce, 0x93, 0xc1, 0x9e, 0x73, 0x1a, + 0xf9, 0x20, 0x35, 0x7a, 0xb8, 0xd4, 0x25, 0x83, 0x46, 0xf1, 0xcf, 0x56, 0xdb, 0xa8, 0x3d, 0x20 + ]), + fe([ + 0x2f, 0x11, 0x32, 0xca, 0x61, 0xab, 0x38, 0xdf, 0xf0, 0x0f, 0x2f, 0xea, 0x32, 0x28, 0xf2, 0x4c, + 0x6c, 0x71, 0xd5, 0x80, 0x85, 0xb8, 0x0e, 0x47, 0xe1, 0x95, 0x15, 0xcb, 0x27, 0xe8, 0xd0, 0x47 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xc8, 0x84, 0xa5, 0x08, 0xbc, 0xfd, 0x87, 0x3b, 0x99, 0x8b, 0x69, 0x80, 0x7b, 0xc6, 0x3a, 0xeb, - 0x93, 0xcf, 0x4e, 0xf8, 0x5c, 0x2d, 0x86, 0x42, 0xb6, 0x71, 0xd7, 0x97, 0x5f, 0xe1, 0x42, 0x67]) , - fe([0xb4, 0xb9, 0x37, 0xfc, 0xa9, 0x5b, 0x2f, 0x1e, 0x93, 0xe4, 0x1e, 0x62, 0xfc, 0x3c, 0x78, 0x81, - 0x8f, 0xf3, 0x8a, 0x66, 0x09, 0x6f, 0xad, 0x6e, 0x79, 0x73, 0xe5, 0xc9, 0x00, 0x06, 0xd3, 0x21])) + aff( + fe([ + 0xc8, 0x84, 0xa5, 0x08, 0xbc, 0xfd, 0x87, 0x3b, 0x99, 0x8b, 0x69, 0x80, 0x7b, 0xc6, 0x3a, 0xeb, + 0x93, 0xcf, 0x4e, 0xf8, 0x5c, 0x2d, 0x86, 0x42, 0xb6, 0x71, 0xd7, 0x97, 0x5f, 0xe1, 0x42, 0x67 + ]), + fe([ + 0xb4, 0xb9, 0x37, 0xfc, 0xa9, 0x5b, 0x2f, 0x1e, 0x93, 0xe4, 0x1e, 0x62, 0xfc, 0x3c, 0x78, 0x81, + 0x8f, 0xf3, 0x8a, 0x66, 0x09, 0x6f, 0xad, 0x6e, 0x79, 0x73, 0xe5, 0xc9, 0x00, 0x06, 0xd3, 0x21 + ]) + ) ) v.append( - aff(fe([0xf8, 0xf9, 0x28, 0x6c, 0x6d, 0x59, 0xb2, 0x59, 0x74, 0x23, 0xbf, 0xe7, 0x33, 0x8d, 0x57, 0x09, - 0x91, 0x9c, 0x24, 0x08, 0x15, 0x2b, 0xe2, 0xb8, 0xee, 0x3a, 0xe5, 0x27, 0x06, 0x86, 0xa4, 0x23]) , - fe([0xeb, 0x27, 0x67, 0xc1, 0x37, 0xab, 0x7a, 0xd8, 0x27, 0x9c, 0x07, 0x8e, 0xff, 0x11, 0x6a, 0xb0, - 0x78, 0x6e, 0xad, 0x3a, 0x2e, 0x0f, 0x98, 0x9f, 0x72, 0xc3, 0x7f, 0x82, 0xf2, 0x96, 0x96, 0x70])) + aff( + fe([ + 0xf8, 0xf9, 0x28, 0x6c, 0x6d, 0x59, 0xb2, 0x59, 0x74, 0x23, 0xbf, 0xe7, 0x33, 0x8d, 0x57, 0x09, + 0x91, 0x9c, 0x24, 0x08, 0x15, 0x2b, 0xe2, 0xb8, 0xee, 0x3a, 0xe5, 0x27, 0x06, 0x86, 0xa4, 0x23 + ]), + fe([ + 0xeb, 0x27, 0x67, 0xc1, 0x37, 0xab, 0x7a, 0xd8, 0x27, 0x9c, 0x07, 0x8e, 0xff, 0x11, 0x6a, 0xb0, + 0x78, 0x6e, 0xad, 0x3a, 0x2e, 0x0f, 0x98, 0x9f, 0x72, 0xc3, 0x7f, 0x82, 0xf2, 0x96, 0x96, 0x70 + ]) + ) ) v.append( - aff(fe([0x81, 0x6b, 0x88, 0xe8, 0x1e, 0xc7, 0x77, 0x96, 0x0e, 0xa1, 0xa9, 0x52, 0xe0, 0xd8, 0x0e, 0x61, - 0x9e, 0x79, 0x2d, 0x95, 0x9c, 0x8d, 0x96, 0xe0, 0x06, 0x40, 0x5d, 0x87, 0x28, 0x5f, 0x98, 0x70]) , - fe([0xf1, 0x79, 0x7b, 0xed, 0x4f, 0x44, 0xb2, 0xe7, 0x08, 0x0d, 0xc2, 0x08, 0x12, 0xd2, 0x9f, 0xdf, - 0xcd, 0x93, 0x20, 0x8a, 0xcf, 0x33, 0xca, 0x6d, 0x89, 0xb9, 0x77, 0xc8, 0x93, 0x1b, 0x4e, 0x60])) + aff( + fe([ + 0x81, 0x6b, 0x88, 0xe8, 0x1e, 0xc7, 0x77, 0x96, 0x0e, 0xa1, 0xa9, 0x52, 0xe0, 0xd8, 0x0e, 0x61, + 0x9e, 0x79, 0x2d, 0x95, 0x9c, 0x8d, 0x96, 0xe0, 0x06, 0x40, 0x5d, 0x87, 0x28, 0x5f, 0x98, 0x70 + ]), + fe([ + 0xf1, 0x79, 0x7b, 0xed, 0x4f, 0x44, 0xb2, 0xe7, 0x08, 0x0d, 0xc2, 0x08, 0x12, 0xd2, 0x9f, 0xdf, + 0xcd, 0x93, 0x20, 0x8a, 0xcf, 0x33, 0xca, 0x6d, 0x89, 0xb9, 0x77, 0xc8, 0x93, 0x1b, 0x4e, 0x60 + ]) + ) ) v.append( - aff(fe([0x26, 0x4f, 0x7e, 0x97, 0xf6, 0x40, 0xdd, 0x4f, 0xfc, 0x52, 0x78, 0xf9, 0x90, 0x31, 0x03, 0xe6, - 0x7d, 0x56, 0x39, 0x0b, 0x1d, 0x56, 0x82, 0x85, 0xf9, 0x1a, 0x42, 0x17, 0x69, 0x6c, 0xcf, 0x39]) , - fe([0x69, 0xd2, 0x06, 0x3a, 0x4f, 0x39, 0x2d, 0xf9, 0x38, 0x40, 0x8c, 0x4c, 0xe7, 0x05, 0x12, 0xb4, - 0x78, 0x8b, 0xf8, 0xc0, 0xec, 0x93, 0xde, 0x7a, 0x6b, 0xce, 0x2c, 0xe1, 0x0e, 0xa9, 0x34, 0x44])) + aff( + fe([ + 0x26, 0x4f, 0x7e, 0x97, 0xf6, 0x40, 0xdd, 0x4f, 0xfc, 0x52, 0x78, 0xf9, 0x90, 0x31, 0x03, 0xe6, + 0x7d, 0x56, 0x39, 0x0b, 0x1d, 0x56, 0x82, 0x85, 0xf9, 0x1a, 0x42, 0x17, 0x69, 0x6c, 0xcf, 0x39 + ]), + fe([ + 0x69, 0xd2, 0x06, 0x3a, 0x4f, 0x39, 0x2d, 0xf9, 0x38, 0x40, 0x8c, 0x4c, 0xe7, 0x05, 0x12, 0xb4, + 0x78, 0x8b, 0xf8, 0xc0, 0xec, 0x93, 0xde, 0x7a, 0x6b, 0xce, 0x2c, 0xe1, 0x0e, 0xa9, 0x34, 0x44 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x0b, 0xa4, 0x3c, 0xb0, 0x0f, 0x7a, 0x51, 0xf1, 0x78, 0xd6, 0xd9, 0x6a, 0xfd, 0x46, 0xe8, 0xb8, - 0xa8, 0x79, 0x1d, 0x87, 0xf9, 0x90, 0xf2, 0x9c, 0x13, 0x29, 0xf8, 0x0b, 0x20, 0x64, 0xfa, 0x05]) , - fe([0x26, 0x09, 0xda, 0x17, 0xaf, 0x95, 0xd6, 0xfb, 0x6a, 0x19, 0x0d, 0x6e, 0x5e, 0x12, 0xf1, 0x99, - 0x4c, 0xaa, 0xa8, 0x6f, 0x79, 0x86, 0xf4, 0x72, 0x28, 0x00, 0x26, 0xf9, 0xea, 0x9e, 0x19, 0x3d])) + aff( + fe([ + 0x0b, 0xa4, 0x3c, 0xb0, 0x0f, 0x7a, 0x51, 0xf1, 0x78, 0xd6, 0xd9, 0x6a, 0xfd, 0x46, 0xe8, 0xb8, + 0xa8, 0x79, 0x1d, 0x87, 0xf9, 0x90, 0xf2, 0x9c, 0x13, 0x29, 0xf8, 0x0b, 0x20, 0x64, 0xfa, 0x05 + ]), + fe([ + 0x26, 0x09, 0xda, 0x17, 0xaf, 0x95, 0xd6, 0xfb, 0x6a, 0x19, 0x0d, 0x6e, 0x5e, 0x12, 0xf1, 0x99, + 0x4c, 0xaa, 0xa8, 0x6f, 0x79, 0x86, 0xf4, 0x72, 0x28, 0x00, 0x26, 0xf9, 0xea, 0x9e, 0x19, 0x3d + ]) + ) ) v.append( - aff(fe([0x87, 0xdd, 0xcf, 0xf0, 0x5b, 0x49, 0xa2, 0x5d, 0x40, 0x7a, 0x23, 0x26, 0xa4, 0x7a, 0x83, 0x8a, - 0xb7, 0x8b, 0xd2, 0x1a, 0xbf, 0xea, 0x02, 0x24, 0x08, 0x5f, 0x7b, 0xa9, 0xb1, 0xbe, 0x9d, 0x37]) , - fe([0xfc, 0x86, 0x4b, 0x08, 0xee, 0xe7, 0xa0, 0xfd, 0x21, 0x45, 0x09, 0x34, 0xc1, 0x61, 0x32, 0x23, - 0xfc, 0x9b, 0x55, 0x48, 0x53, 0x99, 0xf7, 0x63, 0xd0, 0x99, 0xce, 0x01, 0xe0, 0x9f, 0xeb, 0x28])) + aff( + fe([ + 0x87, 0xdd, 0xcf, 0xf0, 0x5b, 0x49, 0xa2, 0x5d, 0x40, 0x7a, 0x23, 0x26, 0xa4, 0x7a, 0x83, 0x8a, + 0xb7, 0x8b, 0xd2, 0x1a, 0xbf, 0xea, 0x02, 0x24, 0x08, 0x5f, 0x7b, 0xa9, 0xb1, 0xbe, 0x9d, 0x37 + ]), + fe([ + 0xfc, 0x86, 0x4b, 0x08, 0xee, 0xe7, 0xa0, 0xfd, 0x21, 0x45, 0x09, 0x34, 0xc1, 0x61, 0x32, 0x23, + 0xfc, 0x9b, 0x55, 0x48, 0x53, 0x99, 0xf7, 0x63, 0xd0, 0x99, 0xce, 0x01, 0xe0, 0x9f, 0xeb, 0x28 + ]) + ) ) v.append( - aff(fe([0x47, 0xfc, 0xab, 0x5a, 0x17, 0xf0, 0x85, 0x56, 0x3a, 0x30, 0x86, 0x20, 0x28, 0x4b, 0x8e, 0x44, - 0x74, 0x3a, 0x6e, 0x02, 0xf1, 0x32, 0x8f, 0x9f, 0x3f, 0x08, 0x35, 0xe9, 0xca, 0x16, 0x5f, 0x6e]) , - fe([0x1c, 0x59, 0x1c, 0x65, 0x5d, 0x34, 0xa4, 0x09, 0xcd, 0x13, 0x9c, 0x70, 0x7d, 0xb1, 0x2a, 0xc5, - 0x88, 0xaf, 0x0b, 0x60, 0xc7, 0x9f, 0x34, 0x8d, 0xd6, 0xb7, 0x7f, 0xea, 0x78, 0x65, 0x8d, 0x77])) + aff( + fe([ + 0x47, 0xfc, 0xab, 0x5a, 0x17, 0xf0, 0x85, 0x56, 0x3a, 0x30, 0x86, 0x20, 0x28, 0x4b, 0x8e, 0x44, + 0x74, 0x3a, 0x6e, 0x02, 0xf1, 0x32, 0x8f, 0x9f, 0x3f, 0x08, 0x35, 0xe9, 0xca, 0x16, 0x5f, 0x6e + ]), + fe([ + 0x1c, 0x59, 0x1c, 0x65, 0x5d, 0x34, 0xa4, 0x09, 0xcd, 0x13, 0x9c, 0x70, 0x7d, 0xb1, 0x2a, 0xc5, + 0x88, 0xaf, 0x0b, 0x60, 0xc7, 0x9f, 0x34, 0x8d, 0xd6, 0xb7, 0x7f, 0xea, 0x78, 0x65, 0x8d, 0x77 + ]) + ) ) v.append( - aff(fe([0x56, 0xa5, 0xc2, 0x0c, 0xdd, 0xbc, 0xb8, 0x20, 0x6d, 0x57, 0x61, 0xb5, 0xfb, 0x78, 0xb5, 0xd4, - 0x49, 0x54, 0x90, 0x26, 0xc1, 0xcb, 0xe9, 0xe6, 0xbf, 0xec, 0x1d, 0x4e, 0xed, 0x07, 0x7e, 0x5e]) , - fe([0xc7, 0xf6, 0x6c, 0x56, 0x31, 0x20, 0x14, 0x0e, 0xa8, 0xd9, 0x27, 0xc1, 0x9a, 0x3d, 0x1b, 0x7d, - 0x0e, 0x26, 0xd3, 0x81, 0xaa, 0xeb, 0xf5, 0x6b, 0x79, 0x02, 0xf1, 0x51, 0x5c, 0x75, 0x55, 0x0f])) + aff( + fe([ + 0x56, 0xa5, 0xc2, 0x0c, 0xdd, 0xbc, 0xb8, 0x20, 0x6d, 0x57, 0x61, 0xb5, 0xfb, 0x78, 0xb5, 0xd4, + 0x49, 0x54, 0x90, 0x26, 0xc1, 0xcb, 0xe9, 0xe6, 0xbf, 0xec, 0x1d, 0x4e, 0xed, 0x07, 0x7e, 0x5e + ]), + fe([ + 0xc7, 0xf6, 0x6c, 0x56, 0x31, 0x20, 0x14, 0x0e, 0xa8, 0xd9, 0x27, 0xc1, 0x9a, 0x3d, 0x1b, 0x7d, + 0x0e, 0x26, 0xd3, 0x81, 0xaa, 0xeb, 0xf5, 0x6b, 0x79, 0x02, 0xf1, 0x51, 0x5c, 0x75, 0x55, 0x0f + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x0a, 0x34, 0xcd, 0x82, 0x3c, 0x33, 0x09, 0x54, 0xd2, 0x61, 0x39, 0x30, 0x9b, 0xfd, 0xef, 0x21, - 0x26, 0xd4, 0x70, 0xfa, 0xee, 0xf9, 0x31, 0x33, 0x73, 0x84, 0xd0, 0xb3, 0x81, 0xbf, 0xec, 0x2e]) , - fe([0xe8, 0x93, 0x8b, 0x00, 0x64, 0xf7, 0x9c, 0xb8, 0x74, 0xe0, 0xe6, 0x49, 0x48, 0x4d, 0x4d, 0x48, - 0xb6, 0x19, 0xa1, 0x40, 0xb7, 0xd9, 0x32, 0x41, 0x7c, 0x82, 0x37, 0xa1, 0x2d, 0xdc, 0xd2, 0x54])) + aff( + fe([ + 0x0a, 0x34, 0xcd, 0x82, 0x3c, 0x33, 0x09, 0x54, 0xd2, 0x61, 0x39, 0x30, 0x9b, 0xfd, 0xef, 0x21, + 0x26, 0xd4, 0x70, 0xfa, 0xee, 0xf9, 0x31, 0x33, 0x73, 0x84, 0xd0, 0xb3, 0x81, 0xbf, 0xec, 0x2e + ]), + fe([ + 0xe8, 0x93, 0x8b, 0x00, 0x64, 0xf7, 0x9c, 0xb8, 0x74, 0xe0, 0xe6, 0x49, 0x48, 0x4d, 0x4d, 0x48, + 0xb6, 0x19, 0xa1, 0x40, 0xb7, 0xd9, 0x32, 0x41, 0x7c, 0x82, 0x37, 0xa1, 0x2d, 0xdc, 0xd2, 0x54 + ]) + ) ) v.append( - aff(fe([0x68, 0x2b, 0x4a, 0x5b, 0xd5, 0xc7, 0x51, 0x91, 0x1d, 0xe1, 0x2a, 0x4b, 0xc4, 0x47, 0xf1, 0xbc, - 0x7a, 0xb3, 0xcb, 0xc8, 0xb6, 0x7c, 0xac, 0x90, 0x05, 0xfd, 0xf3, 0xf9, 0x52, 0x3a, 0x11, 0x6b]) , - fe([0x3d, 0xc1, 0x27, 0xf3, 0x59, 0x43, 0x95, 0x90, 0xc5, 0x96, 0x79, 0xf5, 0xf4, 0x95, 0x65, 0x29, - 0x06, 0x9c, 0x51, 0x05, 0x18, 0xda, 0xb8, 0x2e, 0x79, 0x7e, 0x69, 0x59, 0x71, 0x01, 0xeb, 0x1a])) + aff( + fe([ + 0x68, 0x2b, 0x4a, 0x5b, 0xd5, 0xc7, 0x51, 0x91, 0x1d, 0xe1, 0x2a, 0x4b, 0xc4, 0x47, 0xf1, 0xbc, + 0x7a, 0xb3, 0xcb, 0xc8, 0xb6, 0x7c, 0xac, 0x90, 0x05, 0xfd, 0xf3, 0xf9, 0x52, 0x3a, 0x11, 0x6b + ]), + fe([ + 0x3d, 0xc1, 0x27, 0xf3, 0x59, 0x43, 0x95, 0x90, 0xc5, 0x96, 0x79, 0xf5, 0xf4, 0x95, 0x65, 0x29, + 0x06, 0x9c, 0x51, 0x05, 0x18, 0xda, 0xb8, 0x2e, 0x79, 0x7e, 0x69, 0x59, 0x71, 0x01, 0xeb, 0x1a + ]) + ) ) v.append( - aff(fe([0x15, 0x06, 0x49, 0xb6, 0x8a, 0x3c, 0xea, 0x2f, 0x34, 0x20, 0x14, 0xc3, 0xaa, 0xd6, 0xaf, 0x2c, - 0x3e, 0xbd, 0x65, 0x20, 0xe2, 0x4d, 0x4b, 0x3b, 0xeb, 0x9f, 0x4a, 0xc3, 0xad, 0xa4, 0x3b, 0x60]) , - fe([0xbc, 0x58, 0xe6, 0xc0, 0x95, 0x2a, 0x2a, 0x81, 0x9a, 0x7a, 0xf3, 0xd2, 0x06, 0xbe, 0x48, 0xbc, - 0x0c, 0xc5, 0x46, 0xe0, 0x6a, 0xd4, 0xac, 0x0f, 0xd9, 0xcc, 0x82, 0x34, 0x2c, 0xaf, 0xdb, 0x1f])) + aff( + fe([ + 0x15, 0x06, 0x49, 0xb6, 0x8a, 0x3c, 0xea, 0x2f, 0x34, 0x20, 0x14, 0xc3, 0xaa, 0xd6, 0xaf, 0x2c, + 0x3e, 0xbd, 0x65, 0x20, 0xe2, 0x4d, 0x4b, 0x3b, 0xeb, 0x9f, 0x4a, 0xc3, 0xad, 0xa4, 0x3b, 0x60 + ]), + fe([ + 0xbc, 0x58, 0xe6, 0xc0, 0x95, 0x2a, 0x2a, 0x81, 0x9a, 0x7a, 0xf3, 0xd2, 0x06, 0xbe, 0x48, 0xbc, + 0x0c, 0xc5, 0x46, 0xe0, 0x6a, 0xd4, 0xac, 0x0f, 0xd9, 0xcc, 0x82, 0x34, 0x2c, 0xaf, 0xdb, 0x1f + ]) + ) ) v.append( - aff(fe([0xf7, 0x17, 0x13, 0xbd, 0xfb, 0xbc, 0xd2, 0xec, 0x45, 0xb3, 0x15, 0x31, 0xe9, 0xaf, 0x82, 0x84, - 0x3d, 0x28, 0xc6, 0xfc, 0x11, 0xf5, 0x41, 0xb5, 0x8b, 0xd3, 0x12, 0x76, 0x52, 0xe7, 0x1a, 0x3c]) , - fe([0x4e, 0x36, 0x11, 0x07, 0xa2, 0x15, 0x20, 0x51, 0xc4, 0x2a, 0xc3, 0x62, 0x8b, 0x5e, 0x7f, 0xa6, - 0x0f, 0xf9, 0x45, 0x85, 0x6c, 0x11, 0x86, 0xb7, 0x7e, 0xe5, 0xd7, 0xf9, 0xc3, 0x91, 0x1c, 0x05])) + aff( + fe([ + 0xf7, 0x17, 0x13, 0xbd, 0xfb, 0xbc, 0xd2, 0xec, 0x45, 0xb3, 0x15, 0x31, 0xe9, 0xaf, 0x82, 0x84, + 0x3d, 0x28, 0xc6, 0xfc, 0x11, 0xf5, 0x41, 0xb5, 0x8b, 0xd3, 0x12, 0x76, 0x52, 0xe7, 0x1a, 0x3c + ]), + fe([ + 0x4e, 0x36, 0x11, 0x07, 0xa2, 0x15, 0x20, 0x51, 0xc4, 0x2a, 0xc3, 0x62, 0x8b, 0x5e, 0x7f, 0xa6, + 0x0f, 0xf9, 0x45, 0x85, 0x6c, 0x11, 0x86, 0xb7, 0x7e, 0xe5, 0xd7, 0xf9, 0xc3, 0x91, 0x1c, 0x05 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xea, 0xd6, 0xde, 0x29, 0x3a, 0x00, 0xb9, 0x02, 0x59, 0xcb, 0x26, 0xc4, 0xba, 0x99, 0xb1, 0x97, - 0x2f, 0x8e, 0x00, 0x92, 0x26, 0x4f, 0x52, 0xeb, 0x47, 0x1b, 0x89, 0x8b, 0x24, 0xc0, 0x13, 0x7d]) , - fe([0xd5, 0x20, 0x5b, 0x80, 0xa6, 0x80, 0x20, 0x95, 0xc3, 0xe9, 0x9f, 0x8e, 0x87, 0x9e, 0x1e, 0x9e, - 0x7a, 0xc7, 0xcc, 0x75, 0x6c, 0xa5, 0xf1, 0x91, 0x1a, 0xa8, 0x01, 0x2c, 0xab, 0x76, 0xa9, 0x59])) + aff( + fe([ + 0xea, 0xd6, 0xde, 0x29, 0x3a, 0x00, 0xb9, 0x02, 0x59, 0xcb, 0x26, 0xc4, 0xba, 0x99, 0xb1, 0x97, + 0x2f, 0x8e, 0x00, 0x92, 0x26, 0x4f, 0x52, 0xeb, 0x47, 0x1b, 0x89, 0x8b, 0x24, 0xc0, 0x13, 0x7d + ]), + fe([ + 0xd5, 0x20, 0x5b, 0x80, 0xa6, 0x80, 0x20, 0x95, 0xc3, 0xe9, 0x9f, 0x8e, 0x87, 0x9e, 0x1e, 0x9e, + 0x7a, 0xc7, 0xcc, 0x75, 0x6c, 0xa5, 0xf1, 0x91, 0x1a, 0xa8, 0x01, 0x2c, 0xab, 0x76, 0xa9, 0x59 + ]) + ) ) v.append( - aff(fe([0xde, 0xc9, 0xb1, 0x31, 0x10, 0x16, 0xaa, 0x35, 0x14, 0x6a, 0xd4, 0xb5, 0x34, 0x82, 0x71, 0xd2, - 0x4a, 0x5d, 0x9a, 0x1f, 0x53, 0x26, 0x3c, 0xe5, 0x8e, 0x8d, 0x33, 0x7f, 0xff, 0xa9, 0xd5, 0x17]) , - fe([0x89, 0xaf, 0xf6, 0xa4, 0x64, 0xd5, 0x10, 0xe0, 0x1d, 0xad, 0xef, 0x44, 0xbd, 0xda, 0x83, 0xac, - 0x7a, 0xa8, 0xf0, 0x1c, 0x07, 0xf9, 0xc3, 0x43, 0x6c, 0x3f, 0xb7, 0xd3, 0x87, 0x22, 0x02, 0x73])) + aff( + fe([ + 0xde, 0xc9, 0xb1, 0x31, 0x10, 0x16, 0xaa, 0x35, 0x14, 0x6a, 0xd4, 0xb5, 0x34, 0x82, 0x71, 0xd2, + 0x4a, 0x5d, 0x9a, 0x1f, 0x53, 0x26, 0x3c, 0xe5, 0x8e, 0x8d, 0x33, 0x7f, 0xff, 0xa9, 0xd5, 0x17 + ]), + fe([ + 0x89, 0xaf, 0xf6, 0xa4, 0x64, 0xd5, 0x10, 0xe0, 0x1d, 0xad, 0xef, 0x44, 0xbd, 0xda, 0x83, 0xac, + 0x7a, 0xa8, 0xf0, 0x1c, 0x07, 0xf9, 0xc3, 0x43, 0x6c, 0x3f, 0xb7, 0xd3, 0x87, 0x22, 0x02, 0x73 + ]) + ) ) v.append( - aff(fe([0x64, 0x1d, 0x49, 0x13, 0x2f, 0x71, 0xec, 0x69, 0x87, 0xd0, 0x42, 0xee, 0x13, 0xec, 0xe3, 0xed, - 0x56, 0x7b, 0xbf, 0xbd, 0x8c, 0x2f, 0x7d, 0x7b, 0x9d, 0x28, 0xec, 0x8e, 0x76, 0x2f, 0x6f, 0x08]) , - fe([0x22, 0xf5, 0x5f, 0x4d, 0x15, 0xef, 0xfc, 0x4e, 0x57, 0x03, 0x36, 0x89, 0xf0, 0xeb, 0x5b, 0x91, - 0xd6, 0xe2, 0xca, 0x01, 0xa5, 0xee, 0x52, 0xec, 0xa0, 0x3c, 0x8f, 0x33, 0x90, 0x5a, 0x94, 0x72])) + aff( + fe([ + 0x64, 0x1d, 0x49, 0x13, 0x2f, 0x71, 0xec, 0x69, 0x87, 0xd0, 0x42, 0xee, 0x13, 0xec, 0xe3, 0xed, + 0x56, 0x7b, 0xbf, 0xbd, 0x8c, 0x2f, 0x7d, 0x7b, 0x9d, 0x28, 0xec, 0x8e, 0x76, 0x2f, 0x6f, 0x08 + ]), + fe([ + 0x22, 0xf5, 0x5f, 0x4d, 0x15, 0xef, 0xfc, 0x4e, 0x57, 0x03, 0x36, 0x89, 0xf0, 0xeb, 0x5b, 0x91, + 0xd6, 0xe2, 0xca, 0x01, 0xa5, 0xee, 0x52, 0xec, 0xa0, 0x3c, 0x8f, 0x33, 0x90, 0x5a, 0x94, 0x72 + ]) + ) ) v.append( - aff(fe([0x8a, 0x4b, 0xe7, 0x38, 0xbc, 0xda, 0xc2, 0xb0, 0x85, 0xe1, 0x4a, 0xfe, 0x2d, 0x44, 0x84, 0xcb, - 0x20, 0x6b, 0x2d, 0xbf, 0x11, 0x9c, 0xd7, 0xbe, 0xd3, 0x3e, 0x5f, 0xbf, 0x68, 0xbc, 0xa8, 0x07]) , - fe([0x01, 0x89, 0x28, 0x22, 0x6a, 0x78, 0xaa, 0x29, 0x03, 0xc8, 0x74, 0x95, 0x03, 0x3e, 0xdc, 0xbd, - 0x07, 0x13, 0xa8, 0xa2, 0x20, 0x2d, 0xb3, 0x18, 0x70, 0x42, 0xfd, 0x7a, 0xc4, 0xd7, 0x49, 0x72])) + aff( + fe([ + 0x8a, 0x4b, 0xe7, 0x38, 0xbc, 0xda, 0xc2, 0xb0, 0x85, 0xe1, 0x4a, 0xfe, 0x2d, 0x44, 0x84, 0xcb, + 0x20, 0x6b, 0x2d, 0xbf, 0x11, 0x9c, 0xd7, 0xbe, 0xd3, 0x3e, 0x5f, 0xbf, 0x68, 0xbc, 0xa8, 0x07 + ]), + fe([ + 0x01, 0x89, 0x28, 0x22, 0x6a, 0x78, 0xaa, 0x29, 0x03, 0xc8, 0x74, 0x95, 0x03, 0x3e, 0xdc, 0xbd, + 0x07, 0x13, 0xa8, 0xa2, 0x20, 0x2d, 0xb3, 0x18, 0x70, 0x42, 0xfd, 0x7a, 0xc4, 0xd7, 0x49, 0x72 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x02, 0xff, 0x32, 0x2b, 0x5c, 0x93, 0x54, 0x32, 0xe8, 0x57, 0x54, 0x1a, 0x8b, 0x33, 0x60, 0x65, - 0xd3, 0x67, 0xa4, 0xc1, 0x26, 0xc4, 0xa4, 0x34, 0x1f, 0x9b, 0xa7, 0xa9, 0xf4, 0xd9, 0x4f, 0x5b]) , - fe([0x46, 0x8d, 0xb0, 0x33, 0x54, 0x26, 0x5b, 0x68, 0xdf, 0xbb, 0xc5, 0xec, 0xc2, 0xf9, 0x3c, 0x5a, - 0x37, 0xc1, 0x8e, 0x27, 0x47, 0xaa, 0x49, 0x5a, 0xf8, 0xfb, 0x68, 0x04, 0x23, 0xd1, 0xeb, 0x40])) + aff( + fe([ + 0x02, 0xff, 0x32, 0x2b, 0x5c, 0x93, 0x54, 0x32, 0xe8, 0x57, 0x54, 0x1a, 0x8b, 0x33, 0x60, 0x65, + 0xd3, 0x67, 0xa4, 0xc1, 0x26, 0xc4, 0xa4, 0x34, 0x1f, 0x9b, 0xa7, 0xa9, 0xf4, 0xd9, 0x4f, 0x5b + ]), + fe([ + 0x46, 0x8d, 0xb0, 0x33, 0x54, 0x26, 0x5b, 0x68, 0xdf, 0xbb, 0xc5, 0xec, 0xc2, 0xf9, 0x3c, 0x5a, + 0x37, 0xc1, 0x8e, 0x27, 0x47, 0xaa, 0x49, 0x5a, 0xf8, 0xfb, 0x68, 0x04, 0x23, 0xd1, 0xeb, 0x40 + ]) + ) ) v.append( - aff(fe([0x65, 0xa5, 0x11, 0x84, 0x8a, 0x67, 0x9d, 0x9e, 0xd1, 0x44, 0x68, 0x7a, 0x34, 0xe1, 0x9f, 0xa3, - 0x54, 0xcd, 0x07, 0xca, 0x79, 0x1f, 0x54, 0x2f, 0x13, 0x70, 0x4e, 0xee, 0xa2, 0xfa, 0xe7, 0x5d]) , - fe([0x36, 0xec, 0x54, 0xf8, 0xce, 0xe4, 0x85, 0xdf, 0xf6, 0x6f, 0x1d, 0x90, 0x08, 0xbc, 0xe8, 0xc0, - 0x92, 0x2d, 0x43, 0x6b, 0x92, 0xa9, 0x8e, 0xab, 0x0a, 0x2e, 0x1c, 0x1e, 0x64, 0x23, 0x9f, 0x2c])) + aff( + fe([ + 0x65, 0xa5, 0x11, 0x84, 0x8a, 0x67, 0x9d, 0x9e, 0xd1, 0x44, 0x68, 0x7a, 0x34, 0xe1, 0x9f, 0xa3, + 0x54, 0xcd, 0x07, 0xca, 0x79, 0x1f, 0x54, 0x2f, 0x13, 0x70, 0x4e, 0xee, 0xa2, 0xfa, 0xe7, 0x5d + ]), + fe([ + 0x36, 0xec, 0x54, 0xf8, 0xce, 0xe4, 0x85, 0xdf, 0xf6, 0x6f, 0x1d, 0x90, 0x08, 0xbc, 0xe8, 0xc0, + 0x92, 0x2d, 0x43, 0x6b, 0x92, 0xa9, 0x8e, 0xab, 0x0a, 0x2e, 0x1c, 0x1e, 0x64, 0x23, 0x9f, 0x2c + ]) + ) ) v.append( - aff(fe([0xa7, 0xd6, 0x2e, 0xd5, 0xcc, 0xd4, 0xcb, 0x5a, 0x3b, 0xa7, 0xf9, 0x46, 0x03, 0x1d, 0xad, 0x2b, - 0x34, 0x31, 0x90, 0x00, 0x46, 0x08, 0x82, 0x14, 0xc4, 0xe0, 0x9c, 0xf0, 0xe3, 0x55, 0x43, 0x31]) , - fe([0x60, 0xd6, 0xdd, 0x78, 0xe6, 0xd4, 0x22, 0x42, 0x1f, 0x00, 0xf9, 0xb1, 0x6a, 0x63, 0xe2, 0x92, - 0x59, 0xd1, 0x1a, 0xb7, 0x00, 0x54, 0x29, 0xc9, 0xc1, 0xf6, 0x6f, 0x7a, 0xc5, 0x3c, 0x5f, 0x65])) + aff( + fe([ + 0xa7, 0xd6, 0x2e, 0xd5, 0xcc, 0xd4, 0xcb, 0x5a, 0x3b, 0xa7, 0xf9, 0x46, 0x03, 0x1d, 0xad, 0x2b, + 0x34, 0x31, 0x90, 0x00, 0x46, 0x08, 0x82, 0x14, 0xc4, 0xe0, 0x9c, 0xf0, 0xe3, 0x55, 0x43, 0x31 + ]), + fe([ + 0x60, 0xd6, 0xdd, 0x78, 0xe6, 0xd4, 0x22, 0x42, 0x1f, 0x00, 0xf9, 0xb1, 0x6a, 0x63, 0xe2, 0x92, + 0x59, 0xd1, 0x1a, 0xb7, 0x00, 0x54, 0x29, 0xc9, 0xc1, 0xf6, 0x6f, 0x7a, 0xc5, 0x3c, 0x5f, 0x65 + ]) + ) ) v.append( - aff(fe([0x27, 0x4f, 0xd0, 0x72, 0xb1, 0x11, 0x14, 0x27, 0x15, 0x94, 0x48, 0x81, 0x7e, 0x74, 0xd8, 0x32, - 0xd5, 0xd1, 0x11, 0x28, 0x60, 0x63, 0x36, 0x32, 0x37, 0xb5, 0x13, 0x1c, 0xa0, 0x37, 0xe3, 0x74]) , - fe([0xf1, 0x25, 0x4e, 0x11, 0x96, 0x67, 0xe6, 0x1c, 0xc2, 0xb2, 0x53, 0xe2, 0xda, 0x85, 0xee, 0xb2, - 0x9f, 0x59, 0xf3, 0xba, 0xbd, 0xfa, 0xcf, 0x6e, 0xf9, 0xda, 0xa4, 0xb3, 0x02, 0x8f, 0x64, 0x08])) + aff( + fe([ + 0x27, 0x4f, 0xd0, 0x72, 0xb1, 0x11, 0x14, 0x27, 0x15, 0x94, 0x48, 0x81, 0x7e, 0x74, 0xd8, 0x32, + 0xd5, 0xd1, 0x11, 0x28, 0x60, 0x63, 0x36, 0x32, 0x37, 0xb5, 0x13, 0x1c, 0xa0, 0x37, 0xe3, 0x74 + ]), + fe([ + 0xf1, 0x25, 0x4e, 0x11, 0x96, 0x67, 0xe6, 0x1c, 0xc2, 0xb2, 0x53, 0xe2, 0xda, 0x85, 0xee, 0xb2, + 0x9f, 0x59, 0xf3, 0xba, 0xbd, 0xfa, 0xcf, 0x6e, 0xf9, 0xda, 0xa4, 0xb3, 0x02, 0x8f, 0x64, 0x08 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x34, 0x94, 0xf2, 0x64, 0x54, 0x47, 0x37, 0x07, 0x40, 0x8a, 0x20, 0xba, 0x4a, 0x55, 0xd7, 0x3f, - 0x47, 0xba, 0x25, 0x23, 0x14, 0xb0, 0x2c, 0xe8, 0x55, 0xa8, 0xa6, 0xef, 0x51, 0xbd, 0x6f, 0x6a]) , - fe([0x71, 0xd6, 0x16, 0x76, 0xb2, 0x06, 0xea, 0x79, 0xf5, 0xc4, 0xc3, 0x52, 0x7e, 0x61, 0xd1, 0xe1, - 0xad, 0x70, 0x78, 0x1d, 0x16, 0x11, 0xf8, 0x7c, 0x2b, 0xfc, 0x55, 0x9f, 0x52, 0xf8, 0xf5, 0x16])) + aff( + fe([ + 0x34, 0x94, 0xf2, 0x64, 0x54, 0x47, 0x37, 0x07, 0x40, 0x8a, 0x20, 0xba, 0x4a, 0x55, 0xd7, 0x3f, + 0x47, 0xba, 0x25, 0x23, 0x14, 0xb0, 0x2c, 0xe8, 0x55, 0xa8, 0xa6, 0xef, 0x51, 0xbd, 0x6f, 0x6a + ]), + fe([ + 0x71, 0xd6, 0x16, 0x76, 0xb2, 0x06, 0xea, 0x79, 0xf5, 0xc4, 0xc3, 0x52, 0x7e, 0x61, 0xd1, 0xe1, + 0xad, 0x70, 0x78, 0x1d, 0x16, 0x11, 0xf8, 0x7c, 0x2b, 0xfc, 0x55, 0x9f, 0x52, 0xf8, 0xf5, 0x16 + ]) + ) ) v.append( - aff(fe([0x34, 0x96, 0x9a, 0xf6, 0xc5, 0xe0, 0x14, 0x03, 0x24, 0x0e, 0x4c, 0xad, 0x9e, 0x9a, 0x70, 0x23, - 0x96, 0xb2, 0xf1, 0x2e, 0x9d, 0xc3, 0x32, 0x9b, 0x54, 0xa5, 0x73, 0xde, 0x88, 0xb1, 0x3e, 0x24]) , - fe([0xf6, 0xe2, 0x4c, 0x1f, 0x5b, 0xb2, 0xaf, 0x82, 0xa5, 0xcf, 0x81, 0x10, 0x04, 0xef, 0xdb, 0xa2, - 0xcc, 0x24, 0xb2, 0x7e, 0x0b, 0x7a, 0xeb, 0x01, 0xd8, 0x52, 0xf4, 0x51, 0x89, 0x29, 0x79, 0x37])) + aff( + fe([ + 0x34, 0x96, 0x9a, 0xf6, 0xc5, 0xe0, 0x14, 0x03, 0x24, 0x0e, 0x4c, 0xad, 0x9e, 0x9a, 0x70, 0x23, + 0x96, 0xb2, 0xf1, 0x2e, 0x9d, 0xc3, 0x32, 0x9b, 0x54, 0xa5, 0x73, 0xde, 0x88, 0xb1, 0x3e, 0x24 + ]), + fe([ + 0xf6, 0xe2, 0x4c, 0x1f, 0x5b, 0xb2, 0xaf, 0x82, 0xa5, 0xcf, 0x81, 0x10, 0x04, 0xef, 0xdb, 0xa2, + 0xcc, 0x24, 0xb2, 0x7e, 0x0b, 0x7a, 0xeb, 0x01, 0xd8, 0x52, 0xf4, 0x51, 0x89, 0x29, 0x79, 0x37 + ]) + ) ) v.append( - aff(fe([0x74, 0xde, 0x12, 0xf3, 0x68, 0xb7, 0x66, 0xc3, 0xee, 0x68, 0xdc, 0x81, 0xb5, 0x55, 0x99, 0xab, - 0xd9, 0x28, 0x63, 0x6d, 0x8b, 0x40, 0x69, 0x75, 0x6c, 0xcd, 0x5c, 0x2a, 0x7e, 0x32, 0x7b, 0x29]) , - fe([0x02, 0xcc, 0x22, 0x74, 0x4d, 0x19, 0x07, 0xc0, 0xda, 0xb5, 0x76, 0x51, 0x2a, 0xaa, 0xa6, 0x0a, - 0x5f, 0x26, 0xd4, 0xbc, 0xaf, 0x48, 0x88, 0x7f, 0x02, 0xbc, 0xf2, 0xe1, 0xcf, 0xe9, 0xdd, 0x15])) + aff( + fe([ + 0x74, 0xde, 0x12, 0xf3, 0x68, 0xb7, 0x66, 0xc3, 0xee, 0x68, 0xdc, 0x81, 0xb5, 0x55, 0x99, 0xab, + 0xd9, 0x28, 0x63, 0x6d, 0x8b, 0x40, 0x69, 0x75, 0x6c, 0xcd, 0x5c, 0x2a, 0x7e, 0x32, 0x7b, 0x29 + ]), + fe([ + 0x02, 0xcc, 0x22, 0x74, 0x4d, 0x19, 0x07, 0xc0, 0xda, 0xb5, 0x76, 0x51, 0x2a, 0xaa, 0xa6, 0x0a, + 0x5f, 0x26, 0xd4, 0xbc, 0xaf, 0x48, 0x88, 0x7f, 0x02, 0xbc, 0xf2, 0xe1, 0xcf, 0xe9, 0xdd, 0x15 + ]) + ) ) v.append( - aff(fe([0xed, 0xb5, 0x9a, 0x8c, 0x9a, 0xdd, 0x27, 0xf4, 0x7f, 0x47, 0xd9, 0x52, 0xa7, 0xcd, 0x65, 0xa5, - 0x31, 0x22, 0xed, 0xa6, 0x63, 0x5b, 0x80, 0x4a, 0xad, 0x4d, 0xed, 0xbf, 0xee, 0x49, 0xb3, 0x06]) , - fe([0xf8, 0x64, 0x8b, 0x60, 0x90, 0xe9, 0xde, 0x44, 0x77, 0xb9, 0x07, 0x36, 0x32, 0xc2, 0x50, 0xf5, - 0x65, 0xdf, 0x48, 0x4c, 0x37, 0xaa, 0x68, 0xab, 0x9a, 0x1f, 0x3e, 0xff, 0x89, 0x92, 0xa0, 0x07])) + aff( + fe([ + 0xed, 0xb5, 0x9a, 0x8c, 0x9a, 0xdd, 0x27, 0xf4, 0x7f, 0x47, 0xd9, 0x52, 0xa7, 0xcd, 0x65, 0xa5, + 0x31, 0x22, 0xed, 0xa6, 0x63, 0x5b, 0x80, 0x4a, 0xad, 0x4d, 0xed, 0xbf, 0xee, 0x49, 0xb3, 0x06 + ]), + fe([ + 0xf8, 0x64, 0x8b, 0x60, 0x90, 0xe9, 0xde, 0x44, 0x77, 0xb9, 0x07, 0x36, 0x32, 0xc2, 0x50, 0xf5, + 0x65, 0xdf, 0x48, 0x4c, 0x37, 0xaa, 0x68, 0xab, 0x9a, 0x1f, 0x3e, 0xff, 0x89, 0x92, 0xa0, 0x07 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x7d, 0x4f, 0x9c, 0x19, 0xc0, 0x4a, 0x31, 0xec, 0xf9, 0xaa, 0xeb, 0xb2, 0x16, 0x9c, 0xa3, 0x66, - 0x5f, 0xd1, 0xd4, 0xed, 0xb8, 0x92, 0x1c, 0xab, 0xda, 0xea, 0xd9, 0x57, 0xdf, 0x4c, 0x2a, 0x48]) , - fe([0x4b, 0xb0, 0x4e, 0x6e, 0x11, 0x3b, 0x51, 0xbd, 0x6a, 0xfd, 0xe4, 0x25, 0xa5, 0x5f, 0x11, 0x3f, - 0x98, 0x92, 0x51, 0x14, 0xc6, 0x5f, 0x3c, 0x0b, 0xa8, 0xf7, 0xc2, 0x81, 0x43, 0xde, 0x91, 0x73])) + aff( + fe([ + 0x7d, 0x4f, 0x9c, 0x19, 0xc0, 0x4a, 0x31, 0xec, 0xf9, 0xaa, 0xeb, 0xb2, 0x16, 0x9c, 0xa3, 0x66, + 0x5f, 0xd1, 0xd4, 0xed, 0xb8, 0x92, 0x1c, 0xab, 0xda, 0xea, 0xd9, 0x57, 0xdf, 0x4c, 0x2a, 0x48 + ]), + fe([ + 0x4b, 0xb0, 0x4e, 0x6e, 0x11, 0x3b, 0x51, 0xbd, 0x6a, 0xfd, 0xe4, 0x25, 0xa5, 0x5f, 0x11, 0x3f, + 0x98, 0x92, 0x51, 0x14, 0xc6, 0x5f, 0x3c, 0x0b, 0xa8, 0xf7, 0xc2, 0x81, 0x43, 0xde, 0x91, 0x73 + ]) + ) ) v.append( - aff(fe([0x3c, 0x8f, 0x9f, 0x33, 0x2a, 0x1f, 0x43, 0x33, 0x8f, 0x68, 0xff, 0x1f, 0x3d, 0x73, 0x6b, 0xbf, - 0x68, 0xcc, 0x7d, 0x13, 0x6c, 0x24, 0x4b, 0xcc, 0x4d, 0x24, 0x0d, 0xfe, 0xde, 0x86, 0xad, 0x3b]) , - fe([0x79, 0x51, 0x81, 0x01, 0xdc, 0x73, 0x53, 0xe0, 0x6e, 0x9b, 0xea, 0x68, 0x3f, 0x5c, 0x14, 0x84, - 0x53, 0x8d, 0x4b, 0xc0, 0x9f, 0x9f, 0x89, 0x2b, 0x8c, 0xba, 0x86, 0xfa, 0xf2, 0xcd, 0xe3, 0x2d])) + aff( + fe([ + 0x3c, 0x8f, 0x9f, 0x33, 0x2a, 0x1f, 0x43, 0x33, 0x8f, 0x68, 0xff, 0x1f, 0x3d, 0x73, 0x6b, 0xbf, + 0x68, 0xcc, 0x7d, 0x13, 0x6c, 0x24, 0x4b, 0xcc, 0x4d, 0x24, 0x0d, 0xfe, 0xde, 0x86, 0xad, 0x3b + ]), + fe([ + 0x79, 0x51, 0x81, 0x01, 0xdc, 0x73, 0x53, 0xe0, 0x6e, 0x9b, 0xea, 0x68, 0x3f, 0x5c, 0x14, 0x84, + 0x53, 0x8d, 0x4b, 0xc0, 0x9f, 0x9f, 0x89, 0x2b, 0x8c, 0xba, 0x86, 0xfa, 0xf2, 0xcd, 0xe3, 0x2d + ]) + ) ) v.append( - aff(fe([0x06, 0xf9, 0x29, 0x5a, 0xdb, 0x3d, 0x84, 0x52, 0xab, 0xcc, 0x6b, 0x60, 0x9d, 0xb7, 0x4a, 0x0e, - 0x36, 0x63, 0x91, 0xad, 0xa0, 0x95, 0xb0, 0x97, 0x89, 0x4e, 0xcf, 0x7d, 0x3c, 0xe5, 0x7c, 0x28]) , - fe([0x2e, 0x69, 0x98, 0xfd, 0xc6, 0xbd, 0xcc, 0xca, 0xdf, 0x9a, 0x44, 0x7e, 0x9d, 0xca, 0x89, 0x6d, - 0xbf, 0x27, 0xc2, 0xf8, 0xcd, 0x46, 0x00, 0x2b, 0xb5, 0x58, 0x4e, 0xb7, 0x89, 0x09, 0xe9, 0x2d])) + aff( + fe([ + 0x06, 0xf9, 0x29, 0x5a, 0xdb, 0x3d, 0x84, 0x52, 0xab, 0xcc, 0x6b, 0x60, 0x9d, 0xb7, 0x4a, 0x0e, + 0x36, 0x63, 0x91, 0xad, 0xa0, 0x95, 0xb0, 0x97, 0x89, 0x4e, 0xcf, 0x7d, 0x3c, 0xe5, 0x7c, 0x28 + ]), + fe([ + 0x2e, 0x69, 0x98, 0xfd, 0xc6, 0xbd, 0xcc, 0xca, 0xdf, 0x9a, 0x44, 0x7e, 0x9d, 0xca, 0x89, 0x6d, + 0xbf, 0x27, 0xc2, 0xf8, 0xcd, 0x46, 0x00, 0x2b, 0xb5, 0x58, 0x4e, 0xb7, 0x89, 0x09, 0xe9, 0x2d + ]) + ) ) v.append( - aff(fe([0x54, 0xbe, 0x75, 0xcb, 0x05, 0xb0, 0x54, 0xb7, 0xe7, 0x26, 0x86, 0x4a, 0xfc, 0x19, 0xcf, 0x27, - 0x46, 0xd4, 0x22, 0x96, 0x5a, 0x11, 0xe8, 0xd5, 0x1b, 0xed, 0x71, 0xc5, 0x5d, 0xc8, 0xaf, 0x45]) , - fe([0x40, 0x7b, 0x77, 0x57, 0x49, 0x9e, 0x80, 0x39, 0x23, 0xee, 0x81, 0x0b, 0x22, 0xcf, 0xdb, 0x7a, - 0x2f, 0x14, 0xb8, 0x57, 0x8f, 0xa1, 0x39, 0x1e, 0x77, 0xfc, 0x0b, 0xa6, 0xbf, 0x8a, 0x0c, 0x6c])) + aff( + fe([ + 0x54, 0xbe, 0x75, 0xcb, 0x05, 0xb0, 0x54, 0xb7, 0xe7, 0x26, 0x86, 0x4a, 0xfc, 0x19, 0xcf, 0x27, + 0x46, 0xd4, 0x22, 0x96, 0x5a, 0x11, 0xe8, 0xd5, 0x1b, 0xed, 0x71, 0xc5, 0x5d, 0xc8, 0xaf, 0x45 + ]), + fe([ + 0x40, 0x7b, 0x77, 0x57, 0x49, 0x9e, 0x80, 0x39, 0x23, 0xee, 0x81, 0x0b, 0x22, 0xcf, 0xdb, 0x7a, + 0x2f, 0x14, 0xb8, 0x57, 0x8f, 0xa1, 0x39, 0x1e, 0x77, 0xfc, 0x0b, 0xa6, 0xbf, 0x8a, 0x0c, 0x6c + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x77, 0x3a, 0xd4, 0xd8, 0x27, 0xcf, 0xe8, 0xa1, 0x72, 0x9d, 0xca, 0xdd, 0x0d, 0x96, 0xda, 0x79, - 0xed, 0x56, 0x42, 0x15, 0x60, 0xc7, 0x1c, 0x6b, 0x26, 0x30, 0xf6, 0x6a, 0x95, 0x67, 0xf3, 0x0a]) , - fe([0xc5, 0x08, 0xa4, 0x2b, 0x2f, 0xbd, 0x31, 0x81, 0x2a, 0xa6, 0xb6, 0xe4, 0x00, 0x91, 0xda, 0x3d, - 0xb2, 0xb0, 0x96, 0xce, 0x8a, 0xd2, 0x8d, 0x70, 0xb3, 0xd3, 0x34, 0x01, 0x90, 0x8d, 0x10, 0x21])) + aff( + fe([ + 0x77, 0x3a, 0xd4, 0xd8, 0x27, 0xcf, 0xe8, 0xa1, 0x72, 0x9d, 0xca, 0xdd, 0x0d, 0x96, 0xda, 0x79, + 0xed, 0x56, 0x42, 0x15, 0x60, 0xc7, 0x1c, 0x6b, 0x26, 0x30, 0xf6, 0x6a, 0x95, 0x67, 0xf3, 0x0a + ]), + fe([ + 0xc5, 0x08, 0xa4, 0x2b, 0x2f, 0xbd, 0x31, 0x81, 0x2a, 0xa6, 0xb6, 0xe4, 0x00, 0x91, 0xda, 0x3d, + 0xb2, 0xb0, 0x96, 0xce, 0x8a, 0xd2, 0x8d, 0x70, 0xb3, 0xd3, 0x34, 0x01, 0x90, 0x8d, 0x10, 0x21 + ]) + ) ) v.append( - aff(fe([0x33, 0x0d, 0xe7, 0xba, 0x4f, 0x07, 0xdf, 0x8d, 0xea, 0x7d, 0xa0, 0xc5, 0xd6, 0xb1, 0xb0, 0xe5, - 0x57, 0x1b, 0x5b, 0xf5, 0x45, 0x13, 0x14, 0x64, 0x5a, 0xeb, 0x5c, 0xfc, 0x54, 0x01, 0x76, 0x2b]) , - fe([0x02, 0x0c, 0xc2, 0xaf, 0x96, 0x36, 0xfe, 0x4a, 0xe2, 0x54, 0x20, 0x6a, 0xeb, 0xb2, 0x9f, 0x62, - 0xd7, 0xce, 0xa2, 0x3f, 0x20, 0x11, 0x34, 0x37, 0xe0, 0x42, 0xed, 0x6f, 0xf9, 0x1a, 0xc8, 0x7d])) + aff( + fe([ + 0x33, 0x0d, 0xe7, 0xba, 0x4f, 0x07, 0xdf, 0x8d, 0xea, 0x7d, 0xa0, 0xc5, 0xd6, 0xb1, 0xb0, 0xe5, + 0x57, 0x1b, 0x5b, 0xf5, 0x45, 0x13, 0x14, 0x64, 0x5a, 0xeb, 0x5c, 0xfc, 0x54, 0x01, 0x76, 0x2b + ]), + fe([ + 0x02, 0x0c, 0xc2, 0xaf, 0x96, 0x36, 0xfe, 0x4a, 0xe2, 0x54, 0x20, 0x6a, 0xeb, 0xb2, 0x9f, 0x62, + 0xd7, 0xce, 0xa2, 0x3f, 0x20, 0x11, 0x34, 0x37, 0xe0, 0x42, 0xed, 0x6f, 0xf9, 0x1a, 0xc8, 0x7d + ]) + ) ) v.append( - aff(fe([0xd8, 0xb9, 0x11, 0xe8, 0x36, 0x3f, 0x42, 0xc1, 0xca, 0xdc, 0xd3, 0xf1, 0xc8, 0x23, 0x3d, 0x4f, - 0x51, 0x7b, 0x9d, 0x8d, 0xd8, 0xe4, 0xa0, 0xaa, 0xf3, 0x04, 0xd6, 0x11, 0x93, 0xc8, 0x35, 0x45]) , - fe([0x61, 0x36, 0xd6, 0x08, 0x90, 0xbf, 0xa7, 0x7a, 0x97, 0x6c, 0x0f, 0x84, 0xd5, 0x33, 0x2d, 0x37, - 0xc9, 0x6a, 0x80, 0x90, 0x3d, 0x0a, 0xa2, 0xaa, 0xe1, 0xb8, 0x84, 0xba, 0x61, 0x36, 0xdd, 0x69])) + aff( + fe([ + 0xd8, 0xb9, 0x11, 0xe8, 0x36, 0x3f, 0x42, 0xc1, 0xca, 0xdc, 0xd3, 0xf1, 0xc8, 0x23, 0x3d, 0x4f, + 0x51, 0x7b, 0x9d, 0x8d, 0xd8, 0xe4, 0xa0, 0xaa, 0xf3, 0x04, 0xd6, 0x11, 0x93, 0xc8, 0x35, 0x45 + ]), + fe([ + 0x61, 0x36, 0xd6, 0x08, 0x90, 0xbf, 0xa7, 0x7a, 0x97, 0x6c, 0x0f, 0x84, 0xd5, 0x33, 0x2d, 0x37, + 0xc9, 0x6a, 0x80, 0x90, 0x3d, 0x0a, 0xa2, 0xaa, 0xe1, 0xb8, 0x84, 0xba, 0x61, 0x36, 0xdd, 0x69 + ]) + ) ) v.append( - aff(fe([0x6b, 0xdb, 0x5b, 0x9c, 0xc6, 0x92, 0xbc, 0x23, 0xaf, 0xc5, 0xb8, 0x75, 0xf8, 0x42, 0xfa, 0xd6, - 0xb6, 0x84, 0x94, 0x63, 0x98, 0x93, 0x48, 0x78, 0x38, 0xcd, 0xbb, 0x18, 0x34, 0xc3, 0xdb, 0x67]) , - fe([0x96, 0xf3, 0x3a, 0x09, 0x56, 0xb0, 0x6f, 0x7c, 0x51, 0x1e, 0x1b, 0x39, 0x48, 0xea, 0xc9, 0x0c, - 0x25, 0xa2, 0x7a, 0xca, 0xe7, 0x92, 0xfc, 0x59, 0x30, 0xa3, 0x89, 0x85, 0xdf, 0x6f, 0x43, 0x38])) + aff( + fe([ + 0x6b, 0xdb, 0x5b, 0x9c, 0xc6, 0x92, 0xbc, 0x23, 0xaf, 0xc5, 0xb8, 0x75, 0xf8, 0x42, 0xfa, 0xd6, + 0xb6, 0x84, 0x94, 0x63, 0x98, 0x93, 0x48, 0x78, 0x38, 0xcd, 0xbb, 0x18, 0x34, 0xc3, 0xdb, 0x67 + ]), + fe([ + 0x96, 0xf3, 0x3a, 0x09, 0x56, 0xb0, 0x6f, 0x7c, 0x51, 0x1e, 0x1b, 0x39, 0x48, 0xea, 0xc9, 0x0c, + 0x25, 0xa2, 0x7a, 0xca, 0xe7, 0x92, 0xfc, 0x59, 0x30, 0xa3, 0x89, 0x85, 0xdf, 0x6f, 0x43, 0x38 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x79, 0x84, 0x44, 0x19, 0xbd, 0xe9, 0x54, 0xc4, 0xc0, 0x6e, 0x2a, 0xa8, 0xa8, 0x9b, 0x43, 0xd5, - 0x71, 0x22, 0x5f, 0xdc, 0x01, 0xfa, 0xdf, 0xb3, 0xb8, 0x47, 0x4b, 0x0a, 0xa5, 0x44, 0xea, 0x29]) , - fe([0x05, 0x90, 0x50, 0xaf, 0x63, 0x5f, 0x9d, 0x9e, 0xe1, 0x9d, 0x38, 0x97, 0x1f, 0x6c, 0xac, 0x30, - 0x46, 0xb2, 0x6a, 0x19, 0xd1, 0x4b, 0xdb, 0xbb, 0x8c, 0xda, 0x2e, 0xab, 0xc8, 0x5a, 0x77, 0x6c])) + aff( + fe([ + 0x79, 0x84, 0x44, 0x19, 0xbd, 0xe9, 0x54, 0xc4, 0xc0, 0x6e, 0x2a, 0xa8, 0xa8, 0x9b, 0x43, 0xd5, + 0x71, 0x22, 0x5f, 0xdc, 0x01, 0xfa, 0xdf, 0xb3, 0xb8, 0x47, 0x4b, 0x0a, 0xa5, 0x44, 0xea, 0x29 + ]), + fe([ + 0x05, 0x90, 0x50, 0xaf, 0x63, 0x5f, 0x9d, 0x9e, 0xe1, 0x9d, 0x38, 0x97, 0x1f, 0x6c, 0xac, 0x30, + 0x46, 0xb2, 0x6a, 0x19, 0xd1, 0x4b, 0xdb, 0xbb, 0x8c, 0xda, 0x2e, 0xab, 0xc8, 0x5a, 0x77, 0x6c + ]) + ) ) v.append( - aff(fe([0x2b, 0xbe, 0xaf, 0xa1, 0x6d, 0x2f, 0x0b, 0xb1, 0x8f, 0xe3, 0xe0, 0x38, 0xcd, 0x0b, 0x41, 0x1b, - 0x4a, 0x15, 0x07, 0xf3, 0x6f, 0xdc, 0xb8, 0xe9, 0xde, 0xb2, 0xa3, 0x40, 0x01, 0xa6, 0x45, 0x1e]) , - fe([0x76, 0x0a, 0xda, 0x8d, 0x2c, 0x07, 0x3f, 0x89, 0x7d, 0x04, 0xad, 0x43, 0x50, 0x6e, 0xd2, 0x47, - 0xcb, 0x8a, 0xe6, 0x85, 0x1a, 0x24, 0xf3, 0xd2, 0x60, 0xfd, 0xdf, 0x73, 0xa4, 0x0d, 0x73, 0x0e])) + aff( + fe([ + 0x2b, 0xbe, 0xaf, 0xa1, 0x6d, 0x2f, 0x0b, 0xb1, 0x8f, 0xe3, 0xe0, 0x38, 0xcd, 0x0b, 0x41, 0x1b, + 0x4a, 0x15, 0x07, 0xf3, 0x6f, 0xdc, 0xb8, 0xe9, 0xde, 0xb2, 0xa3, 0x40, 0x01, 0xa6, 0x45, 0x1e + ]), + fe([ + 0x76, 0x0a, 0xda, 0x8d, 0x2c, 0x07, 0x3f, 0x89, 0x7d, 0x04, 0xad, 0x43, 0x50, 0x6e, 0xd2, 0x47, + 0xcb, 0x8a, 0xe6, 0x85, 0x1a, 0x24, 0xf3, 0xd2, 0x60, 0xfd, 0xdf, 0x73, 0xa4, 0x0d, 0x73, 0x0e + ]) + ) ) v.append( - aff(fe([0xfd, 0x67, 0x6b, 0x71, 0x9b, 0x81, 0x53, 0x39, 0x39, 0xf4, 0xb8, 0xd5, 0xc3, 0x30, 0x9b, 0x3b, - 0x7c, 0xa3, 0xf0, 0xd0, 0x84, 0x21, 0xd6, 0xbf, 0xb7, 0x4c, 0x87, 0x13, 0x45, 0x2d, 0xa7, 0x55]) , - fe([0x5d, 0x04, 0xb3, 0x40, 0x28, 0x95, 0x2d, 0x30, 0x83, 0xec, 0x5e, 0xe4, 0xff, 0x75, 0xfe, 0x79, - 0x26, 0x9d, 0x1d, 0x36, 0xcd, 0x0a, 0x15, 0xd2, 0x24, 0x14, 0x77, 0x71, 0xd7, 0x8a, 0x1b, 0x04])) + aff( + fe([ + 0xfd, 0x67, 0x6b, 0x71, 0x9b, 0x81, 0x53, 0x39, 0x39, 0xf4, 0xb8, 0xd5, 0xc3, 0x30, 0x9b, 0x3b, + 0x7c, 0xa3, 0xf0, 0xd0, 0x84, 0x21, 0xd6, 0xbf, 0xb7, 0x4c, 0x87, 0x13, 0x45, 0x2d, 0xa7, 0x55 + ]), + fe([ + 0x5d, 0x04, 0xb3, 0x40, 0x28, 0x95, 0x2d, 0x30, 0x83, 0xec, 0x5e, 0xe4, 0xff, 0x75, 0xfe, 0x79, + 0x26, 0x9d, 0x1d, 0x36, 0xcd, 0x0a, 0x15, 0xd2, 0x24, 0x14, 0x77, 0x71, 0xd7, 0x8a, 0x1b, 0x04 + ]) + ) ) v.append( - aff(fe([0x5d, 0x93, 0xc9, 0xbe, 0xaa, 0x90, 0xcd, 0x9b, 0xfb, 0x73, 0x7e, 0xb0, 0x64, 0x98, 0x57, 0x44, - 0x42, 0x41, 0xb1, 0xaf, 0xea, 0xc1, 0xc3, 0x22, 0xff, 0x60, 0x46, 0xcb, 0x61, 0x81, 0x70, 0x61]) , - fe([0x0d, 0x82, 0xb9, 0xfe, 0x21, 0xcd, 0xc4, 0xf5, 0x98, 0x0c, 0x4e, 0x72, 0xee, 0x87, 0x49, 0xf8, - 0xa1, 0x95, 0xdf, 0x8f, 0x2d, 0xbd, 0x21, 0x06, 0x7c, 0x15, 0xe8, 0x12, 0x6d, 0x93, 0xd6, 0x38])) + aff( + fe([ + 0x5d, 0x93, 0xc9, 0xbe, 0xaa, 0x90, 0xcd, 0x9b, 0xfb, 0x73, 0x7e, 0xb0, 0x64, 0x98, 0x57, 0x44, + 0x42, 0x41, 0xb1, 0xaf, 0xea, 0xc1, 0xc3, 0x22, 0xff, 0x60, 0x46, 0xcb, 0x61, 0x81, 0x70, 0x61 + ]), + fe([ + 0x0d, 0x82, 0xb9, 0xfe, 0x21, 0xcd, 0xc4, 0xf5, 0x98, 0x0c, 0x4e, 0x72, 0xee, 0x87, 0x49, 0xf8, + 0xa1, 0x95, 0xdf, 0x8f, 0x2d, 0xbd, 0x21, 0x06, 0x7c, 0x15, 0xe8, 0x12, 0x6d, 0x93, 0xd6, 0x38 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x91, 0xf7, 0x51, 0xd9, 0xef, 0x7d, 0x42, 0x01, 0x13, 0xe9, 0xb8, 0x7f, 0xa6, 0x49, 0x17, 0x64, - 0x21, 0x80, 0x83, 0x2c, 0x63, 0x4c, 0x60, 0x09, 0x59, 0x91, 0x92, 0x77, 0x39, 0x51, 0xf4, 0x48]) , - fe([0x60, 0xd5, 0x22, 0x83, 0x08, 0x2f, 0xff, 0x99, 0x3e, 0x69, 0x6d, 0x88, 0xda, 0xe7, 0x5b, 0x52, - 0x26, 0x31, 0x2a, 0xe5, 0x89, 0xde, 0x68, 0x90, 0xb6, 0x22, 0x5a, 0xbd, 0xd3, 0x85, 0x53, 0x31])) + aff( + fe([ + 0x91, 0xf7, 0x51, 0xd9, 0xef, 0x7d, 0x42, 0x01, 0x13, 0xe9, 0xb8, 0x7f, 0xa6, 0x49, 0x17, 0x64, + 0x21, 0x80, 0x83, 0x2c, 0x63, 0x4c, 0x60, 0x09, 0x59, 0x91, 0x92, 0x77, 0x39, 0x51, 0xf4, 0x48 + ]), + fe([ + 0x60, 0xd5, 0x22, 0x83, 0x08, 0x2f, 0xff, 0x99, 0x3e, 0x69, 0x6d, 0x88, 0xda, 0xe7, 0x5b, 0x52, + 0x26, 0x31, 0x2a, 0xe5, 0x89, 0xde, 0x68, 0x90, 0xb6, 0x22, 0x5a, 0xbd, 0xd3, 0x85, 0x53, 0x31 + ]) + ) ) v.append( - aff(fe([0xd8, 0xce, 0xdc, 0xf9, 0x3c, 0x4b, 0xa2, 0x1d, 0x2c, 0x2f, 0x36, 0xbe, 0x7a, 0xfc, 0xcd, 0xbc, - 0xdc, 0xf9, 0x30, 0xbd, 0xff, 0x05, 0xc7, 0xe4, 0x8e, 0x17, 0x62, 0xf8, 0x4d, 0xa0, 0x56, 0x79]) , - fe([0x82, 0xe7, 0xf6, 0xba, 0x53, 0x84, 0x0a, 0xa3, 0x34, 0xff, 0x3c, 0xa3, 0x6a, 0xa1, 0x37, 0xea, - 0xdd, 0xb6, 0x95, 0xb3, 0x78, 0x19, 0x76, 0x1e, 0x55, 0x2f, 0x77, 0x2e, 0x7f, 0xc1, 0xea, 0x5e])) + aff( + fe([ + 0xd8, 0xce, 0xdc, 0xf9, 0x3c, 0x4b, 0xa2, 0x1d, 0x2c, 0x2f, 0x36, 0xbe, 0x7a, 0xfc, 0xcd, 0xbc, + 0xdc, 0xf9, 0x30, 0xbd, 0xff, 0x05, 0xc7, 0xe4, 0x8e, 0x17, 0x62, 0xf8, 0x4d, 0xa0, 0x56, 0x79 + ]), + fe([ + 0x82, 0xe7, 0xf6, 0xba, 0x53, 0x84, 0x0a, 0xa3, 0x34, 0xff, 0x3c, 0xa3, 0x6a, 0xa1, 0x37, 0xea, + 0xdd, 0xb6, 0x95, 0xb3, 0x78, 0x19, 0x76, 0x1e, 0x55, 0x2f, 0x77, 0x2e, 0x7f, 0xc1, 0xea, 0x5e + ]) + ) ) v.append( - aff(fe([0x83, 0xe1, 0x6e, 0xa9, 0x07, 0x33, 0x3e, 0x83, 0xff, 0xcb, 0x1c, 0x9f, 0xb1, 0xa3, 0xb4, 0xc9, - 0xe1, 0x07, 0x97, 0xff, 0xf8, 0x23, 0x8f, 0xce, 0x40, 0xfd, 0x2e, 0x5e, 0xdb, 0x16, 0x43, 0x2d]) , - fe([0xba, 0x38, 0x02, 0xf7, 0x81, 0x43, 0x83, 0xa3, 0x20, 0x4f, 0x01, 0x3b, 0x8a, 0x04, 0x38, 0x31, - 0xc6, 0x0f, 0xc8, 0xdf, 0xd7, 0xfa, 0x2f, 0x88, 0x3f, 0xfc, 0x0c, 0x76, 0xc4, 0xa6, 0x45, 0x72])) + aff( + fe([ + 0x83, 0xe1, 0x6e, 0xa9, 0x07, 0x33, 0x3e, 0x83, 0xff, 0xcb, 0x1c, 0x9f, 0xb1, 0xa3, 0xb4, 0xc9, + 0xe1, 0x07, 0x97, 0xff, 0xf8, 0x23, 0x8f, 0xce, 0x40, 0xfd, 0x2e, 0x5e, 0xdb, 0x16, 0x43, 0x2d + ]), + fe([ + 0xba, 0x38, 0x02, 0xf7, 0x81, 0x43, 0x83, 0xa3, 0x20, 0x4f, 0x01, 0x3b, 0x8a, 0x04, 0x38, 0x31, + 0xc6, 0x0f, 0xc8, 0xdf, 0xd7, 0xfa, 0x2f, 0x88, 0x3f, 0xfc, 0x0c, 0x76, 0xc4, 0xa6, 0x45, 0x72 + ]) + ) ) v.append( - aff(fe([0xbb, 0x0c, 0xbc, 0x6a, 0xa4, 0x97, 0x17, 0x93, 0x2d, 0x6f, 0xde, 0x72, 0x10, 0x1c, 0x08, 0x2c, - 0x0f, 0x80, 0x32, 0x68, 0x27, 0xd4, 0xab, 0xdd, 0xc5, 0x58, 0x61, 0x13, 0x6d, 0x11, 0x1e, 0x4d]) , - fe([0x1a, 0xb9, 0xc9, 0x10, 0xfb, 0x1e, 0x4e, 0xf4, 0x84, 0x4b, 0x8a, 0x5e, 0x7b, 0x4b, 0xe8, 0x43, - 0x8c, 0x8f, 0x00, 0xb5, 0x54, 0x13, 0xc5, 0x5c, 0xb6, 0x35, 0x4e, 0x9d, 0xe4, 0x5b, 0x41, 0x6d])) + aff( + fe([ + 0xbb, 0x0c, 0xbc, 0x6a, 0xa4, 0x97, 0x17, 0x93, 0x2d, 0x6f, 0xde, 0x72, 0x10, 0x1c, 0x08, 0x2c, + 0x0f, 0x80, 0x32, 0x68, 0x27, 0xd4, 0xab, 0xdd, 0xc5, 0x58, 0x61, 0x13, 0x6d, 0x11, 0x1e, 0x4d + ]), + fe([ + 0x1a, 0xb9, 0xc9, 0x10, 0xfb, 0x1e, 0x4e, 0xf4, 0x84, 0x4b, 0x8a, 0x5e, 0x7b, 0x4b, 0xe8, 0x43, + 0x8c, 0x8f, 0x00, 0xb5, 0x54, 0x13, 0xc5, 0x5c, 0xb6, 0x35, 0x4e, 0x9d, 0xe4, 0x5b, 0x41, 0x6d + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x15, 0x7d, 0x12, 0x48, 0x82, 0x14, 0x42, 0xcd, 0x32, 0xd4, 0x4b, 0xc1, 0x72, 0x61, 0x2a, 0x8c, - 0xec, 0xe2, 0xf8, 0x24, 0x45, 0x94, 0xe3, 0xbe, 0xdd, 0x67, 0xa8, 0x77, 0x5a, 0xae, 0x5b, 0x4b]) , - fe([0xcb, 0x77, 0x9a, 0x20, 0xde, 0xb8, 0x23, 0xd9, 0xa0, 0x0f, 0x8c, 0x7b, 0xa5, 0xcb, 0xae, 0xb6, - 0xec, 0x42, 0x67, 0x0e, 0x58, 0xa4, 0x75, 0x98, 0x21, 0x71, 0x84, 0xb3, 0xe0, 0x76, 0x94, 0x73])) + aff( + fe([ + 0x15, 0x7d, 0x12, 0x48, 0x82, 0x14, 0x42, 0xcd, 0x32, 0xd4, 0x4b, 0xc1, 0x72, 0x61, 0x2a, 0x8c, + 0xec, 0xe2, 0xf8, 0x24, 0x45, 0x94, 0xe3, 0xbe, 0xdd, 0x67, 0xa8, 0x77, 0x5a, 0xae, 0x5b, 0x4b + ]), + fe([ + 0xcb, 0x77, 0x9a, 0x20, 0xde, 0xb8, 0x23, 0xd9, 0xa0, 0x0f, 0x8c, 0x7b, 0xa5, 0xcb, 0xae, 0xb6, + 0xec, 0x42, 0x67, 0x0e, 0x58, 0xa4, 0x75, 0x98, 0x21, 0x71, 0x84, 0xb3, 0xe0, 0x76, 0x94, 0x73 + ]) + ) ) v.append( - aff(fe([0xdf, 0xfc, 0x69, 0x28, 0x23, 0x3f, 0x5b, 0xf8, 0x3b, 0x24, 0x37, 0xf3, 0x1d, 0xd5, 0x22, 0x6b, - 0xd0, 0x98, 0xa8, 0x6c, 0xcf, 0xff, 0x06, 0xe1, 0x13, 0xdf, 0xb9, 0xc1, 0x0c, 0xa9, 0xbf, 0x33]) , - fe([0xd9, 0x81, 0xda, 0xb2, 0x4f, 0x82, 0x9d, 0x43, 0x81, 0x09, 0xf1, 0xd2, 0x01, 0xef, 0xac, 0xf4, - 0x2d, 0x7d, 0x01, 0x09, 0xf1, 0xff, 0xa5, 0x9f, 0xe5, 0xca, 0x27, 0x63, 0xdb, 0x20, 0xb1, 0x53])) + aff( + fe([ + 0xdf, 0xfc, 0x69, 0x28, 0x23, 0x3f, 0x5b, 0xf8, 0x3b, 0x24, 0x37, 0xf3, 0x1d, 0xd5, 0x22, 0x6b, + 0xd0, 0x98, 0xa8, 0x6c, 0xcf, 0xff, 0x06, 0xe1, 0x13, 0xdf, 0xb9, 0xc1, 0x0c, 0xa9, 0xbf, 0x33 + ]), + fe([ + 0xd9, 0x81, 0xda, 0xb2, 0x4f, 0x82, 0x9d, 0x43, 0x81, 0x09, 0xf1, 0xd2, 0x01, 0xef, 0xac, 0xf4, + 0x2d, 0x7d, 0x01, 0x09, 0xf1, 0xff, 0xa5, 0x9f, 0xe5, 0xca, 0x27, 0x63, 0xdb, 0x20, 0xb1, 0x53 + ]) + ) ) v.append( - aff(fe([0x67, 0x02, 0xe8, 0xad, 0xa9, 0x34, 0xd4, 0xf0, 0x15, 0x81, 0xaa, 0xc7, 0x4d, 0x87, 0x94, 0xea, - 0x75, 0xe7, 0x4c, 0x94, 0x04, 0x0e, 0x69, 0x87, 0xe7, 0x51, 0x91, 0x10, 0x03, 0xc7, 0xbe, 0x56]) , - fe([0x32, 0xfb, 0x86, 0xec, 0x33, 0x6b, 0x2e, 0x51, 0x2b, 0xc8, 0xfa, 0x6c, 0x70, 0x47, 0x7e, 0xce, - 0x05, 0x0c, 0x71, 0xf3, 0xb4, 0x56, 0xa6, 0xdc, 0xcc, 0x78, 0x07, 0x75, 0xd0, 0xdd, 0xb2, 0x6a])) + aff( + fe([ + 0x67, 0x02, 0xe8, 0xad, 0xa9, 0x34, 0xd4, 0xf0, 0x15, 0x81, 0xaa, 0xc7, 0x4d, 0x87, 0x94, 0xea, + 0x75, 0xe7, 0x4c, 0x94, 0x04, 0x0e, 0x69, 0x87, 0xe7, 0x51, 0x91, 0x10, 0x03, 0xc7, 0xbe, 0x56 + ]), + fe([ + 0x32, 0xfb, 0x86, 0xec, 0x33, 0x6b, 0x2e, 0x51, 0x2b, 0xc8, 0xfa, 0x6c, 0x70, 0x47, 0x7e, 0xce, + 0x05, 0x0c, 0x71, 0xf3, 0xb4, 0x56, 0xa6, 0xdc, 0xcc, 0x78, 0x07, 0x75, 0xd0, 0xdd, 0xb2, 0x6a + ]) + ) ) v.append( - aff(fe([0xc6, 0xef, 0xb9, 0xc0, 0x2b, 0x22, 0x08, 0x1e, 0x71, 0x70, 0xb3, 0x35, 0x9c, 0x7a, 0x01, 0x92, - 0x44, 0x9a, 0xf6, 0xb0, 0x58, 0x95, 0xc1, 0x9b, 0x02, 0xed, 0x2d, 0x7c, 0x34, 0x29, 0x49, 0x44]) , - fe([0x45, 0x62, 0x1d, 0x2e, 0xff, 0x2a, 0x1c, 0x21, 0xa4, 0x25, 0x7b, 0x0d, 0x8c, 0x15, 0x39, 0xfc, - 0x8f, 0x7c, 0xa5, 0x7d, 0x1e, 0x25, 0xa3, 0x45, 0xd6, 0xab, 0xbd, 0xcb, 0xc5, 0x5e, 0x78, 0x77])) + aff( + fe([ + 0xc6, 0xef, 0xb9, 0xc0, 0x2b, 0x22, 0x08, 0x1e, 0x71, 0x70, 0xb3, 0x35, 0x9c, 0x7a, 0x01, 0x92, + 0x44, 0x9a, 0xf6, 0xb0, 0x58, 0x95, 0xc1, 0x9b, 0x02, 0xed, 0x2d, 0x7c, 0x34, 0x29, 0x49, 0x44 + ]), + fe([ + 0x45, 0x62, 0x1d, 0x2e, 0xff, 0x2a, 0x1c, 0x21, 0xa4, 0x25, 0x7b, 0x0d, 0x8c, 0x15, 0x39, 0xfc, + 0x8f, 0x7c, 0xa5, 0x7d, 0x1e, 0x25, 0xa3, 0x45, 0xd6, 0xab, 0xbd, 0xcb, 0xc5, 0x5e, 0x78, 0x77 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xd0, 0xd3, 0x42, 0xed, 0x1d, 0x00, 0x3c, 0x15, 0x2c, 0x9c, 0x77, 0x81, 0xd2, 0x73, 0xd1, 0x06, - 0xd5, 0xc4, 0x7f, 0x94, 0xbb, 0x92, 0x2d, 0x2c, 0x4b, 0x45, 0x4b, 0xe9, 0x2a, 0x89, 0x6b, 0x2b]) , - fe([0xd2, 0x0c, 0x88, 0xc5, 0x48, 0x4d, 0xea, 0x0d, 0x4a, 0xc9, 0x52, 0x6a, 0x61, 0x79, 0xe9, 0x76, - 0xf3, 0x85, 0x52, 0x5c, 0x1b, 0x2c, 0xe1, 0xd6, 0xc4, 0x0f, 0x18, 0x0e, 0x4e, 0xf6, 0x1c, 0x7f])) + aff( + fe([ + 0xd0, 0xd3, 0x42, 0xed, 0x1d, 0x00, 0x3c, 0x15, 0x2c, 0x9c, 0x77, 0x81, 0xd2, 0x73, 0xd1, 0x06, + 0xd5, 0xc4, 0x7f, 0x94, 0xbb, 0x92, 0x2d, 0x2c, 0x4b, 0x45, 0x4b, 0xe9, 0x2a, 0x89, 0x6b, 0x2b + ]), + fe([ + 0xd2, 0x0c, 0x88, 0xc5, 0x48, 0x4d, 0xea, 0x0d, 0x4a, 0xc9, 0x52, 0x6a, 0x61, 0x79, 0xe9, 0x76, + 0xf3, 0x85, 0x52, 0x5c, 0x1b, 0x2c, 0xe1, 0xd6, 0xc4, 0x0f, 0x18, 0x0e, 0x4e, 0xf6, 0x1c, 0x7f + ]) + ) ) v.append( - aff(fe([0xb4, 0x04, 0x2e, 0x42, 0xcb, 0x1f, 0x2b, 0x11, 0x51, 0x7b, 0x08, 0xac, 0xaa, 0x3e, 0x9e, 0x52, - 0x60, 0xb7, 0xc2, 0x61, 0x57, 0x8c, 0x84, 0xd5, 0x18, 0xa6, 0x19, 0xfc, 0xb7, 0x75, 0x91, 0x1b]) , - fe([0xe8, 0x68, 0xca, 0x44, 0xc8, 0x38, 0x38, 0xcc, 0x53, 0x0a, 0x32, 0x35, 0xcc, 0x52, 0xcb, 0x0e, - 0xf7, 0xc5, 0xe7, 0xec, 0x3d, 0x85, 0xcc, 0x58, 0xe2, 0x17, 0x47, 0xff, 0x9f, 0xa5, 0x30, 0x17])) + aff( + fe([ + 0xb4, 0x04, 0x2e, 0x42, 0xcb, 0x1f, 0x2b, 0x11, 0x51, 0x7b, 0x08, 0xac, 0xaa, 0x3e, 0x9e, 0x52, + 0x60, 0xb7, 0xc2, 0x61, 0x57, 0x8c, 0x84, 0xd5, 0x18, 0xa6, 0x19, 0xfc, 0xb7, 0x75, 0x91, 0x1b + ]), + fe([ + 0xe8, 0x68, 0xca, 0x44, 0xc8, 0x38, 0x38, 0xcc, 0x53, 0x0a, 0x32, 0x35, 0xcc, 0x52, 0xcb, 0x0e, + 0xf7, 0xc5, 0xe7, 0xec, 0x3d, 0x85, 0xcc, 0x58, 0xe2, 0x17, 0x47, 0xff, 0x9f, 0xa5, 0x30, 0x17 + ]) + ) ) v.append( - aff(fe([0xe3, 0xae, 0xc8, 0xc1, 0x71, 0x75, 0x31, 0x00, 0x37, 0x41, 0x5c, 0x0e, 0x39, 0xda, 0x73, 0xa0, - 0xc7, 0x97, 0x36, 0x6c, 0x5b, 0xf2, 0xee, 0x64, 0x0a, 0x3d, 0x89, 0x1e, 0x1d, 0x49, 0x8c, 0x37]) , - fe([0x4c, 0xe6, 0xb0, 0xc1, 0xa5, 0x2a, 0x82, 0x09, 0x08, 0xad, 0x79, 0x9c, 0x56, 0xf6, 0xf9, 0xc1, - 0xd7, 0x7c, 0x39, 0x7f, 0x93, 0xca, 0x11, 0x55, 0xbf, 0x07, 0x1b, 0x82, 0x29, 0x69, 0x95, 0x5c])) + aff( + fe([ + 0xe3, 0xae, 0xc8, 0xc1, 0x71, 0x75, 0x31, 0x00, 0x37, 0x41, 0x5c, 0x0e, 0x39, 0xda, 0x73, 0xa0, + 0xc7, 0x97, 0x36, 0x6c, 0x5b, 0xf2, 0xee, 0x64, 0x0a, 0x3d, 0x89, 0x1e, 0x1d, 0x49, 0x8c, 0x37 + ]), + fe([ + 0x4c, 0xe6, 0xb0, 0xc1, 0xa5, 0x2a, 0x82, 0x09, 0x08, 0xad, 0x79, 0x9c, 0x56, 0xf6, 0xf9, 0xc1, + 0xd7, 0x7c, 0x39, 0x7f, 0x93, 0xca, 0x11, 0x55, 0xbf, 0x07, 0x1b, 0x82, 0x29, 0x69, 0x95, 0x5c + ]) + ) ) v.append( - aff(fe([0x87, 0xee, 0xa6, 0x56, 0x9e, 0xc2, 0x9a, 0x56, 0x24, 0x42, 0x85, 0x4d, 0x98, 0x31, 0x1e, 0x60, - 0x4d, 0x87, 0x85, 0x04, 0xae, 0x46, 0x12, 0xf9, 0x8e, 0x7f, 0xe4, 0x7f, 0xf6, 0x1c, 0x37, 0x01]) , - fe([0x73, 0x4c, 0xb6, 0xc5, 0xc4, 0xe9, 0x6c, 0x85, 0x48, 0x4a, 0x5a, 0xac, 0xd9, 0x1f, 0x43, 0xf8, - 0x62, 0x5b, 0xee, 0x98, 0x2a, 0x33, 0x8e, 0x79, 0xce, 0x61, 0x06, 0x35, 0xd8, 0xd7, 0xca, 0x71])) + aff( + fe([ + 0x87, 0xee, 0xa6, 0x56, 0x9e, 0xc2, 0x9a, 0x56, 0x24, 0x42, 0x85, 0x4d, 0x98, 0x31, 0x1e, 0x60, + 0x4d, 0x87, 0x85, 0x04, 0xae, 0x46, 0x12, 0xf9, 0x8e, 0x7f, 0xe4, 0x7f, 0xf6, 0x1c, 0x37, 0x01 + ]), + fe([ + 0x73, 0x4c, 0xb6, 0xc5, 0xc4, 0xe9, 0x6c, 0x85, 0x48, 0x4a, 0x5a, 0xac, 0xd9, 0x1f, 0x43, 0xf8, + 0x62, 0x5b, 0xee, 0x98, 0x2a, 0x33, 0x8e, 0x79, 0xce, 0x61, 0x06, 0x35, 0xd8, 0xd7, 0xca, 0x71 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x72, 0xd3, 0xae, 0xa6, 0xca, 0x8f, 0xcd, 0xcc, 0x78, 0x8e, 0x19, 0x4d, 0xa7, 0xd2, 0x27, 0xe9, - 0xa4, 0x3c, 0x16, 0x5b, 0x84, 0x80, 0xf9, 0xd0, 0xcc, 0x6a, 0x1e, 0xca, 0x1e, 0x67, 0xbd, 0x63]) , - fe([0x7b, 0x6e, 0x2a, 0xd2, 0x87, 0x48, 0xff, 0xa1, 0xca, 0xe9, 0x15, 0x85, 0xdc, 0xdb, 0x2c, 0x39, - 0x12, 0x91, 0xa9, 0x20, 0xaa, 0x4f, 0x29, 0xf4, 0x15, 0x7a, 0xd2, 0xf5, 0x32, 0xcc, 0x60, 0x04])) + aff( + fe([ + 0x72, 0xd3, 0xae, 0xa6, 0xca, 0x8f, 0xcd, 0xcc, 0x78, 0x8e, 0x19, 0x4d, 0xa7, 0xd2, 0x27, 0xe9, + 0xa4, 0x3c, 0x16, 0x5b, 0x84, 0x80, 0xf9, 0xd0, 0xcc, 0x6a, 0x1e, 0xca, 0x1e, 0x67, 0xbd, 0x63 + ]), + fe([ + 0x7b, 0x6e, 0x2a, 0xd2, 0x87, 0x48, 0xff, 0xa1, 0xca, 0xe9, 0x15, 0x85, 0xdc, 0xdb, 0x2c, 0x39, + 0x12, 0x91, 0xa9, 0x20, 0xaa, 0x4f, 0x29, 0xf4, 0x15, 0x7a, 0xd2, 0xf5, 0x32, 0xcc, 0x60, 0x04 + ]) + ) ) v.append( - aff(fe([0xe5, 0x10, 0x47, 0x3b, 0xfa, 0x90, 0xfc, 0x30, 0xb5, 0xea, 0x6f, 0x56, 0x8f, 0xfb, 0x0e, 0xa7, - 0x3b, 0xc8, 0xb2, 0xff, 0x02, 0x7a, 0x33, 0x94, 0x93, 0x2a, 0x03, 0xe0, 0x96, 0x3a, 0x6c, 0x0f]) , - fe([0x5a, 0x63, 0x67, 0xe1, 0x9b, 0x47, 0x78, 0x9f, 0x38, 0x79, 0xac, 0x97, 0x66, 0x1d, 0x5e, 0x51, - 0xee, 0x24, 0x42, 0xe8, 0x58, 0x4b, 0x8a, 0x03, 0x75, 0x86, 0x37, 0x86, 0xe2, 0x97, 0x4e, 0x3d])) + aff( + fe([ + 0xe5, 0x10, 0x47, 0x3b, 0xfa, 0x90, 0xfc, 0x30, 0xb5, 0xea, 0x6f, 0x56, 0x8f, 0xfb, 0x0e, 0xa7, + 0x3b, 0xc8, 0xb2, 0xff, 0x02, 0x7a, 0x33, 0x94, 0x93, 0x2a, 0x03, 0xe0, 0x96, 0x3a, 0x6c, 0x0f + ]), + fe([ + 0x5a, 0x63, 0x67, 0xe1, 0x9b, 0x47, 0x78, 0x9f, 0x38, 0x79, 0xac, 0x97, 0x66, 0x1d, 0x5e, 0x51, + 0xee, 0x24, 0x42, 0xe8, 0x58, 0x4b, 0x8a, 0x03, 0x75, 0x86, 0x37, 0x86, 0xe2, 0x97, 0x4e, 0x3d + ]) + ) ) v.append( - aff(fe([0x3f, 0x75, 0x8e, 0xb4, 0xff, 0xd8, 0xdd, 0xd6, 0x37, 0x57, 0x9d, 0x6d, 0x3b, 0xbd, 0xd5, 0x60, - 0x88, 0x65, 0x9a, 0xb9, 0x4a, 0x68, 0x84, 0xa2, 0x67, 0xdd, 0x17, 0x25, 0x97, 0x04, 0x8b, 0x5e]) , - fe([0xbb, 0x40, 0x5e, 0xbc, 0x16, 0x92, 0x05, 0xc4, 0xc0, 0x4e, 0x72, 0x90, 0x0e, 0xab, 0xcf, 0x8a, - 0xed, 0xef, 0xb9, 0x2d, 0x3b, 0xf8, 0x43, 0x5b, 0xba, 0x2d, 0xeb, 0x2f, 0x52, 0xd2, 0xd1, 0x5a])) + aff( + fe([ + 0x3f, 0x75, 0x8e, 0xb4, 0xff, 0xd8, 0xdd, 0xd6, 0x37, 0x57, 0x9d, 0x6d, 0x3b, 0xbd, 0xd5, 0x60, + 0x88, 0x65, 0x9a, 0xb9, 0x4a, 0x68, 0x84, 0xa2, 0x67, 0xdd, 0x17, 0x25, 0x97, 0x04, 0x8b, 0x5e + ]), + fe([ + 0xbb, 0x40, 0x5e, 0xbc, 0x16, 0x92, 0x05, 0xc4, 0xc0, 0x4e, 0x72, 0x90, 0x0e, 0xab, 0xcf, 0x8a, + 0xed, 0xef, 0xb9, 0x2d, 0x3b, 0xf8, 0x43, 0x5b, 0xba, 0x2d, 0xeb, 0x2f, 0x52, 0xd2, 0xd1, 0x5a + ]) + ) ) v.append( - aff(fe([0x40, 0xb4, 0xab, 0xe6, 0xad, 0x9f, 0x46, 0x69, 0x4a, 0xb3, 0x8e, 0xaa, 0xea, 0x9c, 0x8a, 0x20, - 0x16, 0x5d, 0x8c, 0x13, 0xbd, 0xf6, 0x1d, 0xc5, 0x24, 0xbd, 0x90, 0x2a, 0x1c, 0xc7, 0x13, 0x3b]) , - fe([0x54, 0xdc, 0x16, 0x0d, 0x18, 0xbe, 0x35, 0x64, 0x61, 0x52, 0x02, 0x80, 0xaf, 0x05, 0xf7, 0xa6, - 0x42, 0xd3, 0x8f, 0x2e, 0x79, 0x26, 0xa8, 0xbb, 0xb2, 0x17, 0x48, 0xb2, 0x7a, 0x0a, 0x89, 0x14])) + aff( + fe([ + 0x40, 0xb4, 0xab, 0xe6, 0xad, 0x9f, 0x46, 0x69, 0x4a, 0xb3, 0x8e, 0xaa, 0xea, 0x9c, 0x8a, 0x20, + 0x16, 0x5d, 0x8c, 0x13, 0xbd, 0xf6, 0x1d, 0xc5, 0x24, 0xbd, 0x90, 0x2a, 0x1c, 0xc7, 0x13, 0x3b + ]), + fe([ + 0x54, 0xdc, 0x16, 0x0d, 0x18, 0xbe, 0x35, 0x64, 0x61, 0x52, 0x02, 0x80, 0xaf, 0x05, 0xf7, 0xa6, + 0x42, 0xd3, 0x8f, 0x2e, 0x79, 0x26, 0xa8, 0xbb, 0xb2, 0x17, 0x48, 0xb2, 0x7a, 0x0a, 0x89, 0x14 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x20, 0xa8, 0x88, 0xe3, 0x91, 0xc0, 0x6e, 0xbb, 0x8a, 0x27, 0x82, 0x51, 0x83, 0xb2, 0x28, 0xa9, - 0x83, 0xeb, 0xa6, 0xa9, 0x4d, 0x17, 0x59, 0x22, 0x54, 0x00, 0x50, 0x45, 0xcb, 0x48, 0x4b, 0x18]) , - fe([0x33, 0x7c, 0xe7, 0x26, 0xba, 0x4d, 0x32, 0xfe, 0x53, 0xf4, 0xfa, 0x83, 0xe3, 0xa5, 0x79, 0x66, - 0x73, 0xef, 0x80, 0x23, 0x68, 0xc2, 0x60, 0xdd, 0xa9, 0x33, 0xdc, 0x03, 0x7a, 0xe0, 0xe0, 0x3e])) + aff( + fe([ + 0x20, 0xa8, 0x88, 0xe3, 0x91, 0xc0, 0x6e, 0xbb, 0x8a, 0x27, 0x82, 0x51, 0x83, 0xb2, 0x28, 0xa9, + 0x83, 0xeb, 0xa6, 0xa9, 0x4d, 0x17, 0x59, 0x22, 0x54, 0x00, 0x50, 0x45, 0xcb, 0x48, 0x4b, 0x18 + ]), + fe([ + 0x33, 0x7c, 0xe7, 0x26, 0xba, 0x4d, 0x32, 0xfe, 0x53, 0xf4, 0xfa, 0x83, 0xe3, 0xa5, 0x79, 0x66, + 0x73, 0xef, 0x80, 0x23, 0x68, 0xc2, 0x60, 0xdd, 0xa9, 0x33, 0xdc, 0x03, 0x7a, 0xe0, 0xe0, 0x3e + ]) + ) ) v.append( - aff(fe([0x34, 0x5c, 0x13, 0xfb, 0xc0, 0xe3, 0x78, 0x2b, 0x54, 0x58, 0x22, 0x9b, 0x76, 0x81, 0x7f, 0x93, - 0x9c, 0x25, 0x3c, 0xd2, 0xe9, 0x96, 0x21, 0x26, 0x08, 0xf5, 0xed, 0x95, 0x11, 0xae, 0x04, 0x5a]) , - fe([0xb9, 0xe8, 0xc5, 0x12, 0x97, 0x1f, 0x83, 0xfe, 0x3e, 0x94, 0x99, 0xd4, 0x2d, 0xf9, 0x52, 0x59, - 0x5c, 0x82, 0xa6, 0xf0, 0x75, 0x7e, 0xe8, 0xec, 0xcc, 0xac, 0x18, 0x21, 0x09, 0x67, 0x66, 0x67])) + aff( + fe([ + 0x34, 0x5c, 0x13, 0xfb, 0xc0, 0xe3, 0x78, 0x2b, 0x54, 0x58, 0x22, 0x9b, 0x76, 0x81, 0x7f, 0x93, + 0x9c, 0x25, 0x3c, 0xd2, 0xe9, 0x96, 0x21, 0x26, 0x08, 0xf5, 0xed, 0x95, 0x11, 0xae, 0x04, 0x5a + ]), + fe([ + 0xb9, 0xe8, 0xc5, 0x12, 0x97, 0x1f, 0x83, 0xfe, 0x3e, 0x94, 0x99, 0xd4, 0x2d, 0xf9, 0x52, 0x59, + 0x5c, 0x82, 0xa6, 0xf0, 0x75, 0x7e, 0xe8, 0xec, 0xcc, 0xac, 0x18, 0x21, 0x09, 0x67, 0x66, 0x67 + ]) + ) ) v.append( - aff(fe([0xb3, 0x40, 0x29, 0xd1, 0xcb, 0x1b, 0x08, 0x9e, 0x9c, 0xb7, 0x53, 0xb9, 0x3b, 0x71, 0x08, 0x95, - 0x12, 0x1a, 0x58, 0xaf, 0x7e, 0x82, 0x52, 0x43, 0x4f, 0x11, 0x39, 0xf4, 0x93, 0x1a, 0x26, 0x05]) , - fe([0x6e, 0x44, 0xa3, 0xf9, 0x64, 0xaf, 0xe7, 0x6d, 0x7d, 0xdf, 0x1e, 0xac, 0x04, 0xea, 0x3b, 0x5f, - 0x9b, 0xe8, 0x24, 0x9d, 0x0e, 0xe5, 0x2e, 0x3e, 0xdf, 0xa9, 0xf7, 0xd4, 0x50, 0x71, 0xf0, 0x78])) + aff( + fe([ + 0xb3, 0x40, 0x29, 0xd1, 0xcb, 0x1b, 0x08, 0x9e, 0x9c, 0xb7, 0x53, 0xb9, 0x3b, 0x71, 0x08, 0x95, + 0x12, 0x1a, 0x58, 0xaf, 0x7e, 0x82, 0x52, 0x43, 0x4f, 0x11, 0x39, 0xf4, 0x93, 0x1a, 0x26, 0x05 + ]), + fe([ + 0x6e, 0x44, 0xa3, 0xf9, 0x64, 0xaf, 0xe7, 0x6d, 0x7d, 0xdf, 0x1e, 0xac, 0x04, 0xea, 0x3b, 0x5f, + 0x9b, 0xe8, 0x24, 0x9d, 0x0e, 0xe5, 0x2e, 0x3e, 0xdf, 0xa9, 0xf7, 0xd4, 0x50, 0x71, 0xf0, 0x78 + ]) + ) ) v.append( - aff(fe([0x3e, 0xa8, 0x38, 0xc2, 0x57, 0x56, 0x42, 0x9a, 0xb1, 0xe2, 0xf8, 0x45, 0xaa, 0x11, 0x48, 0x5f, - 0x17, 0xc4, 0x54, 0x27, 0xdc, 0x5d, 0xaa, 0xdd, 0x41, 0xbc, 0xdf, 0x81, 0xb9, 0x53, 0xee, 0x52]) , - fe([0xc3, 0xf1, 0xa7, 0x6d, 0xb3, 0x5f, 0x92, 0x6f, 0xcc, 0x91, 0xb8, 0x95, 0x05, 0xdf, 0x3c, 0x64, - 0x57, 0x39, 0x61, 0x51, 0xad, 0x8c, 0x38, 0x7b, 0xc8, 0xde, 0x00, 0x34, 0xbe, 0xa1, 0xb0, 0x7e])) + aff( + fe([ + 0x3e, 0xa8, 0x38, 0xc2, 0x57, 0x56, 0x42, 0x9a, 0xb1, 0xe2, 0xf8, 0x45, 0xaa, 0x11, 0x48, 0x5f, + 0x17, 0xc4, 0x54, 0x27, 0xdc, 0x5d, 0xaa, 0xdd, 0x41, 0xbc, 0xdf, 0x81, 0xb9, 0x53, 0xee, 0x52 + ]), + fe([ + 0xc3, 0xf1, 0xa7, 0x6d, 0xb3, 0x5f, 0x92, 0x6f, 0xcc, 0x91, 0xb8, 0x95, 0x05, 0xdf, 0x3c, 0x64, + 0x57, 0x39, 0x61, 0x51, 0xad, 0x8c, 0x38, 0x7b, 0xc8, 0xde, 0x00, 0x34, 0xbe, 0xa1, 0xb0, 0x7e + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x25, 0x24, 0x1d, 0x8a, 0x67, 0x20, 0xee, 0x42, 0xeb, 0x38, 0xed, 0x0b, 0x8b, 0xcd, 0x46, 0x9d, - 0x5e, 0x6b, 0x1e, 0x24, 0x9d, 0x12, 0x05, 0x1a, 0xcc, 0x05, 0x4e, 0x92, 0x38, 0xe1, 0x1f, 0x50]) , - fe([0x4e, 0xee, 0x1c, 0x91, 0xe6, 0x11, 0xbd, 0x8e, 0x55, 0x1a, 0x18, 0x75, 0x66, 0xaf, 0x4d, 0x7b, - 0x0f, 0xae, 0x6d, 0x85, 0xca, 0x82, 0x58, 0x21, 0x9c, 0x18, 0xe0, 0xed, 0xec, 0x22, 0x80, 0x2f])) + aff( + fe([ + 0x25, 0x24, 0x1d, 0x8a, 0x67, 0x20, 0xee, 0x42, 0xeb, 0x38, 0xed, 0x0b, 0x8b, 0xcd, 0x46, 0x9d, + 0x5e, 0x6b, 0x1e, 0x24, 0x9d, 0x12, 0x05, 0x1a, 0xcc, 0x05, 0x4e, 0x92, 0x38, 0xe1, 0x1f, 0x50 + ]), + fe([ + 0x4e, 0xee, 0x1c, 0x91, 0xe6, 0x11, 0xbd, 0x8e, 0x55, 0x1a, 0x18, 0x75, 0x66, 0xaf, 0x4d, 0x7b, + 0x0f, 0xae, 0x6d, 0x85, 0xca, 0x82, 0x58, 0x21, 0x9c, 0x18, 0xe0, 0xed, 0xec, 0x22, 0x80, 0x2f + ]) + ) ) v.append( - aff(fe([0x68, 0x3b, 0x0a, 0x39, 0x1d, 0x6a, 0x15, 0x57, 0xfc, 0xf0, 0x63, 0x54, 0xdb, 0x39, 0xdb, 0xe8, - 0x5c, 0x64, 0xff, 0xa0, 0x09, 0x4f, 0x3b, 0xb7, 0x32, 0x60, 0x99, 0x94, 0xfd, 0x94, 0x82, 0x2d]) , - fe([0x24, 0xf6, 0x5a, 0x44, 0xf1, 0x55, 0x2c, 0xdb, 0xea, 0x7c, 0x84, 0x7c, 0x01, 0xac, 0xe3, 0xfd, - 0xc9, 0x27, 0xc1, 0x5a, 0xb9, 0xde, 0x4f, 0x5a, 0x90, 0xdd, 0xc6, 0x67, 0xaa, 0x6f, 0x8a, 0x3a])) + aff( + fe([ + 0x68, 0x3b, 0x0a, 0x39, 0x1d, 0x6a, 0x15, 0x57, 0xfc, 0xf0, 0x63, 0x54, 0xdb, 0x39, 0xdb, 0xe8, + 0x5c, 0x64, 0xff, 0xa0, 0x09, 0x4f, 0x3b, 0xb7, 0x32, 0x60, 0x99, 0x94, 0xfd, 0x94, 0x82, 0x2d + ]), + fe([ + 0x24, 0xf6, 0x5a, 0x44, 0xf1, 0x55, 0x2c, 0xdb, 0xea, 0x7c, 0x84, 0x7c, 0x01, 0xac, 0xe3, 0xfd, + 0xc9, 0x27, 0xc1, 0x5a, 0xb9, 0xde, 0x4f, 0x5a, 0x90, 0xdd, 0xc6, 0x67, 0xaa, 0x6f, 0x8a, 0x3a + ]) + ) ) v.append( - aff(fe([0x78, 0x52, 0x87, 0xc9, 0x97, 0x63, 0xb1, 0xdd, 0x54, 0x5f, 0xc1, 0xf8, 0xf1, 0x06, 0xa6, 0xa8, - 0xa3, 0x88, 0x82, 0xd4, 0xcb, 0xa6, 0x19, 0xdd, 0xd1, 0x11, 0x87, 0x08, 0x17, 0x4c, 0x37, 0x2a]) , - fe([0xa1, 0x0c, 0xf3, 0x08, 0x43, 0xd9, 0x24, 0x1e, 0x83, 0xa7, 0xdf, 0x91, 0xca, 0xbd, 0x69, 0x47, - 0x8d, 0x1b, 0xe2, 0xb9, 0x4e, 0xb5, 0xe1, 0x76, 0xb3, 0x1c, 0x93, 0x03, 0xce, 0x5f, 0xb3, 0x5a])) + aff( + fe([ + 0x78, 0x52, 0x87, 0xc9, 0x97, 0x63, 0xb1, 0xdd, 0x54, 0x5f, 0xc1, 0xf8, 0xf1, 0x06, 0xa6, 0xa8, + 0xa3, 0x88, 0x82, 0xd4, 0xcb, 0xa6, 0x19, 0xdd, 0xd1, 0x11, 0x87, 0x08, 0x17, 0x4c, 0x37, 0x2a + ]), + fe([ + 0xa1, 0x0c, 0xf3, 0x08, 0x43, 0xd9, 0x24, 0x1e, 0x83, 0xa7, 0xdf, 0x91, 0xca, 0xbd, 0x69, 0x47, + 0x8d, 0x1b, 0xe2, 0xb9, 0x4e, 0xb5, 0xe1, 0x76, 0xb3, 0x1c, 0x93, 0x03, 0xce, 0x5f, 0xb3, 0x5a + ]) + ) ) v.append( - aff(fe([0x1d, 0xda, 0xe4, 0x61, 0x03, 0x50, 0xa9, 0x8b, 0x68, 0x18, 0xef, 0xb2, 0x1c, 0x84, 0x3b, 0xa2, - 0x44, 0x95, 0xa3, 0x04, 0x3b, 0xd6, 0x99, 0x00, 0xaf, 0x76, 0x42, 0x67, 0x02, 0x7d, 0x85, 0x56]) , - fe([0xce, 0x72, 0x0e, 0x29, 0x84, 0xb2, 0x7d, 0xd2, 0x45, 0xbe, 0x57, 0x06, 0xed, 0x7f, 0xcf, 0xed, - 0xcd, 0xef, 0x19, 0xd6, 0xbc, 0x15, 0x79, 0x64, 0xd2, 0x18, 0xe3, 0x20, 0x67, 0x3a, 0x54, 0x0b])) + aff( + fe([ + 0x1d, 0xda, 0xe4, 0x61, 0x03, 0x50, 0xa9, 0x8b, 0x68, 0x18, 0xef, 0xb2, 0x1c, 0x84, 0x3b, 0xa2, + 0x44, 0x95, 0xa3, 0x04, 0x3b, 0xd6, 0x99, 0x00, 0xaf, 0x76, 0x42, 0x67, 0x02, 0x7d, 0x85, 0x56 + ]), + fe([ + 0xce, 0x72, 0x0e, 0x29, 0x84, 0xb2, 0x7d, 0xd2, 0x45, 0xbe, 0x57, 0x06, 0xed, 0x7f, 0xcf, 0xed, + 0xcd, 0xef, 0x19, 0xd6, 0xbc, 0x15, 0x79, 0x64, 0xd2, 0x18, 0xe3, 0x20, 0x67, 0x3a, 0x54, 0x0b + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x52, 0xfd, 0x04, 0xc5, 0xfb, 0x99, 0xe7, 0xe8, 0xfb, 0x8c, 0xe1, 0x42, 0x03, 0xef, 0x9d, 0xd9, - 0x9e, 0x4d, 0xf7, 0x80, 0xcf, 0x2e, 0xcc, 0x9b, 0x45, 0xc9, 0x7b, 0x7a, 0xbc, 0x37, 0xa8, 0x52]) , - fe([0x96, 0x11, 0x41, 0x8a, 0x47, 0x91, 0xfe, 0xb6, 0xda, 0x7a, 0x54, 0x63, 0xd1, 0x14, 0x35, 0x05, - 0x86, 0x8c, 0xa9, 0x36, 0x3f, 0xf2, 0x85, 0x54, 0x4e, 0x92, 0xd8, 0x85, 0x01, 0x46, 0xd6, 0x50])) + aff( + fe([ + 0x52, 0xfd, 0x04, 0xc5, 0xfb, 0x99, 0xe7, 0xe8, 0xfb, 0x8c, 0xe1, 0x42, 0x03, 0xef, 0x9d, 0xd9, + 0x9e, 0x4d, 0xf7, 0x80, 0xcf, 0x2e, 0xcc, 0x9b, 0x45, 0xc9, 0x7b, 0x7a, 0xbc, 0x37, 0xa8, 0x52 + ]), + fe([ + 0x96, 0x11, 0x41, 0x8a, 0x47, 0x91, 0xfe, 0xb6, 0xda, 0x7a, 0x54, 0x63, 0xd1, 0x14, 0x35, 0x05, + 0x86, 0x8c, 0xa9, 0x36, 0x3f, 0xf2, 0x85, 0x54, 0x4e, 0x92, 0xd8, 0x85, 0x01, 0x46, 0xd6, 0x50 + ]) + ) ) v.append( - aff(fe([0x53, 0xcd, 0xf3, 0x86, 0x40, 0xe6, 0x39, 0x42, 0x95, 0xd6, 0xcb, 0x45, 0x1a, 0x20, 0xc8, 0x45, - 0x4b, 0x32, 0x69, 0x04, 0xb1, 0xaf, 0x20, 0x46, 0xc7, 0x6b, 0x23, 0x5b, 0x69, 0xee, 0x30, 0x3f]) , - fe([0x70, 0x83, 0x47, 0xc0, 0xdb, 0x55, 0x08, 0xa8, 0x7b, 0x18, 0x6d, 0xf5, 0x04, 0x5a, 0x20, 0x0c, - 0x4a, 0x8c, 0x60, 0xae, 0xae, 0x0f, 0x64, 0x55, 0x55, 0x2e, 0xd5, 0x1d, 0x53, 0x31, 0x42, 0x41])) + aff( + fe([ + 0x53, 0xcd, 0xf3, 0x86, 0x40, 0xe6, 0x39, 0x42, 0x95, 0xd6, 0xcb, 0x45, 0x1a, 0x20, 0xc8, 0x45, + 0x4b, 0x32, 0x69, 0x04, 0xb1, 0xaf, 0x20, 0x46, 0xc7, 0x6b, 0x23, 0x5b, 0x69, 0xee, 0x30, 0x3f + ]), + fe([ + 0x70, 0x83, 0x47, 0xc0, 0xdb, 0x55, 0x08, 0xa8, 0x7b, 0x18, 0x6d, 0xf5, 0x04, 0x5a, 0x20, 0x0c, + 0x4a, 0x8c, 0x60, 0xae, 0xae, 0x0f, 0x64, 0x55, 0x55, 0x2e, 0xd5, 0x1d, 0x53, 0x31, 0x42, 0x41 + ]) + ) ) v.append( - aff(fe([0xca, 0xfc, 0x88, 0x6b, 0x96, 0x78, 0x0a, 0x8b, 0x83, 0xdc, 0xbc, 0xaf, 0x40, 0xb6, 0x8d, 0x7f, - 0xef, 0xb4, 0xd1, 0x3f, 0xcc, 0xa2, 0x74, 0xc9, 0xc2, 0x92, 0x55, 0x00, 0xab, 0xdb, 0xbf, 0x4f]) , - fe([0x93, 0x1c, 0x06, 0x2d, 0x66, 0x65, 0x02, 0xa4, 0x97, 0x18, 0xfd, 0x00, 0xe7, 0xab, 0x03, 0xec, - 0xce, 0xc1, 0xbf, 0x37, 0xf8, 0x13, 0x53, 0xa5, 0xe5, 0x0c, 0x3a, 0xa8, 0x55, 0xb9, 0xff, 0x68])) + aff( + fe([ + 0xca, 0xfc, 0x88, 0x6b, 0x96, 0x78, 0x0a, 0x8b, 0x83, 0xdc, 0xbc, 0xaf, 0x40, 0xb6, 0x8d, 0x7f, + 0xef, 0xb4, 0xd1, 0x3f, 0xcc, 0xa2, 0x74, 0xc9, 0xc2, 0x92, 0x55, 0x00, 0xab, 0xdb, 0xbf, 0x4f + ]), + fe([ + 0x93, 0x1c, 0x06, 0x2d, 0x66, 0x65, 0x02, 0xa4, 0x97, 0x18, 0xfd, 0x00, 0xe7, 0xab, 0x03, 0xec, + 0xce, 0xc1, 0xbf, 0x37, 0xf8, 0x13, 0x53, 0xa5, 0xe5, 0x0c, 0x3a, 0xa8, 0x55, 0xb9, 0xff, 0x68 + ]) + ) ) v.append( - aff(fe([0xe4, 0xe6, 0x6d, 0x30, 0x7d, 0x30, 0x35, 0xc2, 0x78, 0x87, 0xf9, 0xfc, 0x6b, 0x5a, 0xc3, 0xb7, - 0x65, 0xd8, 0x2e, 0xc7, 0xa5, 0x0c, 0xc6, 0xdc, 0x12, 0xaa, 0xd6, 0x4f, 0xc5, 0x38, 0xbc, 0x0e]) , - fe([0xe2, 0x3c, 0x76, 0x86, 0x38, 0xf2, 0x7b, 0x2c, 0x16, 0x78, 0x8d, 0xf5, 0xa4, 0x15, 0xda, 0xdb, - 0x26, 0x85, 0xa0, 0x56, 0xdd, 0x1d, 0xe3, 0xb3, 0xfd, 0x40, 0xef, 0xf2, 0xd9, 0xa1, 0xb3, 0x04])) + aff( + fe([ + 0xe4, 0xe6, 0x6d, 0x30, 0x7d, 0x30, 0x35, 0xc2, 0x78, 0x87, 0xf9, 0xfc, 0x6b, 0x5a, 0xc3, 0xb7, + 0x65, 0xd8, 0x2e, 0xc7, 0xa5, 0x0c, 0xc6, 0xdc, 0x12, 0xaa, 0xd6, 0x4f, 0xc5, 0x38, 0xbc, 0x0e + ]), + fe([ + 0xe2, 0x3c, 0x76, 0x86, 0x38, 0xf2, 0x7b, 0x2c, 0x16, 0x78, 0x8d, 0xf5, 0xa4, 0x15, 0xda, 0xdb, + 0x26, 0x85, 0xa0, 0x56, 0xdd, 0x1d, 0xe3, 0xb3, 0xfd, 0x40, 0xef, 0xf2, 0xd9, 0xa1, 0xb3, 0x04 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xdb, 0x49, 0x0e, 0xe6, 0x58, 0x10, 0x7a, 0x52, 0xda, 0xb5, 0x7d, 0x37, 0x6a, 0x3e, 0xa1, 0x78, - 0xce, 0xc7, 0x1c, 0x24, 0x23, 0xdb, 0x7d, 0xfb, 0x8c, 0x8d, 0xdc, 0x30, 0x67, 0x69, 0x75, 0x3b]) , - fe([0xa9, 0xea, 0x6d, 0x16, 0x16, 0x60, 0xf4, 0x60, 0x87, 0x19, 0x44, 0x8c, 0x4a, 0x8b, 0x3e, 0xfb, - 0x16, 0x00, 0x00, 0x54, 0xa6, 0x9e, 0x9f, 0xef, 0xcf, 0xd9, 0xd2, 0x4c, 0x74, 0x31, 0xd0, 0x34])) + aff( + fe([ + 0xdb, 0x49, 0x0e, 0xe6, 0x58, 0x10, 0x7a, 0x52, 0xda, 0xb5, 0x7d, 0x37, 0x6a, 0x3e, 0xa1, 0x78, + 0xce, 0xc7, 0x1c, 0x24, 0x23, 0xdb, 0x7d, 0xfb, 0x8c, 0x8d, 0xdc, 0x30, 0x67, 0x69, 0x75, 0x3b + ]), + fe([ + 0xa9, 0xea, 0x6d, 0x16, 0x16, 0x60, 0xf4, 0x60, 0x87, 0x19, 0x44, 0x8c, 0x4a, 0x8b, 0x3e, 0xfb, + 0x16, 0x00, 0x00, 0x54, 0xa6, 0x9e, 0x9f, 0xef, 0xcf, 0xd9, 0xd2, 0x4c, 0x74, 0x31, 0xd0, 0x34 + ]) + ) ) v.append( - aff(fe([0xa4, 0xeb, 0x04, 0xa4, 0x8c, 0x8f, 0x71, 0x27, 0x95, 0x85, 0x5d, 0x55, 0x4b, 0xb1, 0x26, 0x26, - 0xc8, 0xae, 0x6a, 0x7d, 0xa2, 0x21, 0xca, 0xce, 0x38, 0xab, 0x0f, 0xd0, 0xd5, 0x2b, 0x6b, 0x00]) , - fe([0xe5, 0x67, 0x0c, 0xf1, 0x3a, 0x9a, 0xea, 0x09, 0x39, 0xef, 0xd1, 0x30, 0xbc, 0x33, 0xba, 0xb1, - 0x6a, 0xc5, 0x27, 0x08, 0x7f, 0x54, 0x80, 0x3d, 0xab, 0xf6, 0x15, 0x7a, 0xc2, 0x40, 0x73, 0x72])) + aff( + fe([ + 0xa4, 0xeb, 0x04, 0xa4, 0x8c, 0x8f, 0x71, 0x27, 0x95, 0x85, 0x5d, 0x55, 0x4b, 0xb1, 0x26, 0x26, + 0xc8, 0xae, 0x6a, 0x7d, 0xa2, 0x21, 0xca, 0xce, 0x38, 0xab, 0x0f, 0xd0, 0xd5, 0x2b, 0x6b, 0x00 + ]), + fe([ + 0xe5, 0x67, 0x0c, 0xf1, 0x3a, 0x9a, 0xea, 0x09, 0x39, 0xef, 0xd1, 0x30, 0xbc, 0x33, 0xba, 0xb1, + 0x6a, 0xc5, 0x27, 0x08, 0x7f, 0x54, 0x80, 0x3d, 0xab, 0xf6, 0x15, 0x7a, 0xc2, 0x40, 0x73, 0x72 + ]) + ) ) v.append( - aff(fe([0x84, 0x56, 0x82, 0xb6, 0x12, 0x70, 0x7f, 0xf7, 0xf0, 0xbd, 0x5b, 0xa9, 0xd5, 0xc5, 0x5f, 0x59, - 0xbf, 0x7f, 0xb3, 0x55, 0x22, 0x02, 0xc9, 0x44, 0x55, 0x87, 0x8f, 0x96, 0x98, 0x64, 0x6d, 0x15]) , - fe([0xb0, 0x8b, 0xaa, 0x1e, 0xec, 0xc7, 0xa5, 0x8f, 0x1f, 0x92, 0x04, 0xc6, 0x05, 0xf6, 0xdf, 0xa1, - 0xcc, 0x1f, 0x81, 0xf5, 0x0e, 0x9c, 0x57, 0xdc, 0xe3, 0xbb, 0x06, 0x87, 0x1e, 0xfe, 0x23, 0x6c])) + aff( + fe([ + 0x84, 0x56, 0x82, 0xb6, 0x12, 0x70, 0x7f, 0xf7, 0xf0, 0xbd, 0x5b, 0xa9, 0xd5, 0xc5, 0x5f, 0x59, + 0xbf, 0x7f, 0xb3, 0x55, 0x22, 0x02, 0xc9, 0x44, 0x55, 0x87, 0x8f, 0x96, 0x98, 0x64, 0x6d, 0x15 + ]), + fe([ + 0xb0, 0x8b, 0xaa, 0x1e, 0xec, 0xc7, 0xa5, 0x8f, 0x1f, 0x92, 0x04, 0xc6, 0x05, 0xf6, 0xdf, 0xa1, + 0xcc, 0x1f, 0x81, 0xf5, 0x0e, 0x9c, 0x57, 0xdc, 0xe3, 0xbb, 0x06, 0x87, 0x1e, 0xfe, 0x23, 0x6c + ]) + ) ) v.append( - aff(fe([0xd8, 0x2b, 0x5b, 0x16, 0xea, 0x20, 0xf1, 0xd3, 0x68, 0x8f, 0xae, 0x5b, 0xd0, 0xa9, 0x1a, 0x19, - 0xa8, 0x36, 0xfb, 0x2b, 0x57, 0x88, 0x7d, 0x90, 0xd5, 0xa6, 0xf3, 0xdc, 0x38, 0x89, 0x4e, 0x1f]) , - fe([0xcc, 0x19, 0xda, 0x9b, 0x3b, 0x43, 0x48, 0x21, 0x2e, 0x23, 0x4d, 0x3d, 0xae, 0xf8, 0x8c, 0xfc, - 0xdd, 0xa6, 0x74, 0x37, 0x65, 0xca, 0xee, 0x1a, 0x19, 0x8e, 0x9f, 0x64, 0x6f, 0x0c, 0x8b, 0x5a])) + aff( + fe([ + 0xd8, 0x2b, 0x5b, 0x16, 0xea, 0x20, 0xf1, 0xd3, 0x68, 0x8f, 0xae, 0x5b, 0xd0, 0xa9, 0x1a, 0x19, + 0xa8, 0x36, 0xfb, 0x2b, 0x57, 0x88, 0x7d, 0x90, 0xd5, 0xa6, 0xf3, 0xdc, 0x38, 0x89, 0x4e, 0x1f + ]), + fe([ + 0xcc, 0x19, 0xda, 0x9b, 0x3b, 0x43, 0x48, 0x21, 0x2e, 0x23, 0x4d, 0x3d, 0xae, 0xf8, 0x8c, 0xfc, + 0xdd, 0xa6, 0x74, 0x37, 0x65, 0xca, 0xee, 0x1a, 0x19, 0x8e, 0x9f, 0x64, 0x6f, 0x0c, 0x8b, 0x5a + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x25, 0xb9, 0xc2, 0xf0, 0x72, 0xb8, 0x15, 0x16, 0xcc, 0x8d, 0x3c, 0x6f, 0x25, 0xed, 0xf4, 0x46, - 0x2e, 0x0c, 0x60, 0x0f, 0xe2, 0x84, 0x34, 0x55, 0x89, 0x59, 0x34, 0x1b, 0xf5, 0x8d, 0xfe, 0x08]) , - fe([0xf8, 0xab, 0x93, 0xbc, 0x44, 0xba, 0x1b, 0x75, 0x4b, 0x49, 0x6f, 0xd0, 0x54, 0x2e, 0x63, 0xba, - 0xb5, 0xea, 0xed, 0x32, 0x14, 0xc9, 0x94, 0xd8, 0xc5, 0xce, 0xf4, 0x10, 0x68, 0xe0, 0x38, 0x27])) + aff( + fe([ + 0x25, 0xb9, 0xc2, 0xf0, 0x72, 0xb8, 0x15, 0x16, 0xcc, 0x8d, 0x3c, 0x6f, 0x25, 0xed, 0xf4, 0x46, + 0x2e, 0x0c, 0x60, 0x0f, 0xe2, 0x84, 0x34, 0x55, 0x89, 0x59, 0x34, 0x1b, 0xf5, 0x8d, 0xfe, 0x08 + ]), + fe([ + 0xf8, 0xab, 0x93, 0xbc, 0x44, 0xba, 0x1b, 0x75, 0x4b, 0x49, 0x6f, 0xd0, 0x54, 0x2e, 0x63, 0xba, + 0xb5, 0xea, 0xed, 0x32, 0x14, 0xc9, 0x94, 0xd8, 0xc5, 0xce, 0xf4, 0x10, 0x68, 0xe0, 0x38, 0x27 + ]) + ) ) v.append( - aff(fe([0x74, 0x1c, 0x14, 0x9b, 0xd4, 0x64, 0x61, 0x71, 0x5a, 0xb6, 0x21, 0x33, 0x4f, 0xf7, 0x8e, 0xba, - 0xa5, 0x48, 0x9a, 0xc7, 0xfa, 0x9a, 0xf0, 0xb4, 0x62, 0xad, 0xf2, 0x5e, 0xcc, 0x03, 0x24, 0x1a]) , - fe([0xf5, 0x76, 0xfd, 0xe4, 0xaf, 0xb9, 0x03, 0x59, 0xce, 0x63, 0xd2, 0x3b, 0x1f, 0xcd, 0x21, 0x0c, - 0xad, 0x44, 0xa5, 0x97, 0xac, 0x80, 0x11, 0x02, 0x9b, 0x0c, 0xe5, 0x8b, 0xcd, 0xfb, 0x79, 0x77])) + aff( + fe([ + 0x74, 0x1c, 0x14, 0x9b, 0xd4, 0x64, 0x61, 0x71, 0x5a, 0xb6, 0x21, 0x33, 0x4f, 0xf7, 0x8e, 0xba, + 0xa5, 0x48, 0x9a, 0xc7, 0xfa, 0x9a, 0xf0, 0xb4, 0x62, 0xad, 0xf2, 0x5e, 0xcc, 0x03, 0x24, 0x1a + ]), + fe([ + 0xf5, 0x76, 0xfd, 0xe4, 0xaf, 0xb9, 0x03, 0x59, 0xce, 0x63, 0xd2, 0x3b, 0x1f, 0xcd, 0x21, 0x0c, + 0xad, 0x44, 0xa5, 0x97, 0xac, 0x80, 0x11, 0x02, 0x9b, 0x0c, 0xe5, 0x8b, 0xcd, 0xfb, 0x79, 0x77 + ]) + ) ) v.append( - aff(fe([0x15, 0xbe, 0x9a, 0x0d, 0xba, 0x38, 0x72, 0x20, 0x8a, 0xf5, 0xbe, 0x59, 0x93, 0x79, 0xb7, 0xf6, - 0x6a, 0x0c, 0x38, 0x27, 0x1a, 0x60, 0xf4, 0x86, 0x3b, 0xab, 0x5a, 0x00, 0xa0, 0xce, 0x21, 0x7d]) , - fe([0x6c, 0xba, 0x14, 0xc5, 0xea, 0x12, 0x9e, 0x2e, 0x82, 0x63, 0xce, 0x9b, 0x4a, 0xe7, 0x1d, 0xec, - 0xf1, 0x2e, 0x51, 0x1c, 0xf4, 0xd0, 0x69, 0x15, 0x42, 0x9d, 0xa3, 0x3f, 0x0e, 0xbf, 0xe9, 0x5c])) + aff( + fe([ + 0x15, 0xbe, 0x9a, 0x0d, 0xba, 0x38, 0x72, 0x20, 0x8a, 0xf5, 0xbe, 0x59, 0x93, 0x79, 0xb7, 0xf6, + 0x6a, 0x0c, 0x38, 0x27, 0x1a, 0x60, 0xf4, 0x86, 0x3b, 0xab, 0x5a, 0x00, 0xa0, 0xce, 0x21, 0x7d + ]), + fe([ + 0x6c, 0xba, 0x14, 0xc5, 0xea, 0x12, 0x9e, 0x2e, 0x82, 0x63, 0xce, 0x9b, 0x4a, 0xe7, 0x1d, 0xec, + 0xf1, 0x2e, 0x51, 0x1c, 0xf4, 0xd0, 0x69, 0x15, 0x42, 0x9d, 0xa3, 0x3f, 0x0e, 0xbf, 0xe9, 0x5c + ]) + ) ) v.append( - aff(fe([0xe4, 0x0d, 0xf4, 0xbd, 0xee, 0x31, 0x10, 0xed, 0xcb, 0x12, 0x86, 0xad, 0xd4, 0x2f, 0x90, 0x37, - 0x32, 0xc3, 0x0b, 0x73, 0xec, 0x97, 0x85, 0xa4, 0x01, 0x1c, 0x76, 0x35, 0xfe, 0x75, 0xdd, 0x71]) , - fe([0x11, 0xa4, 0x88, 0x9f, 0x3e, 0x53, 0x69, 0x3b, 0x1b, 0xe0, 0xf7, 0xba, 0x9b, 0xad, 0x4e, 0x81, - 0x5f, 0xb5, 0x5c, 0xae, 0xbe, 0x67, 0x86, 0x37, 0x34, 0x8e, 0x07, 0x32, 0x45, 0x4a, 0x67, 0x39])) + aff( + fe([ + 0xe4, 0x0d, 0xf4, 0xbd, 0xee, 0x31, 0x10, 0xed, 0xcb, 0x12, 0x86, 0xad, 0xd4, 0x2f, 0x90, 0x37, + 0x32, 0xc3, 0x0b, 0x73, 0xec, 0x97, 0x85, 0xa4, 0x01, 0x1c, 0x76, 0x35, 0xfe, 0x75, 0xdd, 0x71 + ]), + fe([ + 0x11, 0xa4, 0x88, 0x9f, 0x3e, 0x53, 0x69, 0x3b, 0x1b, 0xe0, 0xf7, 0xba, 0x9b, 0xad, 0x4e, 0x81, + 0x5f, 0xb5, 0x5c, 0xae, 0xbe, 0x67, 0x86, 0x37, 0x34, 0x8e, 0x07, 0x32, 0x45, 0x4a, 0x67, 0x39 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x90, 0x70, 0x58, 0x20, 0x03, 0x1e, 0x67, 0xb2, 0xc8, 0x9b, 0x58, 0xc5, 0xb1, 0xeb, 0x2d, 0x4a, - 0xde, 0x82, 0x8c, 0xf2, 0xd2, 0x14, 0xb8, 0x70, 0x61, 0x4e, 0x73, 0xd6, 0x0b, 0x6b, 0x0d, 0x30]) , - fe([0x81, 0xfc, 0x55, 0x5c, 0xbf, 0xa7, 0xc4, 0xbd, 0xe2, 0xf0, 0x4b, 0x8f, 0xe9, 0x7d, 0x99, 0xfa, - 0xd3, 0xab, 0xbc, 0xc7, 0x83, 0x2b, 0x04, 0x7f, 0x0c, 0x19, 0x43, 0x03, 0x3d, 0x07, 0xca, 0x40])) + aff( + fe([ + 0x90, 0x70, 0x58, 0x20, 0x03, 0x1e, 0x67, 0xb2, 0xc8, 0x9b, 0x58, 0xc5, 0xb1, 0xeb, 0x2d, 0x4a, + 0xde, 0x82, 0x8c, 0xf2, 0xd2, 0x14, 0xb8, 0x70, 0x61, 0x4e, 0x73, 0xd6, 0x0b, 0x6b, 0x0d, 0x30 + ]), + fe([ + 0x81, 0xfc, 0x55, 0x5c, 0xbf, 0xa7, 0xc4, 0xbd, 0xe2, 0xf0, 0x4b, 0x8f, 0xe9, 0x7d, 0x99, 0xfa, + 0xd3, 0xab, 0xbc, 0xc7, 0x83, 0x2b, 0x04, 0x7f, 0x0c, 0x19, 0x43, 0x03, 0x3d, 0x07, 0xca, 0x40 + ]) + ) ) v.append( - aff(fe([0xf9, 0xc8, 0xbe, 0x8c, 0x16, 0x81, 0x39, 0x96, 0xf6, 0x17, 0x58, 0xc8, 0x30, 0x58, 0xfb, 0xc2, - 0x03, 0x45, 0xd2, 0x52, 0x76, 0xe0, 0x6a, 0x26, 0x28, 0x5c, 0x88, 0x59, 0x6a, 0x5a, 0x54, 0x42]) , - fe([0x07, 0xb5, 0x2e, 0x2c, 0x67, 0x15, 0x9b, 0xfb, 0x83, 0x69, 0x1e, 0x0f, 0xda, 0xd6, 0x29, 0xb1, - 0x60, 0xe0, 0xb2, 0xba, 0x69, 0xa2, 0x9e, 0xbd, 0xbd, 0xe0, 0x1c, 0xbd, 0xcd, 0x06, 0x64, 0x70])) + aff( + fe([ + 0xf9, 0xc8, 0xbe, 0x8c, 0x16, 0x81, 0x39, 0x96, 0xf6, 0x17, 0x58, 0xc8, 0x30, 0x58, 0xfb, 0xc2, + 0x03, 0x45, 0xd2, 0x52, 0x76, 0xe0, 0x6a, 0x26, 0x28, 0x5c, 0x88, 0x59, 0x6a, 0x5a, 0x54, 0x42 + ]), + fe([ + 0x07, 0xb5, 0x2e, 0x2c, 0x67, 0x15, 0x9b, 0xfb, 0x83, 0x69, 0x1e, 0x0f, 0xda, 0xd6, 0x29, 0xb1, + 0x60, 0xe0, 0xb2, 0xba, 0x69, 0xa2, 0x9e, 0xbd, 0xbd, 0xe0, 0x1c, 0xbd, 0xcd, 0x06, 0x64, 0x70 + ]) + ) ) v.append( - aff(fe([0x41, 0xfa, 0x8c, 0xe1, 0x89, 0x8f, 0x27, 0xc8, 0x25, 0x8f, 0x6f, 0x5f, 0x55, 0xf8, 0xde, 0x95, - 0x6d, 0x2f, 0x75, 0x16, 0x2b, 0x4e, 0x44, 0xfd, 0x86, 0x6e, 0xe9, 0x70, 0x39, 0x76, 0x97, 0x7e]) , - fe([0x17, 0x62, 0x6b, 0x14, 0xa1, 0x7c, 0xd0, 0x79, 0x6e, 0xd8, 0x8a, 0xa5, 0x6d, 0x8c, 0x93, 0xd2, - 0x3f, 0xec, 0x44, 0x8d, 0x6e, 0x91, 0x01, 0x8c, 0x8f, 0xee, 0x01, 0x8f, 0xc0, 0xb4, 0x85, 0x0e])) + aff( + fe([ + 0x41, 0xfa, 0x8c, 0xe1, 0x89, 0x8f, 0x27, 0xc8, 0x25, 0x8f, 0x6f, 0x5f, 0x55, 0xf8, 0xde, 0x95, + 0x6d, 0x2f, 0x75, 0x16, 0x2b, 0x4e, 0x44, 0xfd, 0x86, 0x6e, 0xe9, 0x70, 0x39, 0x76, 0x97, 0x7e + ]), + fe([ + 0x17, 0x62, 0x6b, 0x14, 0xa1, 0x7c, 0xd0, 0x79, 0x6e, 0xd8, 0x8a, 0xa5, 0x6d, 0x8c, 0x93, 0xd2, + 0x3f, 0xec, 0x44, 0x8d, 0x6e, 0x91, 0x01, 0x8c, 0x8f, 0xee, 0x01, 0x8f, 0xc0, 0xb4, 0x85, 0x0e + ]) + ) ) v.append( - aff(fe([0x02, 0x3a, 0x70, 0x41, 0xe4, 0x11, 0x57, 0x23, 0xac, 0xe6, 0xfc, 0x54, 0x7e, 0xcd, 0xd7, 0x22, - 0xcb, 0x76, 0x9f, 0x20, 0xce, 0xa0, 0x73, 0x76, 0x51, 0x3b, 0xa4, 0xf8, 0xe3, 0x62, 0x12, 0x6c]) , - fe([0x7f, 0x00, 0x9c, 0x26, 0x0d, 0x6f, 0x48, 0x7f, 0x3a, 0x01, 0xed, 0xc5, 0x96, 0xb0, 0x1f, 0x4f, - 0xa8, 0x02, 0x62, 0x27, 0x8a, 0x50, 0x8d, 0x9a, 0x8b, 0x52, 0x0f, 0x1e, 0xcf, 0x41, 0x38, 0x19])) + aff( + fe([ + 0x02, 0x3a, 0x70, 0x41, 0xe4, 0x11, 0x57, 0x23, 0xac, 0xe6, 0xfc, 0x54, 0x7e, 0xcd, 0xd7, 0x22, + 0xcb, 0x76, 0x9f, 0x20, 0xce, 0xa0, 0x73, 0x76, 0x51, 0x3b, 0xa4, 0xf8, 0xe3, 0x62, 0x12, 0x6c + ]), + fe([ + 0x7f, 0x00, 0x9c, 0x26, 0x0d, 0x6f, 0x48, 0x7f, 0x3a, 0x01, 0xed, 0xc5, 0x96, 0xb0, 0x1f, 0x4f, + 0xa8, 0x02, 0x62, 0x27, 0x8a, 0x50, 0x8d, 0x9a, 0x8b, 0x52, 0x0f, 0x1e, 0xcf, 0x41, 0x38, 0x19 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xf5, 0x6c, 0xd4, 0x2f, 0x0f, 0x69, 0x0f, 0x87, 0x3f, 0x61, 0x65, 0x1e, 0x35, 0x34, 0x85, 0xba, - 0x02, 0x30, 0xac, 0x25, 0x3d, 0xe2, 0x62, 0xf1, 0xcc, 0xe9, 0x1b, 0xc2, 0xef, 0x6a, 0x42, 0x57]) , - fe([0x34, 0x1f, 0x2e, 0xac, 0xd1, 0xc7, 0x04, 0x52, 0x32, 0x66, 0xb2, 0x33, 0x73, 0x21, 0x34, 0x54, - 0xf7, 0x71, 0xed, 0x06, 0xb0, 0xff, 0xa6, 0x59, 0x6f, 0x8a, 0x4e, 0xfb, 0x02, 0xb0, 0x45, 0x6b])) + aff( + fe([ + 0xf5, 0x6c, 0xd4, 0x2f, 0x0f, 0x69, 0x0f, 0x87, 0x3f, 0x61, 0x65, 0x1e, 0x35, 0x34, 0x85, 0xba, + 0x02, 0x30, 0xac, 0x25, 0x3d, 0xe2, 0x62, 0xf1, 0xcc, 0xe9, 0x1b, 0xc2, 0xef, 0x6a, 0x42, 0x57 + ]), + fe([ + 0x34, 0x1f, 0x2e, 0xac, 0xd1, 0xc7, 0x04, 0x52, 0x32, 0x66, 0xb2, 0x33, 0x73, 0x21, 0x34, 0x54, + 0xf7, 0x71, 0xed, 0x06, 0xb0, 0xff, 0xa6, 0x59, 0x6f, 0x8a, 0x4e, 0xfb, 0x02, 0xb0, 0x45, 0x6b + ]) + ) ) v.append( - aff(fe([0xf5, 0x48, 0x0b, 0x03, 0xc5, 0x22, 0x7d, 0x80, 0x08, 0x53, 0xfe, 0x32, 0xb1, 0xa1, 0x8a, 0x74, - 0x6f, 0xbd, 0x3f, 0x85, 0xf4, 0xcf, 0xf5, 0x60, 0xaf, 0x41, 0x7e, 0x3e, 0x46, 0xa3, 0x5a, 0x20]) , - fe([0xaa, 0x35, 0x87, 0x44, 0x63, 0x66, 0x97, 0xf8, 0x6e, 0x55, 0x0c, 0x04, 0x3e, 0x35, 0x50, 0xbf, - 0x93, 0x69, 0xd2, 0x8b, 0x05, 0x55, 0x99, 0xbe, 0xe2, 0x53, 0x61, 0xec, 0xe8, 0x08, 0x0b, 0x32])) + aff( + fe([ + 0xf5, 0x48, 0x0b, 0x03, 0xc5, 0x22, 0x7d, 0x80, 0x08, 0x53, 0xfe, 0x32, 0xb1, 0xa1, 0x8a, 0x74, + 0x6f, 0xbd, 0x3f, 0x85, 0xf4, 0xcf, 0xf5, 0x60, 0xaf, 0x41, 0x7e, 0x3e, 0x46, 0xa3, 0x5a, 0x20 + ]), + fe([ + 0xaa, 0x35, 0x87, 0x44, 0x63, 0x66, 0x97, 0xf8, 0x6e, 0x55, 0x0c, 0x04, 0x3e, 0x35, 0x50, 0xbf, + 0x93, 0x69, 0xd2, 0x8b, 0x05, 0x55, 0x99, 0xbe, 0xe2, 0x53, 0x61, 0xec, 0xe8, 0x08, 0x0b, 0x32 + ]) + ) ) v.append( - aff(fe([0xb3, 0x10, 0x45, 0x02, 0x69, 0x59, 0x2e, 0x97, 0xd9, 0x64, 0xf8, 0xdb, 0x25, 0x80, 0xdc, 0xc4, - 0xd5, 0x62, 0x3c, 0xed, 0x65, 0x91, 0xad, 0xd1, 0x57, 0x81, 0x94, 0xaa, 0xa1, 0x29, 0xfc, 0x68]) , - fe([0xdd, 0xb5, 0x7d, 0xab, 0x5a, 0x21, 0x41, 0x53, 0xbb, 0x17, 0x79, 0x0d, 0xd1, 0xa8, 0x0c, 0x0c, - 0x20, 0x88, 0x09, 0xe9, 0x84, 0xe8, 0x25, 0x11, 0x67, 0x7a, 0x8b, 0x1a, 0xe4, 0x5d, 0xe1, 0x5d])) + aff( + fe([ + 0xb3, 0x10, 0x45, 0x02, 0x69, 0x59, 0x2e, 0x97, 0xd9, 0x64, 0xf8, 0xdb, 0x25, 0x80, 0xdc, 0xc4, + 0xd5, 0x62, 0x3c, 0xed, 0x65, 0x91, 0xad, 0xd1, 0x57, 0x81, 0x94, 0xaa, 0xa1, 0x29, 0xfc, 0x68 + ]), + fe([ + 0xdd, 0xb5, 0x7d, 0xab, 0x5a, 0x21, 0x41, 0x53, 0xbb, 0x17, 0x79, 0x0d, 0xd1, 0xa8, 0x0c, 0x0c, + 0x20, 0x88, 0x09, 0xe9, 0x84, 0xe8, 0x25, 0x11, 0x67, 0x7a, 0x8b, 0x1a, 0xe4, 0x5d, 0xe1, 0x5d + ]) + ) ) v.append( - aff(fe([0x37, 0xea, 0xfe, 0x65, 0x3b, 0x25, 0xe8, 0xe1, 0xc2, 0xc5, 0x02, 0xa4, 0xbe, 0x98, 0x0a, 0x2b, - 0x61, 0xc1, 0x9b, 0xe2, 0xd5, 0x92, 0xe6, 0x9e, 0x7d, 0x1f, 0xca, 0x43, 0x88, 0x8b, 0x2c, 0x59]) , - fe([0xe0, 0xb5, 0x00, 0x1d, 0x2a, 0x6f, 0xaf, 0x79, 0x86, 0x2f, 0xa6, 0x5a, 0x93, 0xd1, 0xfe, 0xae, - 0x3a, 0xee, 0xdb, 0x7c, 0x61, 0xbe, 0x7c, 0x01, 0xf9, 0xfe, 0x52, 0xdc, 0xd8, 0x52, 0xa3, 0x42])) + aff( + fe([ + 0x37, 0xea, 0xfe, 0x65, 0x3b, 0x25, 0xe8, 0xe1, 0xc2, 0xc5, 0x02, 0xa4, 0xbe, 0x98, 0x0a, 0x2b, + 0x61, 0xc1, 0x9b, 0xe2, 0xd5, 0x92, 0xe6, 0x9e, 0x7d, 0x1f, 0xca, 0x43, 0x88, 0x8b, 0x2c, 0x59 + ]), + fe([ + 0xe0, 0xb5, 0x00, 0x1d, 0x2a, 0x6f, 0xaf, 0x79, 0x86, 0x2f, 0xa6, 0x5a, 0x93, 0xd1, 0xfe, 0xae, + 0x3a, 0xee, 0xdb, 0x7c, 0x61, 0xbe, 0x7c, 0x01, 0xf9, 0xfe, 0x52, 0xdc, 0xd8, 0x52, 0xa3, 0x42 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x22, 0xaf, 0x13, 0x37, 0xbd, 0x37, 0x71, 0xac, 0x04, 0x46, 0x63, 0xac, 0xa4, 0x77, 0xed, 0x25, - 0x38, 0xe0, 0x15, 0xa8, 0x64, 0x00, 0x0d, 0xce, 0x51, 0x01, 0xa9, 0xbc, 0x0f, 0x03, 0x1c, 0x04]) , - fe([0x89, 0xf9, 0x80, 0x07, 0xcf, 0x3f, 0xb3, 0xe9, 0xe7, 0x45, 0x44, 0x3d, 0x2a, 0x7c, 0xe9, 0xe4, - 0x16, 0x5c, 0x5e, 0x65, 0x1c, 0xc7, 0x7d, 0xc6, 0x7a, 0xfb, 0x43, 0xee, 0x25, 0x76, 0x46, 0x72])) + aff( + fe([ + 0x22, 0xaf, 0x13, 0x37, 0xbd, 0x37, 0x71, 0xac, 0x04, 0x46, 0x63, 0xac, 0xa4, 0x77, 0xed, 0x25, + 0x38, 0xe0, 0x15, 0xa8, 0x64, 0x00, 0x0d, 0xce, 0x51, 0x01, 0xa9, 0xbc, 0x0f, 0x03, 0x1c, 0x04 + ]), + fe([ + 0x89, 0xf9, 0x80, 0x07, 0xcf, 0x3f, 0xb3, 0xe9, 0xe7, 0x45, 0x44, 0x3d, 0x2a, 0x7c, 0xe9, 0xe4, + 0x16, 0x5c, 0x5e, 0x65, 0x1c, 0xc7, 0x7d, 0xc6, 0x7a, 0xfb, 0x43, 0xee, 0x25, 0x76, 0x46, 0x72 + ]) + ) ) v.append( - aff(fe([0x02, 0xa2, 0xed, 0xf4, 0x8f, 0x6b, 0x0b, 0x3e, 0xeb, 0x35, 0x1a, 0xd5, 0x7e, 0xdb, 0x78, 0x00, - 0x96, 0x8a, 0xa0, 0xb4, 0xcf, 0x60, 0x4b, 0xd4, 0xd5, 0xf9, 0x2d, 0xbf, 0x88, 0xbd, 0x22, 0x62]) , - fe([0x13, 0x53, 0xe4, 0x82, 0x57, 0xfa, 0x1e, 0x8f, 0x06, 0x2b, 0x90, 0xba, 0x08, 0xb6, 0x10, 0x54, - 0x4f, 0x7c, 0x1b, 0x26, 0xed, 0xda, 0x6b, 0xdd, 0x25, 0xd0, 0x4e, 0xea, 0x42, 0xbb, 0x25, 0x03])) + aff( + fe([ + 0x02, 0xa2, 0xed, 0xf4, 0x8f, 0x6b, 0x0b, 0x3e, 0xeb, 0x35, 0x1a, 0xd5, 0x7e, 0xdb, 0x78, 0x00, + 0x96, 0x8a, 0xa0, 0xb4, 0xcf, 0x60, 0x4b, 0xd4, 0xd5, 0xf9, 0x2d, 0xbf, 0x88, 0xbd, 0x22, 0x62 + ]), + fe([ + 0x13, 0x53, 0xe4, 0x82, 0x57, 0xfa, 0x1e, 0x8f, 0x06, 0x2b, 0x90, 0xba, 0x08, 0xb6, 0x10, 0x54, + 0x4f, 0x7c, 0x1b, 0x26, 0xed, 0xda, 0x6b, 0xdd, 0x25, 0xd0, 0x4e, 0xea, 0x42, 0xbb, 0x25, 0x03 + ]) + ) ) v.append( - aff(fe([0x51, 0x16, 0x50, 0x7c, 0xd5, 0x5d, 0xf6, 0x99, 0xe8, 0x77, 0x72, 0x4e, 0xfa, 0x62, 0xcb, 0x76, - 0x75, 0x0c, 0xe2, 0x71, 0x98, 0x92, 0xd5, 0xfa, 0x45, 0xdf, 0x5c, 0x6f, 0x1e, 0x9e, 0x28, 0x69]) , - fe([0x0d, 0xac, 0x66, 0x6d, 0xc3, 0x8b, 0xba, 0x16, 0xb5, 0xe2, 0xa0, 0x0d, 0x0c, 0xbd, 0xa4, 0x8e, - 0x18, 0x6c, 0xf2, 0xdc, 0xf9, 0xdc, 0x4a, 0x86, 0x25, 0x95, 0x14, 0xcb, 0xd8, 0x1a, 0x04, 0x0f])) + aff( + fe([ + 0x51, 0x16, 0x50, 0x7c, 0xd5, 0x5d, 0xf6, 0x99, 0xe8, 0x77, 0x72, 0x4e, 0xfa, 0x62, 0xcb, 0x76, + 0x75, 0x0c, 0xe2, 0x71, 0x98, 0x92, 0xd5, 0xfa, 0x45, 0xdf, 0x5c, 0x6f, 0x1e, 0x9e, 0x28, 0x69 + ]), + fe([ + 0x0d, 0xac, 0x66, 0x6d, 0xc3, 0x8b, 0xba, 0x16, 0xb5, 0xe2, 0xa0, 0x0d, 0x0c, 0xbd, 0xa4, 0x8e, + 0x18, 0x6c, 0xf2, 0xdc, 0xf9, 0xdc, 0x4a, 0x86, 0x25, 0x95, 0x14, 0xcb, 0xd8, 0x1a, 0x04, 0x0f + ]) + ) ) v.append( - aff(fe([0x97, 0xa5, 0xdb, 0x8b, 0x2d, 0xaa, 0x42, 0x11, 0x09, 0xf2, 0x93, 0xbb, 0xd9, 0x06, 0x84, 0x4e, - 0x11, 0xa8, 0xa0, 0x25, 0x2b, 0xa6, 0x5f, 0xae, 0xc4, 0xb4, 0x4c, 0xc8, 0xab, 0xc7, 0x3b, 0x02]) , - fe([0xee, 0xc9, 0x29, 0x0f, 0xdf, 0x11, 0x85, 0xed, 0xce, 0x0d, 0x62, 0x2c, 0x8f, 0x4b, 0xf9, 0x04, - 0xe9, 0x06, 0x72, 0x1d, 0x37, 0x20, 0x50, 0xc9, 0x14, 0xeb, 0xec, 0x39, 0xa7, 0x97, 0x2b, 0x4d])) + aff( + fe([ + 0x97, 0xa5, 0xdb, 0x8b, 0x2d, 0xaa, 0x42, 0x11, 0x09, 0xf2, 0x93, 0xbb, 0xd9, 0x06, 0x84, 0x4e, + 0x11, 0xa8, 0xa0, 0x25, 0x2b, 0xa6, 0x5f, 0xae, 0xc4, 0xb4, 0x4c, 0xc8, 0xab, 0xc7, 0x3b, 0x02 + ]), + fe([ + 0xee, 0xc9, 0x29, 0x0f, 0xdf, 0x11, 0x85, 0xed, 0xce, 0x0d, 0x62, 0x2c, 0x8f, 0x4b, 0xf9, 0x04, + 0xe9, 0x06, 0x72, 0x1d, 0x37, 0x20, 0x50, 0xc9, 0x14, 0xeb, 0xec, 0x39, 0xa7, 0x97, 0x2b, 0x4d + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x69, 0xd1, 0x39, 0xbd, 0xfb, 0x33, 0xbe, 0xc4, 0xf0, 0x5c, 0xef, 0xf0, 0x56, 0x68, 0xfc, 0x97, - 0x47, 0xc8, 0x72, 0xb6, 0x53, 0xa4, 0x0a, 0x98, 0xa5, 0xb4, 0x37, 0x71, 0xcf, 0x66, 0x50, 0x6d]) , - fe([0x17, 0xa4, 0x19, 0x52, 0x11, 0x47, 0xb3, 0x5c, 0x5b, 0xa9, 0x2e, 0x22, 0xb4, 0x00, 0x52, 0xf9, - 0x57, 0x18, 0xb8, 0xbe, 0x5a, 0xe3, 0xab, 0x83, 0xc8, 0x87, 0x0a, 0x2a, 0xd8, 0x8c, 0xbb, 0x54])) + aff( + fe([ + 0x69, 0xd1, 0x39, 0xbd, 0xfb, 0x33, 0xbe, 0xc4, 0xf0, 0x5c, 0xef, 0xf0, 0x56, 0x68, 0xfc, 0x97, + 0x47, 0xc8, 0x72, 0xb6, 0x53, 0xa4, 0x0a, 0x98, 0xa5, 0xb4, 0x37, 0x71, 0xcf, 0x66, 0x50, 0x6d + ]), + fe([ + 0x17, 0xa4, 0x19, 0x52, 0x11, 0x47, 0xb3, 0x5c, 0x5b, 0xa9, 0x2e, 0x22, 0xb4, 0x00, 0x52, 0xf9, + 0x57, 0x18, 0xb8, 0xbe, 0x5a, 0xe3, 0xab, 0x83, 0xc8, 0x87, 0x0a, 0x2a, 0xd8, 0x8c, 0xbb, 0x54 + ]) + ) ) v.append( - aff(fe([0xa9, 0x62, 0x93, 0x85, 0xbe, 0xe8, 0x73, 0x4a, 0x0e, 0xb0, 0xb5, 0x2d, 0x94, 0x50, 0xaa, 0xd3, - 0xb2, 0xea, 0x9d, 0x62, 0x76, 0x3b, 0x07, 0x34, 0x4e, 0x2d, 0x70, 0xc8, 0x9a, 0x15, 0x66, 0x6b]) , - fe([0xc5, 0x96, 0xca, 0xc8, 0x22, 0x1a, 0xee, 0x5f, 0xe7, 0x31, 0x60, 0x22, 0x83, 0x08, 0x63, 0xce, - 0xb9, 0x32, 0x44, 0x58, 0x5d, 0x3a, 0x9b, 0xe4, 0x04, 0xd5, 0xef, 0x38, 0xef, 0x4b, 0xdd, 0x19])) + aff( + fe([ + 0xa9, 0x62, 0x93, 0x85, 0xbe, 0xe8, 0x73, 0x4a, 0x0e, 0xb0, 0xb5, 0x2d, 0x94, 0x50, 0xaa, 0xd3, + 0xb2, 0xea, 0x9d, 0x62, 0x76, 0x3b, 0x07, 0x34, 0x4e, 0x2d, 0x70, 0xc8, 0x9a, 0x15, 0x66, 0x6b + ]), + fe([ + 0xc5, 0x96, 0xca, 0xc8, 0x22, 0x1a, 0xee, 0x5f, 0xe7, 0x31, 0x60, 0x22, 0x83, 0x08, 0x63, 0xce, + 0xb9, 0x32, 0x44, 0x58, 0x5d, 0x3a, 0x9b, 0xe4, 0x04, 0xd5, 0xef, 0x38, 0xef, 0x4b, 0xdd, 0x19 + ]) + ) ) v.append( - aff(fe([0x4d, 0xc2, 0x17, 0x75, 0xa1, 0x68, 0xcd, 0xc3, 0xc6, 0x03, 0x44, 0xe3, 0x78, 0x09, 0x91, 0x47, - 0x3f, 0x0f, 0xe4, 0x92, 0x58, 0xfa, 0x7d, 0x1f, 0x20, 0x94, 0x58, 0x5e, 0xbc, 0x19, 0x02, 0x6f]) , - fe([0x20, 0xd6, 0xd8, 0x91, 0x54, 0xa7, 0xf3, 0x20, 0x4b, 0x34, 0x06, 0xfa, 0x30, 0xc8, 0x6f, 0x14, - 0x10, 0x65, 0x74, 0x13, 0x4e, 0xf0, 0x69, 0x26, 0xce, 0xcf, 0x90, 0xf4, 0xd0, 0xc5, 0xc8, 0x64])) + aff( + fe([ + 0x4d, 0xc2, 0x17, 0x75, 0xa1, 0x68, 0xcd, 0xc3, 0xc6, 0x03, 0x44, 0xe3, 0x78, 0x09, 0x91, 0x47, + 0x3f, 0x0f, 0xe4, 0x92, 0x58, 0xfa, 0x7d, 0x1f, 0x20, 0x94, 0x58, 0x5e, 0xbc, 0x19, 0x02, 0x6f + ]), + fe([ + 0x20, 0xd6, 0xd8, 0x91, 0x54, 0xa7, 0xf3, 0x20, 0x4b, 0x34, 0x06, 0xfa, 0x30, 0xc8, 0x6f, 0x14, + 0x10, 0x65, 0x74, 0x13, 0x4e, 0xf0, 0x69, 0x26, 0xce, 0xcf, 0x90, 0xf4, 0xd0, 0xc5, 0xc8, 0x64 + ]) + ) ) v.append( - aff(fe([0x26, 0xa2, 0x50, 0x02, 0x24, 0x72, 0xf1, 0xf0, 0x4e, 0x2d, 0x93, 0xd5, 0x08, 0xe7, 0xae, 0x38, - 0xf7, 0x18, 0xa5, 0x32, 0x34, 0xc2, 0xf0, 0xa6, 0xec, 0xb9, 0x61, 0x7b, 0x64, 0x99, 0xac, 0x71]) , - fe([0x25, 0xcf, 0x74, 0x55, 0x1b, 0xaa, 0xa9, 0x38, 0x41, 0x40, 0xd5, 0x95, 0x95, 0xab, 0x1c, 0x5e, - 0xbc, 0x41, 0x7e, 0x14, 0x30, 0xbe, 0x13, 0x89, 0xf4, 0xe5, 0xeb, 0x28, 0xc0, 0xc2, 0x96, 0x3a])) + aff( + fe([ + 0x26, 0xa2, 0x50, 0x02, 0x24, 0x72, 0xf1, 0xf0, 0x4e, 0x2d, 0x93, 0xd5, 0x08, 0xe7, 0xae, 0x38, + 0xf7, 0x18, 0xa5, 0x32, 0x34, 0xc2, 0xf0, 0xa6, 0xec, 0xb9, 0x61, 0x7b, 0x64, 0x99, 0xac, 0x71 + ]), + fe([ + 0x25, 0xcf, 0x74, 0x55, 0x1b, 0xaa, 0xa9, 0x38, 0x41, 0x40, 0xd5, 0x95, 0x95, 0xab, 0x1c, 0x5e, + 0xbc, 0x41, 0x7e, 0x14, 0x30, 0xbe, 0x13, 0x89, 0xf4, 0xe5, 0xeb, 0x28, 0xc0, 0xc2, 0x96, 0x3a + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x2b, 0x77, 0x45, 0xec, 0x67, 0x76, 0x32, 0x4c, 0xb9, 0xdf, 0x25, 0x32, 0x6b, 0xcb, 0xe7, 0x14, - 0x61, 0x43, 0xee, 0xba, 0x9b, 0x71, 0xef, 0xd2, 0x48, 0x65, 0xbb, 0x1b, 0x8a, 0x13, 0x1b, 0x22]) , - fe([0x84, 0xad, 0x0c, 0x18, 0x38, 0x5a, 0xba, 0xd0, 0x98, 0x59, 0xbf, 0x37, 0xb0, 0x4f, 0x97, 0x60, - 0x20, 0xb3, 0x9b, 0x97, 0xf6, 0x08, 0x6c, 0xa4, 0xff, 0xfb, 0xb7, 0xfa, 0x95, 0xb2, 0x51, 0x79])) + aff( + fe([ + 0x2b, 0x77, 0x45, 0xec, 0x67, 0x76, 0x32, 0x4c, 0xb9, 0xdf, 0x25, 0x32, 0x6b, 0xcb, 0xe7, 0x14, + 0x61, 0x43, 0xee, 0xba, 0x9b, 0x71, 0xef, 0xd2, 0x48, 0x65, 0xbb, 0x1b, 0x8a, 0x13, 0x1b, 0x22 + ]), + fe([ + 0x84, 0xad, 0x0c, 0x18, 0x38, 0x5a, 0xba, 0xd0, 0x98, 0x59, 0xbf, 0x37, 0xb0, 0x4f, 0x97, 0x60, + 0x20, 0xb3, 0x9b, 0x97, 0xf6, 0x08, 0x6c, 0xa4, 0xff, 0xfb, 0xb7, 0xfa, 0x95, 0xb2, 0x51, 0x79 + ]) + ) ) v.append( - aff(fe([0x28, 0x5c, 0x3f, 0xdb, 0x6b, 0x18, 0x3b, 0x5c, 0xd1, 0x04, 0x28, 0xde, 0x85, 0x52, 0x31, 0xb5, - 0xbb, 0xf6, 0xa9, 0xed, 0xbe, 0x28, 0x4f, 0xb3, 0x7e, 0x05, 0x6a, 0xdb, 0x95, 0x0d, 0x1b, 0x1c]) , - fe([0xd5, 0xc5, 0xc3, 0x9a, 0x0a, 0xd0, 0x31, 0x3e, 0x07, 0x36, 0x8e, 0xc0, 0x8a, 0x62, 0xb1, 0xca, - 0xd6, 0x0e, 0x1e, 0x9d, 0xef, 0xab, 0x98, 0x4d, 0xbb, 0x6c, 0x05, 0xe0, 0xe4, 0x5d, 0xbd, 0x57])) + aff( + fe([ + 0x28, 0x5c, 0x3f, 0xdb, 0x6b, 0x18, 0x3b, 0x5c, 0xd1, 0x04, 0x28, 0xde, 0x85, 0x52, 0x31, 0xb5, + 0xbb, 0xf6, 0xa9, 0xed, 0xbe, 0x28, 0x4f, 0xb3, 0x7e, 0x05, 0x6a, 0xdb, 0x95, 0x0d, 0x1b, 0x1c + ]), + fe([ + 0xd5, 0xc5, 0xc3, 0x9a, 0x0a, 0xd0, 0x31, 0x3e, 0x07, 0x36, 0x8e, 0xc0, 0x8a, 0x62, 0xb1, 0xca, + 0xd6, 0x0e, 0x1e, 0x9d, 0xef, 0xab, 0x98, 0x4d, 0xbb, 0x6c, 0x05, 0xe0, 0xe4, 0x5d, 0xbd, 0x57 + ]) + ) ) v.append( - aff(fe([0xcc, 0x21, 0x27, 0xce, 0xfd, 0xa9, 0x94, 0x8e, 0xe1, 0xab, 0x49, 0xe0, 0x46, 0x26, 0xa1, 0xa8, - 0x8c, 0xa1, 0x99, 0x1d, 0xb4, 0x27, 0x6d, 0x2d, 0xc8, 0x39, 0x30, 0x5e, 0x37, 0x52, 0xc4, 0x6e]) , - fe([0xa9, 0x85, 0xf4, 0xe7, 0xb0, 0x15, 0x33, 0x84, 0x1b, 0x14, 0x1a, 0x02, 0xd9, 0x3b, 0xad, 0x0f, - 0x43, 0x6c, 0xea, 0x3e, 0x0f, 0x7e, 0xda, 0xdd, 0x6b, 0x4c, 0x7f, 0x6e, 0xd4, 0x6b, 0xbf, 0x0f])) + aff( + fe([ + 0xcc, 0x21, 0x27, 0xce, 0xfd, 0xa9, 0x94, 0x8e, 0xe1, 0xab, 0x49, 0xe0, 0x46, 0x26, 0xa1, 0xa8, + 0x8c, 0xa1, 0x99, 0x1d, 0xb4, 0x27, 0x6d, 0x2d, 0xc8, 0x39, 0x30, 0x5e, 0x37, 0x52, 0xc4, 0x6e + ]), + fe([ + 0xa9, 0x85, 0xf4, 0xe7, 0xb0, 0x15, 0x33, 0x84, 0x1b, 0x14, 0x1a, 0x02, 0xd9, 0x3b, 0xad, 0x0f, + 0x43, 0x6c, 0xea, 0x3e, 0x0f, 0x7e, 0xda, 0xdd, 0x6b, 0x4c, 0x7f, 0x6e, 0xd4, 0x6b, 0xbf, 0x0f + ]) + ) ) v.append( - aff(fe([0x47, 0x9f, 0x7c, 0x56, 0x7c, 0x43, 0x91, 0x1c, 0xbb, 0x4e, 0x72, 0x3e, 0x64, 0xab, 0xa0, 0xa0, - 0xdf, 0xb4, 0xd8, 0x87, 0x3a, 0xbd, 0xa8, 0x48, 0xc9, 0xb8, 0xef, 0x2e, 0xad, 0x6f, 0x84, 0x4f]) , - fe([0x2d, 0x2d, 0xf0, 0x1b, 0x7e, 0x2a, 0x6c, 0xf8, 0xa9, 0x6a, 0xe1, 0xf0, 0x99, 0xa1, 0x67, 0x9a, - 0xd4, 0x13, 0xca, 0xca, 0xba, 0x27, 0x92, 0xaa, 0xa1, 0x5d, 0x50, 0xde, 0xcc, 0x40, 0x26, 0x0a])) + aff( + fe([ + 0x47, 0x9f, 0x7c, 0x56, 0x7c, 0x43, 0x91, 0x1c, 0xbb, 0x4e, 0x72, 0x3e, 0x64, 0xab, 0xa0, 0xa0, + 0xdf, 0xb4, 0xd8, 0x87, 0x3a, 0xbd, 0xa8, 0x48, 0xc9, 0xb8, 0xef, 0x2e, 0xad, 0x6f, 0x84, 0x4f + ]), + fe([ + 0x2d, 0x2d, 0xf0, 0x1b, 0x7e, 0x2a, 0x6c, 0xf8, 0xa9, 0x6a, 0xe1, 0xf0, 0x99, 0xa1, 0x67, 0x9a, + 0xd4, 0x13, 0xca, 0xca, 0xba, 0x27, 0x92, 0xaa, 0xa1, 0x5d, 0x50, 0xde, 0xcc, 0x40, 0x26, 0x0a + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x9f, 0x3e, 0xf2, 0xb2, 0x90, 0xce, 0xdb, 0x64, 0x3e, 0x03, 0xdd, 0x37, 0x36, 0x54, 0x70, 0x76, - 0x24, 0xb5, 0x69, 0x03, 0xfc, 0xa0, 0x2b, 0x74, 0xb2, 0x05, 0x0e, 0xcc, 0xd8, 0x1f, 0x6a, 0x1f]) , - fe([0x19, 0x5e, 0x60, 0x69, 0x58, 0x86, 0xa0, 0x31, 0xbd, 0x32, 0xe9, 0x2c, 0x5c, 0xd2, 0x85, 0xba, - 0x40, 0x64, 0xa8, 0x74, 0xf8, 0x0e, 0x1c, 0xb3, 0xa9, 0x69, 0xe8, 0x1e, 0x40, 0x64, 0x99, 0x77])) + aff( + fe([ + 0x9f, 0x3e, 0xf2, 0xb2, 0x90, 0xce, 0xdb, 0x64, 0x3e, 0x03, 0xdd, 0x37, 0x36, 0x54, 0x70, 0x76, + 0x24, 0xb5, 0x69, 0x03, 0xfc, 0xa0, 0x2b, 0x74, 0xb2, 0x05, 0x0e, 0xcc, 0xd8, 0x1f, 0x6a, 0x1f + ]), + fe([ + 0x19, 0x5e, 0x60, 0x69, 0x58, 0x86, 0xa0, 0x31, 0xbd, 0x32, 0xe9, 0x2c, 0x5c, 0xd2, 0x85, 0xba, + 0x40, 0x64, 0xa8, 0x74, 0xf8, 0x0e, 0x1c, 0xb3, 0xa9, 0x69, 0xe8, 0x1e, 0x40, 0x64, 0x99, 0x77 + ]) + ) ) v.append( - aff(fe([0x6c, 0x32, 0x4f, 0xfd, 0xbb, 0x5c, 0xbb, 0x8d, 0x64, 0x66, 0x4a, 0x71, 0x1f, 0x79, 0xa3, 0xad, - 0x8d, 0xf9, 0xd4, 0xec, 0xcf, 0x67, 0x70, 0xfa, 0x05, 0x4a, 0x0f, 0x6e, 0xaf, 0x87, 0x0a, 0x6f]) , - fe([0xc6, 0x36, 0x6e, 0x6c, 0x8c, 0x24, 0x09, 0x60, 0xbe, 0x26, 0xd2, 0x4c, 0x5e, 0x17, 0xca, 0x5f, - 0x1d, 0xcc, 0x87, 0xe8, 0x42, 0x6a, 0xcb, 0xcb, 0x7d, 0x92, 0x05, 0x35, 0x81, 0x13, 0x60, 0x6b])) + aff( + fe([ + 0x6c, 0x32, 0x4f, 0xfd, 0xbb, 0x5c, 0xbb, 0x8d, 0x64, 0x66, 0x4a, 0x71, 0x1f, 0x79, 0xa3, 0xad, + 0x8d, 0xf9, 0xd4, 0xec, 0xcf, 0x67, 0x70, 0xfa, 0x05, 0x4a, 0x0f, 0x6e, 0xaf, 0x87, 0x0a, 0x6f + ]), + fe([ + 0xc6, 0x36, 0x6e, 0x6c, 0x8c, 0x24, 0x09, 0x60, 0xbe, 0x26, 0xd2, 0x4c, 0x5e, 0x17, 0xca, 0x5f, + 0x1d, 0xcc, 0x87, 0xe8, 0x42, 0x6a, 0xcb, 0xcb, 0x7d, 0x92, 0x05, 0x35, 0x81, 0x13, 0x60, 0x6b + ]) + ) ) v.append( - aff(fe([0xf4, 0x15, 0xcd, 0x0f, 0x0a, 0xaf, 0x4e, 0x6b, 0x51, 0xfd, 0x14, 0xc4, 0x2e, 0x13, 0x86, 0x74, - 0x44, 0xcb, 0x66, 0x6b, 0xb6, 0x9d, 0x74, 0x56, 0x32, 0xac, 0x8d, 0x8e, 0x8c, 0x8c, 0x8c, 0x39]) , - fe([0xca, 0x59, 0x74, 0x1a, 0x11, 0xef, 0x6d, 0xf7, 0x39, 0x5c, 0x3b, 0x1f, 0xfa, 0xe3, 0x40, 0x41, - 0x23, 0x9e, 0xf6, 0xd1, 0x21, 0xa2, 0xbf, 0xad, 0x65, 0x42, 0x6b, 0x59, 0x8a, 0xe8, 0xc5, 0x7f])) + aff( + fe([ + 0xf4, 0x15, 0xcd, 0x0f, 0x0a, 0xaf, 0x4e, 0x6b, 0x51, 0xfd, 0x14, 0xc4, 0x2e, 0x13, 0x86, 0x74, + 0x44, 0xcb, 0x66, 0x6b, 0xb6, 0x9d, 0x74, 0x56, 0x32, 0xac, 0x8d, 0x8e, 0x8c, 0x8c, 0x8c, 0x39 + ]), + fe([ + 0xca, 0x59, 0x74, 0x1a, 0x11, 0xef, 0x6d, 0xf7, 0x39, 0x5c, 0x3b, 0x1f, 0xfa, 0xe3, 0x40, 0x41, + 0x23, 0x9e, 0xf6, 0xd1, 0x21, 0xa2, 0xbf, 0xad, 0x65, 0x42, 0x6b, 0x59, 0x8a, 0xe8, 0xc5, 0x7f + ]) + ) ) v.append( - aff(fe([0x64, 0x05, 0x7a, 0x84, 0x4a, 0x13, 0xc3, 0xf6, 0xb0, 0x6e, 0x9a, 0x6b, 0x53, 0x6b, 0x32, 0xda, - 0xd9, 0x74, 0x75, 0xc4, 0xba, 0x64, 0x3d, 0x3b, 0x08, 0xdd, 0x10, 0x46, 0xef, 0xc7, 0x90, 0x1f]) , - fe([0x7b, 0x2f, 0x3a, 0xce, 0xc8, 0xa1, 0x79, 0x3c, 0x30, 0x12, 0x44, 0x28, 0xf6, 0xbc, 0xff, 0xfd, - 0xf4, 0xc0, 0x97, 0xb0, 0xcc, 0xc3, 0x13, 0x7a, 0xb9, 0x9a, 0x16, 0xe4, 0xcb, 0x4c, 0x34, 0x63])) + aff( + fe([ + 0x64, 0x05, 0x7a, 0x84, 0x4a, 0x13, 0xc3, 0xf6, 0xb0, 0x6e, 0x9a, 0x6b, 0x53, 0x6b, 0x32, 0xda, + 0xd9, 0x74, 0x75, 0xc4, 0xba, 0x64, 0x3d, 0x3b, 0x08, 0xdd, 0x10, 0x46, 0xef, 0xc7, 0x90, 0x1f + ]), + fe([ + 0x7b, 0x2f, 0x3a, 0xce, 0xc8, 0xa1, 0x79, 0x3c, 0x30, 0x12, 0x44, 0x28, 0xf6, 0xbc, 0xff, 0xfd, + 0xf4, 0xc0, 0x97, 0xb0, 0xcc, 0xc3, 0x13, 0x7a, 0xb9, 0x9a, 0x16, 0xe4, 0xcb, 0x4c, 0x34, 0x63 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x07, 0x4e, 0xd3, 0x2d, 0x09, 0x33, 0x0e, 0xd2, 0x0d, 0xbe, 0x3e, 0xe7, 0xe4, 0xaa, 0xb7, 0x00, - 0x8b, 0xe8, 0xad, 0xaa, 0x7a, 0x8d, 0x34, 0x28, 0xa9, 0x81, 0x94, 0xc5, 0xe7, 0x42, 0xac, 0x47]) , - fe([0x24, 0x89, 0x7a, 0x8f, 0xb5, 0x9b, 0xf0, 0xc2, 0x03, 0x64, 0xd0, 0x1e, 0xf5, 0xa4, 0xb2, 0xf3, - 0x74, 0xe9, 0x1a, 0x16, 0xfd, 0xcb, 0x15, 0xea, 0xeb, 0x10, 0x6c, 0x35, 0xd1, 0xc1, 0xa6, 0x28])) + aff( + fe([ + 0x07, 0x4e, 0xd3, 0x2d, 0x09, 0x33, 0x0e, 0xd2, 0x0d, 0xbe, 0x3e, 0xe7, 0xe4, 0xaa, 0xb7, 0x00, + 0x8b, 0xe8, 0xad, 0xaa, 0x7a, 0x8d, 0x34, 0x28, 0xa9, 0x81, 0x94, 0xc5, 0xe7, 0x42, 0xac, 0x47 + ]), + fe([ + 0x24, 0x89, 0x7a, 0x8f, 0xb5, 0x9b, 0xf0, 0xc2, 0x03, 0x64, 0xd0, 0x1e, 0xf5, 0xa4, 0xb2, 0xf3, + 0x74, 0xe9, 0x1a, 0x16, 0xfd, 0xcb, 0x15, 0xea, 0xeb, 0x10, 0x6c, 0x35, 0xd1, 0xc1, 0xa6, 0x28 + ]) + ) ) v.append( - aff(fe([0xcc, 0xd5, 0x39, 0xfc, 0xa5, 0xa4, 0xad, 0x32, 0x15, 0xce, 0x19, 0xe8, 0x34, 0x2b, 0x1c, 0x60, - 0x91, 0xfc, 0x05, 0xa9, 0xb3, 0xdc, 0x80, 0x29, 0xc4, 0x20, 0x79, 0x06, 0x39, 0xc0, 0xe2, 0x22]) , - fe([0xbb, 0xa8, 0xe1, 0x89, 0x70, 0x57, 0x18, 0x54, 0x3c, 0xf6, 0x0d, 0x82, 0x12, 0x05, 0x87, 0x96, - 0x06, 0x39, 0xe3, 0xf8, 0xb3, 0x95, 0xe5, 0xd7, 0x26, 0xbf, 0x09, 0x5a, 0x94, 0xf9, 0x1c, 0x63])) + aff( + fe([ + 0xcc, 0xd5, 0x39, 0xfc, 0xa5, 0xa4, 0xad, 0x32, 0x15, 0xce, 0x19, 0xe8, 0x34, 0x2b, 0x1c, 0x60, + 0x91, 0xfc, 0x05, 0xa9, 0xb3, 0xdc, 0x80, 0x29, 0xc4, 0x20, 0x79, 0x06, 0x39, 0xc0, 0xe2, 0x22 + ]), + fe([ + 0xbb, 0xa8, 0xe1, 0x89, 0x70, 0x57, 0x18, 0x54, 0x3c, 0xf6, 0x0d, 0x82, 0x12, 0x05, 0x87, 0x96, + 0x06, 0x39, 0xe3, 0xf8, 0xb3, 0x95, 0xe5, 0xd7, 0x26, 0xbf, 0x09, 0x5a, 0x94, 0xf9, 0x1c, 0x63 + ]) + ) ) v.append( - aff(fe([0x2b, 0x8c, 0x2d, 0x9a, 0x8b, 0x84, 0xf2, 0x56, 0xfb, 0xad, 0x2e, 0x7f, 0xb7, 0xfc, 0x30, 0xe1, - 0x35, 0x89, 0xba, 0x4d, 0xa8, 0x6d, 0xce, 0x8c, 0x8b, 0x30, 0xe0, 0xda, 0x29, 0x18, 0x11, 0x17]) , - fe([0x19, 0xa6, 0x5a, 0x65, 0x93, 0xc3, 0xb5, 0x31, 0x22, 0x4f, 0xf3, 0xf6, 0x0f, 0xeb, 0x28, 0xc3, - 0x7c, 0xeb, 0xce, 0x86, 0xec, 0x67, 0x76, 0x6e, 0x35, 0x45, 0x7b, 0xd8, 0x6b, 0x92, 0x01, 0x65])) + aff( + fe([ + 0x2b, 0x8c, 0x2d, 0x9a, 0x8b, 0x84, 0xf2, 0x56, 0xfb, 0xad, 0x2e, 0x7f, 0xb7, 0xfc, 0x30, 0xe1, + 0x35, 0x89, 0xba, 0x4d, 0xa8, 0x6d, 0xce, 0x8c, 0x8b, 0x30, 0xe0, 0xda, 0x29, 0x18, 0x11, 0x17 + ]), + fe([ + 0x19, 0xa6, 0x5a, 0x65, 0x93, 0xc3, 0xb5, 0x31, 0x22, 0x4f, 0xf3, 0xf6, 0x0f, 0xeb, 0x28, 0xc3, + 0x7c, 0xeb, 0xce, 0x86, 0xec, 0x67, 0x76, 0x6e, 0x35, 0x45, 0x7b, 0xd8, 0x6b, 0x92, 0x01, 0x65 + ]) + ) ) v.append( - aff(fe([0x3d, 0xd5, 0x9a, 0x64, 0x73, 0x36, 0xb1, 0xd6, 0x86, 0x98, 0x42, 0x3f, 0x8a, 0xf1, 0xc7, 0xf5, - 0x42, 0xa8, 0x9c, 0x52, 0xa8, 0xdc, 0xf9, 0x24, 0x3f, 0x4a, 0xa1, 0xa4, 0x5b, 0xe8, 0x62, 0x1a]) , - fe([0xc5, 0xbd, 0xc8, 0x14, 0xd5, 0x0d, 0xeb, 0xe1, 0xa5, 0xe6, 0x83, 0x11, 0x09, 0x00, 0x1d, 0x55, - 0x83, 0x51, 0x7e, 0x75, 0x00, 0x81, 0xb9, 0xcb, 0xd8, 0xc5, 0xe5, 0xa1, 0xd9, 0x17, 0x6d, 0x1f])) + aff( + fe([ + 0x3d, 0xd5, 0x9a, 0x64, 0x73, 0x36, 0xb1, 0xd6, 0x86, 0x98, 0x42, 0x3f, 0x8a, 0xf1, 0xc7, 0xf5, + 0x42, 0xa8, 0x9c, 0x52, 0xa8, 0xdc, 0xf9, 0x24, 0x3f, 0x4a, 0xa1, 0xa4, 0x5b, 0xe8, 0x62, 0x1a + ]), + fe([ + 0xc5, 0xbd, 0xc8, 0x14, 0xd5, 0x0d, 0xeb, 0xe1, 0xa5, 0xe6, 0x83, 0x11, 0x09, 0x00, 0x1d, 0x55, + 0x83, 0x51, 0x7e, 0x75, 0x00, 0x81, 0xb9, 0xcb, 0xd8, 0xc5, 0xe5, 0xa1, 0xd9, 0x17, 0x6d, 0x1f + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xea, 0xf9, 0xe4, 0xe9, 0xe1, 0x52, 0x3f, 0x51, 0x19, 0x0d, 0xdd, 0xd9, 0x9d, 0x93, 0x31, 0x87, - 0x23, 0x09, 0xd5, 0x83, 0xeb, 0x92, 0x09, 0x76, 0x6e, 0xe3, 0xf8, 0xc0, 0xa2, 0x66, 0xb5, 0x36]) , - fe([0x3a, 0xbb, 0x39, 0xed, 0x32, 0x02, 0xe7, 0x43, 0x7a, 0x38, 0x14, 0x84, 0xe3, 0x44, 0xd2, 0x5e, - 0x94, 0xdd, 0x78, 0x89, 0x55, 0x4c, 0x73, 0x9e, 0xe1, 0xe4, 0x3e, 0x43, 0xd0, 0x4a, 0xde, 0x1b])) + aff( + fe([ + 0xea, 0xf9, 0xe4, 0xe9, 0xe1, 0x52, 0x3f, 0x51, 0x19, 0x0d, 0xdd, 0xd9, 0x9d, 0x93, 0x31, 0x87, + 0x23, 0x09, 0xd5, 0x83, 0xeb, 0x92, 0x09, 0x76, 0x6e, 0xe3, 0xf8, 0xc0, 0xa2, 0x66, 0xb5, 0x36 + ]), + fe([ + 0x3a, 0xbb, 0x39, 0xed, 0x32, 0x02, 0xe7, 0x43, 0x7a, 0x38, 0x14, 0x84, 0xe3, 0x44, 0xd2, 0x5e, + 0x94, 0xdd, 0x78, 0x89, 0x55, 0x4c, 0x73, 0x9e, 0xe1, 0xe4, 0x3e, 0x43, 0xd0, 0x4a, 0xde, 0x1b + ]) + ) ) v.append( - aff(fe([0xb2, 0xe7, 0x8f, 0xe3, 0xa3, 0xc5, 0xcb, 0x72, 0xee, 0x79, 0x41, 0xf8, 0xdf, 0xee, 0x65, 0xc5, - 0x45, 0x77, 0x27, 0x3c, 0xbd, 0x58, 0xd3, 0x75, 0xe2, 0x04, 0x4b, 0xbb, 0x65, 0xf3, 0xc8, 0x0f]) , - fe([0x24, 0x7b, 0x93, 0x34, 0xb5, 0xe2, 0x74, 0x48, 0xcd, 0xa0, 0x0b, 0x92, 0x97, 0x66, 0x39, 0xf4, - 0xb0, 0xe2, 0x5d, 0x39, 0x6a, 0x5b, 0x45, 0x17, 0x78, 0x1e, 0xdb, 0x91, 0x81, 0x1c, 0xf9, 0x16])) + aff( + fe([ + 0xb2, 0xe7, 0x8f, 0xe3, 0xa3, 0xc5, 0xcb, 0x72, 0xee, 0x79, 0x41, 0xf8, 0xdf, 0xee, 0x65, 0xc5, + 0x45, 0x77, 0x27, 0x3c, 0xbd, 0x58, 0xd3, 0x75, 0xe2, 0x04, 0x4b, 0xbb, 0x65, 0xf3, 0xc8, 0x0f + ]), + fe([ + 0x24, 0x7b, 0x93, 0x34, 0xb5, 0xe2, 0x74, 0x48, 0xcd, 0xa0, 0x0b, 0x92, 0x97, 0x66, 0x39, 0xf4, + 0xb0, 0xe2, 0x5d, 0x39, 0x6a, 0x5b, 0x45, 0x17, 0x78, 0x1e, 0xdb, 0x91, 0x81, 0x1c, 0xf9, 0x16 + ]) + ) ) v.append( - aff(fe([0x16, 0xdf, 0xd1, 0x5a, 0xd5, 0xe9, 0x4e, 0x58, 0x95, 0x93, 0x5f, 0x51, 0x09, 0xc3, 0x2a, 0xc9, - 0xd4, 0x55, 0x48, 0x79, 0xa4, 0xa3, 0xb2, 0xc3, 0x62, 0xaa, 0x8c, 0xe8, 0xad, 0x47, 0x39, 0x1b]) , - fe([0x46, 0xda, 0x9e, 0x51, 0x3a, 0xe6, 0xd1, 0xa6, 0xbb, 0x4d, 0x7b, 0x08, 0xbe, 0x8c, 0xd5, 0xf3, - 0x3f, 0xfd, 0xf7, 0x44, 0x80, 0x2d, 0x53, 0x4b, 0xd0, 0x87, 0x68, 0xc1, 0xb5, 0xd8, 0xf7, 0x07])) + aff( + fe([ + 0x16, 0xdf, 0xd1, 0x5a, 0xd5, 0xe9, 0x4e, 0x58, 0x95, 0x93, 0x5f, 0x51, 0x09, 0xc3, 0x2a, 0xc9, + 0xd4, 0x55, 0x48, 0x79, 0xa4, 0xa3, 0xb2, 0xc3, 0x62, 0xaa, 0x8c, 0xe8, 0xad, 0x47, 0x39, 0x1b + ]), + fe([ + 0x46, 0xda, 0x9e, 0x51, 0x3a, 0xe6, 0xd1, 0xa6, 0xbb, 0x4d, 0x7b, 0x08, 0xbe, 0x8c, 0xd5, 0xf3, + 0x3f, 0xfd, 0xf7, 0x44, 0x80, 0x2d, 0x53, 0x4b, 0xd0, 0x87, 0x68, 0xc1, 0xb5, 0xd8, 0xf7, 0x07 + ]) + ) ) v.append( - aff(fe([0xf4, 0x10, 0x46, 0xbe, 0xb7, 0xd2, 0xd1, 0xce, 0x5e, 0x76, 0xa2, 0xd7, 0x03, 0xdc, 0xe4, 0x81, - 0x5a, 0xf6, 0x3c, 0xde, 0xae, 0x7a, 0x9d, 0x21, 0x34, 0xa5, 0xf6, 0xa9, 0x73, 0xe2, 0x8d, 0x60]) , - fe([0xfa, 0x44, 0x71, 0xf6, 0x41, 0xd8, 0xc6, 0x58, 0x13, 0x37, 0xeb, 0x84, 0x0f, 0x96, 0xc7, 0xdc, - 0xc8, 0xa9, 0x7a, 0x83, 0xb2, 0x2f, 0x31, 0xb1, 0x1a, 0xd8, 0x98, 0x3f, 0x11, 0xd0, 0x31, 0x3b])) + aff( + fe([ + 0xf4, 0x10, 0x46, 0xbe, 0xb7, 0xd2, 0xd1, 0xce, 0x5e, 0x76, 0xa2, 0xd7, 0x03, 0xdc, 0xe4, 0x81, + 0x5a, 0xf6, 0x3c, 0xde, 0xae, 0x7a, 0x9d, 0x21, 0x34, 0xa5, 0xf6, 0xa9, 0x73, 0xe2, 0x8d, 0x60 + ]), + fe([ + 0xfa, 0x44, 0x71, 0xf6, 0x41, 0xd8, 0xc6, 0x58, 0x13, 0x37, 0xeb, 0x84, 0x0f, 0x96, 0xc7, 0xdc, + 0xc8, 0xa9, 0x7a, 0x83, 0xb2, 0x2f, 0x31, 0xb1, 0x1a, 0xd8, 0x98, 0x3f, 0x11, 0xd0, 0x31, 0x3b + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x81, 0xd5, 0x34, 0x16, 0x01, 0xa3, 0x93, 0xea, 0x52, 0x94, 0xec, 0x93, 0xb7, 0x81, 0x11, 0x2d, - 0x58, 0xf9, 0xb5, 0x0a, 0xaa, 0x4f, 0xf6, 0x2e, 0x3f, 0x36, 0xbf, 0x33, 0x5a, 0xe7, 0xd1, 0x08]) , - fe([0x1a, 0xcf, 0x42, 0xae, 0xcc, 0xb5, 0x77, 0x39, 0xc4, 0x5b, 0x5b, 0xd0, 0x26, 0x59, 0x27, 0xd0, - 0x55, 0x71, 0x12, 0x9d, 0x88, 0x3d, 0x9c, 0xea, 0x41, 0x6a, 0xf0, 0x50, 0x93, 0x93, 0xdd, 0x47])) + aff( + fe([ + 0x81, 0xd5, 0x34, 0x16, 0x01, 0xa3, 0x93, 0xea, 0x52, 0x94, 0xec, 0x93, 0xb7, 0x81, 0x11, 0x2d, + 0x58, 0xf9, 0xb5, 0x0a, 0xaa, 0x4f, 0xf6, 0x2e, 0x3f, 0x36, 0xbf, 0x33, 0x5a, 0xe7, 0xd1, 0x08 + ]), + fe([ + 0x1a, 0xcf, 0x42, 0xae, 0xcc, 0xb5, 0x77, 0x39, 0xc4, 0x5b, 0x5b, 0xd0, 0x26, 0x59, 0x27, 0xd0, + 0x55, 0x71, 0x12, 0x9d, 0x88, 0x3d, 0x9c, 0xea, 0x41, 0x6a, 0xf0, 0x50, 0x93, 0x93, 0xdd, 0x47 + ]) + ) ) v.append( - aff(fe([0x6f, 0xc9, 0x51, 0x6d, 0x1c, 0xaa, 0xf5, 0xa5, 0x90, 0x3f, 0x14, 0xe2, 0x6e, 0x8e, 0x64, 0xfd, - 0xac, 0xe0, 0x4e, 0x22, 0xe5, 0xc1, 0xbc, 0x29, 0x0a, 0x6a, 0x9e, 0xa1, 0x60, 0xcb, 0x2f, 0x0b]) , - fe([0xdc, 0x39, 0x32, 0xf3, 0xa1, 0x44, 0xe9, 0xc5, 0xc3, 0x78, 0xfb, 0x95, 0x47, 0x34, 0x35, 0x34, - 0xe8, 0x25, 0xde, 0x93, 0xc6, 0xb4, 0x76, 0x6d, 0x86, 0x13, 0xc6, 0xe9, 0x68, 0xb5, 0x01, 0x63])) + aff( + fe([ + 0x6f, 0xc9, 0x51, 0x6d, 0x1c, 0xaa, 0xf5, 0xa5, 0x90, 0x3f, 0x14, 0xe2, 0x6e, 0x8e, 0x64, 0xfd, + 0xac, 0xe0, 0x4e, 0x22, 0xe5, 0xc1, 0xbc, 0x29, 0x0a, 0x6a, 0x9e, 0xa1, 0x60, 0xcb, 0x2f, 0x0b + ]), + fe([ + 0xdc, 0x39, 0x32, 0xf3, 0xa1, 0x44, 0xe9, 0xc5, 0xc3, 0x78, 0xfb, 0x95, 0x47, 0x34, 0x35, 0x34, + 0xe8, 0x25, 0xde, 0x93, 0xc6, 0xb4, 0x76, 0x6d, 0x86, 0x13, 0xc6, 0xe9, 0x68, 0xb5, 0x01, 0x63 + ]) + ) ) v.append( - aff(fe([0x1f, 0x9a, 0x52, 0x64, 0x97, 0xd9, 0x1c, 0x08, 0x51, 0x6f, 0x26, 0x9d, 0xaa, 0x93, 0x33, 0x43, - 0xfa, 0x77, 0xe9, 0x62, 0x9b, 0x5d, 0x18, 0x75, 0xeb, 0x78, 0xf7, 0x87, 0x8f, 0x41, 0xb4, 0x4d]) , - fe([0x13, 0xa8, 0x82, 0x3e, 0xe9, 0x13, 0xad, 0xeb, 0x01, 0xca, 0xcf, 0xda, 0xcd, 0xf7, 0x6c, 0xc7, - 0x7a, 0xdc, 0x1e, 0x6e, 0xc8, 0x4e, 0x55, 0x62, 0x80, 0xea, 0x78, 0x0c, 0x86, 0xb9, 0x40, 0x51])) + aff( + fe([ + 0x1f, 0x9a, 0x52, 0x64, 0x97, 0xd9, 0x1c, 0x08, 0x51, 0x6f, 0x26, 0x9d, 0xaa, 0x93, 0x33, 0x43, + 0xfa, 0x77, 0xe9, 0x62, 0x9b, 0x5d, 0x18, 0x75, 0xeb, 0x78, 0xf7, 0x87, 0x8f, 0x41, 0xb4, 0x4d + ]), + fe([ + 0x13, 0xa8, 0x82, 0x3e, 0xe9, 0x13, 0xad, 0xeb, 0x01, 0xca, 0xcf, 0xda, 0xcd, 0xf7, 0x6c, 0xc7, + 0x7a, 0xdc, 0x1e, 0x6e, 0xc8, 0x4e, 0x55, 0x62, 0x80, 0xea, 0x78, 0x0c, 0x86, 0xb9, 0x40, 0x51 + ]) + ) ) v.append( - aff(fe([0x27, 0xae, 0xd3, 0x0d, 0x4c, 0x8f, 0x34, 0xea, 0x7d, 0x3c, 0xe5, 0x8a, 0xcf, 0x5b, 0x92, 0xd8, - 0x30, 0x16, 0xb4, 0xa3, 0x75, 0xff, 0xeb, 0x27, 0xc8, 0x5c, 0x6c, 0xc2, 0xee, 0x6c, 0x21, 0x0b]) , - fe([0xc3, 0xba, 0x12, 0x53, 0x2a, 0xaa, 0x77, 0xad, 0x19, 0x78, 0x55, 0x8a, 0x2e, 0x60, 0x87, 0xc2, - 0x6e, 0x91, 0x38, 0x91, 0x3f, 0x7a, 0xc5, 0x24, 0x8f, 0x51, 0xc5, 0xde, 0xb0, 0x53, 0x30, 0x56])) + aff( + fe([ + 0x27, 0xae, 0xd3, 0x0d, 0x4c, 0x8f, 0x34, 0xea, 0x7d, 0x3c, 0xe5, 0x8a, 0xcf, 0x5b, 0x92, 0xd8, + 0x30, 0x16, 0xb4, 0xa3, 0x75, 0xff, 0xeb, 0x27, 0xc8, 0x5c, 0x6c, 0xc2, 0xee, 0x6c, 0x21, 0x0b + ]), + fe([ + 0xc3, 0xba, 0x12, 0x53, 0x2a, 0xaa, 0x77, 0xad, 0x19, 0x78, 0x55, 0x8a, 0x2e, 0x60, 0x87, 0xc2, + 0x6e, 0x91, 0x38, 0x91, 0x3f, 0x7a, 0xc5, 0x24, 0x8f, 0x51, 0xc5, 0xde, 0xb0, 0x53, 0x30, 0x56 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x02, 0xfe, 0x54, 0x12, 0x18, 0xca, 0x7d, 0xa5, 0x68, 0x43, 0xa3, 0x6d, 0x14, 0x2a, 0x6a, 0xa5, - 0x8e, 0x32, 0xe7, 0x63, 0x4f, 0xe3, 0xc6, 0x44, 0x3e, 0xab, 0x63, 0xca, 0x17, 0x86, 0x74, 0x3f]) , - fe([0x1e, 0x64, 0xc1, 0x7d, 0x52, 0xdc, 0x13, 0x5a, 0xa1, 0x9c, 0x4e, 0xee, 0x99, 0x28, 0xbb, 0x4c, - 0xee, 0xac, 0xa9, 0x1b, 0x89, 0xa2, 0x38, 0x39, 0x7b, 0xc4, 0x0f, 0x42, 0xe6, 0x89, 0xed, 0x0f])) + aff( + fe([ + 0x02, 0xfe, 0x54, 0x12, 0x18, 0xca, 0x7d, 0xa5, 0x68, 0x43, 0xa3, 0x6d, 0x14, 0x2a, 0x6a, 0xa5, + 0x8e, 0x32, 0xe7, 0x63, 0x4f, 0xe3, 0xc6, 0x44, 0x3e, 0xab, 0x63, 0xca, 0x17, 0x86, 0x74, 0x3f + ]), + fe([ + 0x1e, 0x64, 0xc1, 0x7d, 0x52, 0xdc, 0x13, 0x5a, 0xa1, 0x9c, 0x4e, 0xee, 0x99, 0x28, 0xbb, 0x4c, + 0xee, 0xac, 0xa9, 0x1b, 0x89, 0xa2, 0x38, 0x39, 0x7b, 0xc4, 0x0f, 0x42, 0xe6, 0x89, 0xed, 0x0f + ]) + ) ) v.append( - aff(fe([0xf3, 0x3c, 0x8c, 0x80, 0x83, 0x10, 0x8a, 0x37, 0x50, 0x9c, 0xb4, 0xdf, 0x3f, 0x8c, 0xf7, 0x23, - 0x07, 0xd6, 0xff, 0xa0, 0x82, 0x6c, 0x75, 0x3b, 0xe4, 0xb5, 0xbb, 0xe4, 0xe6, 0x50, 0xf0, 0x08]) , - fe([0x62, 0xee, 0x75, 0x48, 0x92, 0x33, 0xf2, 0xf4, 0xad, 0x15, 0x7a, 0xa1, 0x01, 0x46, 0xa9, 0x32, - 0x06, 0x88, 0xb6, 0x36, 0x47, 0x35, 0xb9, 0xb4, 0x42, 0x85, 0x76, 0xf0, 0x48, 0x00, 0x90, 0x38])) + aff( + fe([ + 0xf3, 0x3c, 0x8c, 0x80, 0x83, 0x10, 0x8a, 0x37, 0x50, 0x9c, 0xb4, 0xdf, 0x3f, 0x8c, 0xf7, 0x23, + 0x07, 0xd6, 0xff, 0xa0, 0x82, 0x6c, 0x75, 0x3b, 0xe4, 0xb5, 0xbb, 0xe4, 0xe6, 0x50, 0xf0, 0x08 + ]), + fe([ + 0x62, 0xee, 0x75, 0x48, 0x92, 0x33, 0xf2, 0xf4, 0xad, 0x15, 0x7a, 0xa1, 0x01, 0x46, 0xa9, 0x32, + 0x06, 0x88, 0xb6, 0x36, 0x47, 0x35, 0xb9, 0xb4, 0x42, 0x85, 0x76, 0xf0, 0x48, 0x00, 0x90, 0x38 + ]) + ) ) v.append( - aff(fe([0x51, 0x15, 0x9d, 0xc3, 0x95, 0xd1, 0x39, 0xbb, 0x64, 0x9d, 0x15, 0x81, 0xc1, 0x68, 0xd0, 0xb6, - 0xa4, 0x2c, 0x7d, 0x5e, 0x02, 0x39, 0x00, 0xe0, 0x3b, 0xa4, 0xcc, 0xca, 0x1d, 0x81, 0x24, 0x10]) , - fe([0xe7, 0x29, 0xf9, 0x37, 0xd9, 0x46, 0x5a, 0xcd, 0x70, 0xfe, 0x4d, 0x5b, 0xbf, 0xa5, 0xcf, 0x91, - 0xf4, 0xef, 0xee, 0x8a, 0x29, 0xd0, 0xe7, 0xc4, 0x25, 0x92, 0x8a, 0xff, 0x36, 0xfc, 0xe4, 0x49])) + aff( + fe([ + 0x51, 0x15, 0x9d, 0xc3, 0x95, 0xd1, 0x39, 0xbb, 0x64, 0x9d, 0x15, 0x81, 0xc1, 0x68, 0xd0, 0xb6, + 0xa4, 0x2c, 0x7d, 0x5e, 0x02, 0x39, 0x00, 0xe0, 0x3b, 0xa4, 0xcc, 0xca, 0x1d, 0x81, 0x24, 0x10 + ]), + fe([ + 0xe7, 0x29, 0xf9, 0x37, 0xd9, 0x46, 0x5a, 0xcd, 0x70, 0xfe, 0x4d, 0x5b, 0xbf, 0xa5, 0xcf, 0x91, + 0xf4, 0xef, 0xee, 0x8a, 0x29, 0xd0, 0xe7, 0xc4, 0x25, 0x92, 0x8a, 0xff, 0x36, 0xfc, 0xe4, 0x49 + ]) + ) ) v.append( - aff(fe([0xbd, 0x00, 0xb9, 0x04, 0x7d, 0x35, 0xfc, 0xeb, 0xd0, 0x0b, 0x05, 0x32, 0x52, 0x7a, 0x89, 0x24, - 0x75, 0x50, 0xe1, 0x63, 0x02, 0x82, 0x8e, 0xe7, 0x85, 0x0c, 0xf2, 0x56, 0x44, 0x37, 0x83, 0x25]) , - fe([0x8f, 0xa1, 0xce, 0xcb, 0x60, 0xda, 0x12, 0x02, 0x1e, 0x29, 0x39, 0x2a, 0x03, 0xb7, 0xeb, 0x77, - 0x40, 0xea, 0xc9, 0x2b, 0x2c, 0xd5, 0x7d, 0x7e, 0x2c, 0xc7, 0x5a, 0xfd, 0xff, 0xc4, 0xd1, 0x62])) + aff( + fe([ + 0xbd, 0x00, 0xb9, 0x04, 0x7d, 0x35, 0xfc, 0xeb, 0xd0, 0x0b, 0x05, 0x32, 0x52, 0x7a, 0x89, 0x24, + 0x75, 0x50, 0xe1, 0x63, 0x02, 0x82, 0x8e, 0xe7, 0x85, 0x0c, 0xf2, 0x56, 0x44, 0x37, 0x83, 0x25 + ]), + fe([ + 0x8f, 0xa1, 0xce, 0xcb, 0x60, 0xda, 0x12, 0x02, 0x1e, 0x29, 0x39, 0x2a, 0x03, 0xb7, 0xeb, 0x77, + 0x40, 0xea, 0xc9, 0x2b, 0x2c, 0xd5, 0x7d, 0x7e, 0x2c, 0xc7, 0x5a, 0xfd, 0xff, 0xc4, 0xd1, 0x62 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x1d, 0x88, 0x98, 0x5b, 0x4e, 0xfc, 0x41, 0x24, 0x05, 0xe6, 0x50, 0x2b, 0xae, 0x96, 0x51, 0xd9, - 0x6b, 0x72, 0xb2, 0x33, 0x42, 0x98, 0x68, 0xbb, 0x10, 0x5a, 0x7a, 0x8c, 0x9d, 0x07, 0xb4, 0x05]) , - fe([0x2f, 0x61, 0x9f, 0xd7, 0xa8, 0x3f, 0x83, 0x8c, 0x10, 0x69, 0x90, 0xe6, 0xcf, 0xd2, 0x63, 0xa3, - 0xe4, 0x54, 0x7e, 0xe5, 0x69, 0x13, 0x1c, 0x90, 0x57, 0xaa, 0xe9, 0x53, 0x22, 0x43, 0x29, 0x23])) + aff( + fe([ + 0x1d, 0x88, 0x98, 0x5b, 0x4e, 0xfc, 0x41, 0x24, 0x05, 0xe6, 0x50, 0x2b, 0xae, 0x96, 0x51, 0xd9, + 0x6b, 0x72, 0xb2, 0x33, 0x42, 0x98, 0x68, 0xbb, 0x10, 0x5a, 0x7a, 0x8c, 0x9d, 0x07, 0xb4, 0x05 + ]), + fe([ + 0x2f, 0x61, 0x9f, 0xd7, 0xa8, 0x3f, 0x83, 0x8c, 0x10, 0x69, 0x90, 0xe6, 0xcf, 0xd2, 0x63, 0xa3, + 0xe4, 0x54, 0x7e, 0xe5, 0x69, 0x13, 0x1c, 0x90, 0x57, 0xaa, 0xe9, 0x53, 0x22, 0x43, 0x29, 0x23 + ]) + ) ) v.append( - aff(fe([0xe5, 0x1c, 0xf8, 0x0a, 0xfd, 0x2d, 0x7e, 0xf5, 0xf5, 0x70, 0x7d, 0x41, 0x6b, 0x11, 0xfe, 0xbe, - 0x99, 0xd1, 0x55, 0x29, 0x31, 0xbf, 0xc0, 0x97, 0x6c, 0xd5, 0x35, 0xcc, 0x5e, 0x8b, 0xd9, 0x69]) , - fe([0x8e, 0x4e, 0x9f, 0x25, 0xf8, 0x81, 0x54, 0x2d, 0x0e, 0xd5, 0x54, 0x81, 0x9b, 0xa6, 0x92, 0xce, - 0x4b, 0xe9, 0x8f, 0x24, 0x3b, 0xca, 0xe0, 0x44, 0xab, 0x36, 0xfe, 0xfb, 0x87, 0xd4, 0x26, 0x3e])) + aff( + fe([ + 0xe5, 0x1c, 0xf8, 0x0a, 0xfd, 0x2d, 0x7e, 0xf5, 0xf5, 0x70, 0x7d, 0x41, 0x6b, 0x11, 0xfe, 0xbe, + 0x99, 0xd1, 0x55, 0x29, 0x31, 0xbf, 0xc0, 0x97, 0x6c, 0xd5, 0x35, 0xcc, 0x5e, 0x8b, 0xd9, 0x69 + ]), + fe([ + 0x8e, 0x4e, 0x9f, 0x25, 0xf8, 0x81, 0x54, 0x2d, 0x0e, 0xd5, 0x54, 0x81, 0x9b, 0xa6, 0x92, 0xce, + 0x4b, 0xe9, 0x8f, 0x24, 0x3b, 0xca, 0xe0, 0x44, 0xab, 0x36, 0xfe, 0xfb, 0x87, 0xd4, 0x26, 0x3e + ]) + ) ) v.append( - aff(fe([0x0f, 0x93, 0x9c, 0x11, 0xe7, 0xdb, 0xf1, 0xf0, 0x85, 0x43, 0x28, 0x15, 0x37, 0xdd, 0xde, 0x27, - 0xdf, 0xad, 0x3e, 0x49, 0x4f, 0xe0, 0x5b, 0xf6, 0x80, 0x59, 0x15, 0x3c, 0x85, 0xb7, 0x3e, 0x12]) , - fe([0xf5, 0xff, 0xcc, 0xf0, 0xb4, 0x12, 0x03, 0x5f, 0xc9, 0x84, 0xcb, 0x1d, 0x17, 0xe0, 0xbc, 0xcc, - 0x03, 0x62, 0xa9, 0x8b, 0x94, 0xa6, 0xaa, 0x18, 0xcb, 0x27, 0x8d, 0x49, 0xa6, 0x17, 0x15, 0x07])) + aff( + fe([ + 0x0f, 0x93, 0x9c, 0x11, 0xe7, 0xdb, 0xf1, 0xf0, 0x85, 0x43, 0x28, 0x15, 0x37, 0xdd, 0xde, 0x27, + 0xdf, 0xad, 0x3e, 0x49, 0x4f, 0xe0, 0x5b, 0xf6, 0x80, 0x59, 0x15, 0x3c, 0x85, 0xb7, 0x3e, 0x12 + ]), + fe([ + 0xf5, 0xff, 0xcc, 0xf0, 0xb4, 0x12, 0x03, 0x5f, 0xc9, 0x84, 0xcb, 0x1d, 0x17, 0xe0, 0xbc, 0xcc, + 0x03, 0x62, 0xa9, 0x8b, 0x94, 0xa6, 0xaa, 0x18, 0xcb, 0x27, 0x8d, 0x49, 0xa6, 0x17, 0x15, 0x07 + ]) + ) ) v.append( - aff(fe([0xd9, 0xb6, 0xd4, 0x9d, 0xd4, 0x6a, 0xaf, 0x70, 0x07, 0x2c, 0x10, 0x9e, 0xbd, 0x11, 0xad, 0xe4, - 0x26, 0x33, 0x70, 0x92, 0x78, 0x1c, 0x74, 0x9f, 0x75, 0x60, 0x56, 0xf4, 0x39, 0xa8, 0xa8, 0x62]) , - fe([0x3b, 0xbf, 0x55, 0x35, 0x61, 0x8b, 0x44, 0x97, 0xe8, 0x3a, 0x55, 0xc1, 0xc8, 0x3b, 0xfd, 0x95, - 0x29, 0x11, 0x60, 0x96, 0x1e, 0xcb, 0x11, 0x9d, 0xc2, 0x03, 0x8a, 0x1b, 0xc6, 0xd6, 0x45, 0x3d])) + aff( + fe([ + 0xd9, 0xb6, 0xd4, 0x9d, 0xd4, 0x6a, 0xaf, 0x70, 0x07, 0x2c, 0x10, 0x9e, 0xbd, 0x11, 0xad, 0xe4, + 0x26, 0x33, 0x70, 0x92, 0x78, 0x1c, 0x74, 0x9f, 0x75, 0x60, 0x56, 0xf4, 0x39, 0xa8, 0xa8, 0x62 + ]), + fe([ + 0x3b, 0xbf, 0x55, 0x35, 0x61, 0x8b, 0x44, 0x97, 0xe8, 0x3a, 0x55, 0xc1, 0xc8, 0x3b, 0xfd, 0x95, + 0x29, 0x11, 0x60, 0x96, 0x1e, 0xcb, 0x11, 0x9d, 0xc2, 0x03, 0x8a, 0x1b, 0xc6, 0xd6, 0x45, 0x3d + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x7e, 0x0e, 0x50, 0xb2, 0xcc, 0x0d, 0x6b, 0xa6, 0x71, 0x5b, 0x42, 0xed, 0xbd, 0xaf, 0xac, 0xf0, - 0xfc, 0x12, 0xa2, 0x3f, 0x4e, 0xda, 0xe8, 0x11, 0xf3, 0x23, 0xe1, 0x04, 0x62, 0x03, 0x1c, 0x4e]) , - fe([0xc8, 0xb1, 0x1b, 0x6f, 0x73, 0x61, 0x3d, 0x27, 0x0d, 0x7d, 0x7a, 0x25, 0x5f, 0x73, 0x0e, 0x2f, - 0x93, 0xf6, 0x24, 0xd8, 0x4f, 0x90, 0xac, 0xa2, 0x62, 0x0a, 0xf0, 0x61, 0xd9, 0x08, 0x59, 0x6a])) + aff( + fe([ + 0x7e, 0x0e, 0x50, 0xb2, 0xcc, 0x0d, 0x6b, 0xa6, 0x71, 0x5b, 0x42, 0xed, 0xbd, 0xaf, 0xac, 0xf0, + 0xfc, 0x12, 0xa2, 0x3f, 0x4e, 0xda, 0xe8, 0x11, 0xf3, 0x23, 0xe1, 0x04, 0x62, 0x03, 0x1c, 0x4e + ]), + fe([ + 0xc8, 0xb1, 0x1b, 0x6f, 0x73, 0x61, 0x3d, 0x27, 0x0d, 0x7d, 0x7a, 0x25, 0x5f, 0x73, 0x0e, 0x2f, + 0x93, 0xf6, 0x24, 0xd8, 0x4f, 0x90, 0xac, 0xa2, 0x62, 0x0a, 0xf0, 0x61, 0xd9, 0x08, 0x59, 0x6a + ]) + ) ) v.append( - aff(fe([0x6f, 0x2d, 0x55, 0xf8, 0x2f, 0x8e, 0xf0, 0x18, 0x3b, 0xea, 0xdd, 0x26, 0x72, 0xd1, 0xf5, 0xfe, - 0xe5, 0xb8, 0xe6, 0xd3, 0x10, 0x48, 0x46, 0x49, 0x3a, 0x9f, 0x5e, 0x45, 0x6b, 0x90, 0xe8, 0x7f]) , - fe([0xd3, 0x76, 0x69, 0x33, 0x7b, 0xb9, 0x40, 0x70, 0xee, 0xa6, 0x29, 0x6b, 0xdd, 0xd0, 0x5d, 0x8d, - 0xc1, 0x3e, 0x4a, 0xea, 0x37, 0xb1, 0x03, 0x02, 0x03, 0x35, 0xf1, 0x28, 0x9d, 0xff, 0x00, 0x13])) + aff( + fe([ + 0x6f, 0x2d, 0x55, 0xf8, 0x2f, 0x8e, 0xf0, 0x18, 0x3b, 0xea, 0xdd, 0x26, 0x72, 0xd1, 0xf5, 0xfe, + 0xe5, 0xb8, 0xe6, 0xd3, 0x10, 0x48, 0x46, 0x49, 0x3a, 0x9f, 0x5e, 0x45, 0x6b, 0x90, 0xe8, 0x7f + ]), + fe([ + 0xd3, 0x76, 0x69, 0x33, 0x7b, 0xb9, 0x40, 0x70, 0xee, 0xa6, 0x29, 0x6b, 0xdd, 0xd0, 0x5d, 0x8d, + 0xc1, 0x3e, 0x4a, 0xea, 0x37, 0xb1, 0x03, 0x02, 0x03, 0x35, 0xf1, 0x28, 0x9d, 0xff, 0x00, 0x13 + ]) + ) ) v.append( - aff(fe([0x7a, 0xdb, 0x12, 0xd2, 0x8a, 0x82, 0x03, 0x1b, 0x1e, 0xaf, 0xf9, 0x4b, 0x9c, 0xbe, 0xae, 0x7c, - 0xe4, 0x94, 0x2a, 0x23, 0xb3, 0x62, 0x86, 0xe7, 0xfd, 0x23, 0xaa, 0x99, 0xbd, 0x2b, 0x11, 0x6c]) , - fe([0x8d, 0xa6, 0xd5, 0xac, 0x9d, 0xcc, 0x68, 0x75, 0x7f, 0xc3, 0x4d, 0x4b, 0xdd, 0x6c, 0xbb, 0x11, - 0x5a, 0x60, 0xe5, 0xbd, 0x7d, 0x27, 0x8b, 0xda, 0xb4, 0x95, 0xf6, 0x03, 0x27, 0xa4, 0x92, 0x3f])) + aff( + fe([ + 0x7a, 0xdb, 0x12, 0xd2, 0x8a, 0x82, 0x03, 0x1b, 0x1e, 0xaf, 0xf9, 0x4b, 0x9c, 0xbe, 0xae, 0x7c, + 0xe4, 0x94, 0x2a, 0x23, 0xb3, 0x62, 0x86, 0xe7, 0xfd, 0x23, 0xaa, 0x99, 0xbd, 0x2b, 0x11, 0x6c + ]), + fe([ + 0x8d, 0xa6, 0xd5, 0xac, 0x9d, 0xcc, 0x68, 0x75, 0x7f, 0xc3, 0x4d, 0x4b, 0xdd, 0x6c, 0xbb, 0x11, + 0x5a, 0x60, 0xe5, 0xbd, 0x7d, 0x27, 0x8b, 0xda, 0xb4, 0x95, 0xf6, 0x03, 0x27, 0xa4, 0x92, 0x3f + ]) + ) ) v.append( - aff(fe([0x22, 0xd6, 0xb5, 0x17, 0x84, 0xbf, 0x12, 0xcc, 0x23, 0x14, 0x4a, 0xdf, 0x14, 0x31, 0xbc, 0xa1, - 0xac, 0x6e, 0xab, 0xfa, 0x57, 0x11, 0x53, 0xb3, 0x27, 0xe6, 0xf9, 0x47, 0x33, 0x44, 0x34, 0x1e]) , - fe([0x79, 0xfc, 0xa6, 0xb4, 0x0b, 0x35, 0x20, 0xc9, 0x4d, 0x22, 0x84, 0xc4, 0xa9, 0x20, 0xec, 0x89, - 0x94, 0xba, 0x66, 0x56, 0x48, 0xb9, 0x87, 0x7f, 0xca, 0x1e, 0x06, 0xed, 0xa5, 0x55, 0x59, 0x29])) + aff( + fe([ + 0x22, 0xd6, 0xb5, 0x17, 0x84, 0xbf, 0x12, 0xcc, 0x23, 0x14, 0x4a, 0xdf, 0x14, 0x31, 0xbc, 0xa1, + 0xac, 0x6e, 0xab, 0xfa, 0x57, 0x11, 0x53, 0xb3, 0x27, 0xe6, 0xf9, 0x47, 0x33, 0x44, 0x34, 0x1e + ]), + fe([ + 0x79, 0xfc, 0xa6, 0xb4, 0x0b, 0x35, 0x20, 0xc9, 0x4d, 0x22, 0x84, 0xc4, 0xa9, 0x20, 0xec, 0x89, + 0x94, 0xba, 0x66, 0x56, 0x48, 0xb9, 0x87, 0x7f, 0xca, 0x1e, 0x06, 0xed, 0xa5, 0x55, 0x59, 0x29 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x56, 0xe1, 0xf5, 0xf1, 0xd5, 0xab, 0xa8, 0x2b, 0xae, 0x89, 0xf3, 0xcf, 0x56, 0x9f, 0xf2, 0x4b, - 0x31, 0xbc, 0x18, 0xa9, 0x06, 0x5b, 0xbe, 0xb4, 0x61, 0xf8, 0xb2, 0x06, 0x9c, 0x81, 0xab, 0x4c]) , - fe([0x1f, 0x68, 0x76, 0x01, 0x16, 0x38, 0x2b, 0x0f, 0x77, 0x97, 0x92, 0x67, 0x4e, 0x86, 0x6a, 0x8b, - 0xe5, 0xe8, 0x0c, 0xf7, 0x36, 0x39, 0xb5, 0x33, 0xe6, 0xcf, 0x5e, 0xbd, 0x18, 0xfb, 0x10, 0x1f])) + aff( + fe([ + 0x56, 0xe1, 0xf5, 0xf1, 0xd5, 0xab, 0xa8, 0x2b, 0xae, 0x89, 0xf3, 0xcf, 0x56, 0x9f, 0xf2, 0x4b, + 0x31, 0xbc, 0x18, 0xa9, 0x06, 0x5b, 0xbe, 0xb4, 0x61, 0xf8, 0xb2, 0x06, 0x9c, 0x81, 0xab, 0x4c + ]), + fe([ + 0x1f, 0x68, 0x76, 0x01, 0x16, 0x38, 0x2b, 0x0f, 0x77, 0x97, 0x92, 0x67, 0x4e, 0x86, 0x6a, 0x8b, + 0xe5, 0xe8, 0x0c, 0xf7, 0x36, 0x39, 0xb5, 0x33, 0xe6, 0xcf, 0x5e, 0xbd, 0x18, 0xfb, 0x10, 0x1f + ]) + ) ) v.append( - aff(fe([0x83, 0xf0, 0x0d, 0x63, 0xef, 0x53, 0x6b, 0xb5, 0x6b, 0xf9, 0x83, 0xcf, 0xde, 0x04, 0x22, 0x9b, - 0x2c, 0x0a, 0xe0, 0xa5, 0xd8, 0xc7, 0x9c, 0xa5, 0xa3, 0xf6, 0x6f, 0xcf, 0x90, 0x6b, 0x68, 0x7c]) , - fe([0x33, 0x15, 0xd7, 0x7f, 0x1a, 0xd5, 0x21, 0x58, 0xc4, 0x18, 0xa5, 0xf0, 0xcc, 0x73, 0xa8, 0xfd, - 0xfa, 0x18, 0xd1, 0x03, 0x91, 0x8d, 0x52, 0xd2, 0xa3, 0xa4, 0xd3, 0xb1, 0xea, 0x1d, 0x0f, 0x00])) + aff( + fe([ + 0x83, 0xf0, 0x0d, 0x63, 0xef, 0x53, 0x6b, 0xb5, 0x6b, 0xf9, 0x83, 0xcf, 0xde, 0x04, 0x22, 0x9b, + 0x2c, 0x0a, 0xe0, 0xa5, 0xd8, 0xc7, 0x9c, 0xa5, 0xa3, 0xf6, 0x6f, 0xcf, 0x90, 0x6b, 0x68, 0x7c + ]), + fe([ + 0x33, 0x15, 0xd7, 0x7f, 0x1a, 0xd5, 0x21, 0x58, 0xc4, 0x18, 0xa5, 0xf0, 0xcc, 0x73, 0xa8, 0xfd, + 0xfa, 0x18, 0xd1, 0x03, 0x91, 0x8d, 0x52, 0xd2, 0xa3, 0xa4, 0xd3, 0xb1, 0xea, 0x1d, 0x0f, 0x00 + ]) + ) ) v.append( - aff(fe([0xcc, 0x48, 0x83, 0x90, 0xe5, 0xfd, 0x3f, 0x84, 0xaa, 0xf9, 0x8b, 0x82, 0x59, 0x24, 0x34, 0x68, - 0x4f, 0x1c, 0x23, 0xd9, 0xcc, 0x71, 0xe1, 0x7f, 0x8c, 0xaf, 0xf1, 0xee, 0x00, 0xb6, 0xa0, 0x77]) , - fe([0xf5, 0x1a, 0x61, 0xf7, 0x37, 0x9d, 0x00, 0xf4, 0xf2, 0x69, 0x6f, 0x4b, 0x01, 0x85, 0x19, 0x45, - 0x4d, 0x7f, 0x02, 0x7c, 0x6a, 0x05, 0x47, 0x6c, 0x1f, 0x81, 0x20, 0xd4, 0xe8, 0x50, 0x27, 0x72])) + aff( + fe([ + 0xcc, 0x48, 0x83, 0x90, 0xe5, 0xfd, 0x3f, 0x84, 0xaa, 0xf9, 0x8b, 0x82, 0x59, 0x24, 0x34, 0x68, + 0x4f, 0x1c, 0x23, 0xd9, 0xcc, 0x71, 0xe1, 0x7f, 0x8c, 0xaf, 0xf1, 0xee, 0x00, 0xb6, 0xa0, 0x77 + ]), + fe([ + 0xf5, 0x1a, 0x61, 0xf7, 0x37, 0x9d, 0x00, 0xf4, 0xf2, 0x69, 0x6f, 0x4b, 0x01, 0x85, 0x19, 0x45, + 0x4d, 0x7f, 0x02, 0x7c, 0x6a, 0x05, 0x47, 0x6c, 0x1f, 0x81, 0x20, 0xd4, 0xe8, 0x50, 0x27, 0x72 + ]) + ) ) v.append( - aff(fe([0x2c, 0x3a, 0xe5, 0xad, 0xf4, 0xdd, 0x2d, 0xf7, 0x5c, 0x44, 0xb5, 0x5b, 0x21, 0xa3, 0x89, 0x5f, - 0x96, 0x45, 0xca, 0x4d, 0xa4, 0x21, 0x99, 0x70, 0xda, 0xc4, 0xc4, 0xa0, 0xe5, 0xf4, 0xec, 0x0a]) , - fe([0x07, 0x68, 0x21, 0x65, 0xe9, 0x08, 0xa0, 0x0b, 0x6a, 0x4a, 0xba, 0xb5, 0x80, 0xaf, 0xd0, 0x1b, - 0xc5, 0xf5, 0x4b, 0x73, 0x50, 0x60, 0x2d, 0x71, 0x69, 0x61, 0x0e, 0xc0, 0x20, 0x40, 0x30, 0x19])) + aff( + fe([ + 0x2c, 0x3a, 0xe5, 0xad, 0xf4, 0xdd, 0x2d, 0xf7, 0x5c, 0x44, 0xb5, 0x5b, 0x21, 0xa3, 0x89, 0x5f, + 0x96, 0x45, 0xca, 0x4d, 0xa4, 0x21, 0x99, 0x70, 0xda, 0xc4, 0xc4, 0xa0, 0xe5, 0xf4, 0xec, 0x0a + ]), + fe([ + 0x07, 0x68, 0x21, 0x65, 0xe9, 0x08, 0xa0, 0x0b, 0x6a, 0x4a, 0xba, 0xb5, 0x80, 0xaf, 0xd0, 0x1b, + 0xc5, 0xf5, 0x4b, 0x73, 0x50, 0x60, 0x2d, 0x71, 0x69, 0x61, 0x0e, 0xc0, 0x20, 0x40, 0x30, 0x19 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xd0, 0x75, 0x57, 0x3b, 0xeb, 0x5c, 0x14, 0x56, 0x50, 0xc9, 0x4f, 0xb8, 0xb8, 0x1e, 0xa3, 0xf4, - 0xab, 0xf5, 0xa9, 0x20, 0x15, 0x94, 0x82, 0xda, 0x96, 0x1c, 0x9b, 0x59, 0x8c, 0xff, 0xf4, 0x51]) , - fe([0xc1, 0x3a, 0x86, 0xd7, 0xb0, 0x06, 0x84, 0x7f, 0x1b, 0xbd, 0xd4, 0x07, 0x78, 0x80, 0x2e, 0xb1, - 0xb4, 0xee, 0x52, 0x38, 0xee, 0x9a, 0xf9, 0xf6, 0xf3, 0x41, 0x6e, 0xd4, 0x88, 0x95, 0xac, 0x35])) + aff( + fe([ + 0xd0, 0x75, 0x57, 0x3b, 0xeb, 0x5c, 0x14, 0x56, 0x50, 0xc9, 0x4f, 0xb8, 0xb8, 0x1e, 0xa3, 0xf4, + 0xab, 0xf5, 0xa9, 0x20, 0x15, 0x94, 0x82, 0xda, 0x96, 0x1c, 0x9b, 0x59, 0x8c, 0xff, 0xf4, 0x51 + ]), + fe([ + 0xc1, 0x3a, 0x86, 0xd7, 0xb0, 0x06, 0x84, 0x7f, 0x1b, 0xbd, 0xd4, 0x07, 0x78, 0x80, 0x2e, 0xb1, + 0xb4, 0xee, 0x52, 0x38, 0xee, 0x9a, 0xf9, 0xf6, 0xf3, 0x41, 0x6e, 0xd4, 0x88, 0x95, 0xac, 0x35 + ]) + ) ) v.append( - aff(fe([0x41, 0x97, 0xbf, 0x71, 0x6a, 0x9b, 0x72, 0xec, 0xf3, 0xf8, 0x6b, 0xe6, 0x0e, 0x6c, 0x69, 0xa5, - 0x2f, 0x68, 0x52, 0xd8, 0x61, 0x81, 0xc0, 0x63, 0x3f, 0xa6, 0x3c, 0x13, 0x90, 0xe6, 0x8d, 0x56]) , - fe([0xe8, 0x39, 0x30, 0x77, 0x23, 0xb1, 0xfd, 0x1b, 0x3d, 0x3e, 0x74, 0x4d, 0x7f, 0xae, 0x5b, 0x3a, - 0xb4, 0x65, 0x0e, 0x3a, 0x43, 0xdc, 0xdc, 0x41, 0x47, 0xe6, 0xe8, 0x92, 0x09, 0x22, 0x48, 0x4c])) + aff( + fe([ + 0x41, 0x97, 0xbf, 0x71, 0x6a, 0x9b, 0x72, 0xec, 0xf3, 0xf8, 0x6b, 0xe6, 0x0e, 0x6c, 0x69, 0xa5, + 0x2f, 0x68, 0x52, 0xd8, 0x61, 0x81, 0xc0, 0x63, 0x3f, 0xa6, 0x3c, 0x13, 0x90, 0xe6, 0x8d, 0x56 + ]), + fe([ + 0xe8, 0x39, 0x30, 0x77, 0x23, 0xb1, 0xfd, 0x1b, 0x3d, 0x3e, 0x74, 0x4d, 0x7f, 0xae, 0x5b, 0x3a, + 0xb4, 0x65, 0x0e, 0x3a, 0x43, 0xdc, 0xdc, 0x41, 0x47, 0xe6, 0xe8, 0x92, 0x09, 0x22, 0x48, 0x4c + ]) + ) ) v.append( - aff(fe([0x85, 0x57, 0x9f, 0xb5, 0xc8, 0x06, 0xb2, 0x9f, 0x47, 0x3f, 0xf0, 0xfa, 0xe6, 0xa9, 0xb1, 0x9b, - 0x6f, 0x96, 0x7d, 0xf9, 0xa4, 0x65, 0x09, 0x75, 0x32, 0xa6, 0x6c, 0x7f, 0x47, 0x4b, 0x2f, 0x4f]) , - fe([0x34, 0xe9, 0x59, 0x93, 0x9d, 0x26, 0x80, 0x54, 0xf2, 0xcc, 0x3c, 0xc2, 0x25, 0x85, 0xe3, 0x6a, - 0xc1, 0x62, 0x04, 0xa7, 0x08, 0x32, 0x6d, 0xa1, 0x39, 0x84, 0x8a, 0x3b, 0x87, 0x5f, 0x11, 0x13])) + aff( + fe([ + 0x85, 0x57, 0x9f, 0xb5, 0xc8, 0x06, 0xb2, 0x9f, 0x47, 0x3f, 0xf0, 0xfa, 0xe6, 0xa9, 0xb1, 0x9b, + 0x6f, 0x96, 0x7d, 0xf9, 0xa4, 0x65, 0x09, 0x75, 0x32, 0xa6, 0x6c, 0x7f, 0x47, 0x4b, 0x2f, 0x4f + ]), + fe([ + 0x34, 0xe9, 0x59, 0x93, 0x9d, 0x26, 0x80, 0x54, 0xf2, 0xcc, 0x3c, 0xc2, 0x25, 0x85, 0xe3, 0x6a, + 0xc1, 0x62, 0x04, 0xa7, 0x08, 0x32, 0x6d, 0xa1, 0x39, 0x84, 0x8a, 0x3b, 0x87, 0x5f, 0x11, 0x13 + ]) + ) ) v.append( - aff(fe([0xda, 0x03, 0x34, 0x66, 0xc4, 0x0c, 0x73, 0x6e, 0xbc, 0x24, 0xb5, 0xf9, 0x70, 0x81, 0x52, 0xe9, - 0xf4, 0x7c, 0x23, 0xdd, 0x9f, 0xb8, 0x46, 0xef, 0x1d, 0x22, 0x55, 0x7d, 0x71, 0xc4, 0x42, 0x33]) , - fe([0xc5, 0x37, 0x69, 0x5b, 0xa8, 0xc6, 0x9d, 0xa4, 0xfc, 0x61, 0x6e, 0x68, 0x46, 0xea, 0xd7, 0x1c, - 0x67, 0xd2, 0x7d, 0xfa, 0xf1, 0xcc, 0x54, 0x8d, 0x36, 0x35, 0xc9, 0x00, 0xdf, 0x6c, 0x67, 0x50])) + aff( + fe([ + 0xda, 0x03, 0x34, 0x66, 0xc4, 0x0c, 0x73, 0x6e, 0xbc, 0x24, 0xb5, 0xf9, 0x70, 0x81, 0x52, 0xe9, + 0xf4, 0x7c, 0x23, 0xdd, 0x9f, 0xb8, 0x46, 0xef, 0x1d, 0x22, 0x55, 0x7d, 0x71, 0xc4, 0x42, 0x33 + ]), + fe([ + 0xc5, 0x37, 0x69, 0x5b, 0xa8, 0xc6, 0x9d, 0xa4, 0xfc, 0x61, 0x6e, 0x68, 0x46, 0xea, 0xd7, 0x1c, + 0x67, 0xd2, 0x7d, 0xfa, 0xf1, 0xcc, 0x54, 0x8d, 0x36, 0x35, 0xc9, 0x00, 0xdf, 0x6c, 0x67, 0x50 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x9a, 0x4d, 0x42, 0x29, 0x5d, 0xa4, 0x6b, 0x6f, 0xa8, 0x8a, 0x4d, 0x91, 0x7b, 0xd2, 0xdf, 0x36, - 0xef, 0x01, 0x22, 0xc5, 0xcc, 0x8d, 0xeb, 0x58, 0x3d, 0xb3, 0x50, 0xfc, 0x8b, 0x97, 0x96, 0x33]) , - fe([0x93, 0x33, 0x07, 0xc8, 0x4a, 0xca, 0xd0, 0xb1, 0xab, 0xbd, 0xdd, 0xa7, 0x7c, 0xac, 0x3e, 0x45, - 0xcb, 0xcc, 0x07, 0x91, 0xbf, 0x35, 0x9d, 0xcb, 0x7d, 0x12, 0x3c, 0x11, 0x59, 0x13, 0xcf, 0x5c])) + aff( + fe([ + 0x9a, 0x4d, 0x42, 0x29, 0x5d, 0xa4, 0x6b, 0x6f, 0xa8, 0x8a, 0x4d, 0x91, 0x7b, 0xd2, 0xdf, 0x36, + 0xef, 0x01, 0x22, 0xc5, 0xcc, 0x8d, 0xeb, 0x58, 0x3d, 0xb3, 0x50, 0xfc, 0x8b, 0x97, 0x96, 0x33 + ]), + fe([ + 0x93, 0x33, 0x07, 0xc8, 0x4a, 0xca, 0xd0, 0xb1, 0xab, 0xbd, 0xdd, 0xa7, 0x7c, 0xac, 0x3e, 0x45, + 0xcb, 0xcc, 0x07, 0x91, 0xbf, 0x35, 0x9d, 0xcb, 0x7d, 0x12, 0x3c, 0x11, 0x59, 0x13, 0xcf, 0x5c + ]) + ) ) v.append( - aff(fe([0x45, 0xb8, 0x41, 0xd7, 0xab, 0x07, 0x15, 0x00, 0x8e, 0xce, 0xdf, 0xb2, 0x43, 0x5c, 0x01, 0xdc, - 0xf4, 0x01, 0x51, 0x95, 0x10, 0x5a, 0xf6, 0x24, 0x24, 0xa0, 0x19, 0x3a, 0x09, 0x2a, 0xaa, 0x3f]) , - fe([0xdc, 0x8e, 0xeb, 0xc6, 0xbf, 0xdd, 0x11, 0x7b, 0xe7, 0x47, 0xe6, 0xce, 0xe7, 0xb6, 0xc5, 0xe8, - 0x8a, 0xdc, 0x4b, 0x57, 0x15, 0x3b, 0x66, 0xca, 0x89, 0xa3, 0xfd, 0xac, 0x0d, 0xe1, 0x1d, 0x7a])) + aff( + fe([ + 0x45, 0xb8, 0x41, 0xd7, 0xab, 0x07, 0x15, 0x00, 0x8e, 0xce, 0xdf, 0xb2, 0x43, 0x5c, 0x01, 0xdc, + 0xf4, 0x01, 0x51, 0x95, 0x10, 0x5a, 0xf6, 0x24, 0x24, 0xa0, 0x19, 0x3a, 0x09, 0x2a, 0xaa, 0x3f + ]), + fe([ + 0xdc, 0x8e, 0xeb, 0xc6, 0xbf, 0xdd, 0x11, 0x7b, 0xe7, 0x47, 0xe6, 0xce, 0xe7, 0xb6, 0xc5, 0xe8, + 0x8a, 0xdc, 0x4b, 0x57, 0x15, 0x3b, 0x66, 0xca, 0x89, 0xa3, 0xfd, 0xac, 0x0d, 0xe1, 0x1d, 0x7a + ]) + ) ) v.append( - aff(fe([0x89, 0xef, 0xbf, 0x03, 0x75, 0xd0, 0x29, 0x50, 0xcb, 0x7d, 0xd6, 0xbe, 0xad, 0x5f, 0x7b, 0x00, - 0x32, 0xaa, 0x98, 0xed, 0x3f, 0x8f, 0x92, 0xcb, 0x81, 0x56, 0x01, 0x63, 0x64, 0xa3, 0x38, 0x39]) , - fe([0x8b, 0xa4, 0xd6, 0x50, 0xb4, 0xaa, 0x5d, 0x64, 0x64, 0x76, 0x2e, 0xa1, 0xa6, 0xb3, 0xb8, 0x7c, - 0x7a, 0x56, 0xf5, 0x5c, 0x4e, 0x84, 0x5c, 0xfb, 0xdd, 0xca, 0x48, 0x8b, 0x48, 0xb9, 0xba, 0x34])) + aff( + fe([ + 0x89, 0xef, 0xbf, 0x03, 0x75, 0xd0, 0x29, 0x50, 0xcb, 0x7d, 0xd6, 0xbe, 0xad, 0x5f, 0x7b, 0x00, + 0x32, 0xaa, 0x98, 0xed, 0x3f, 0x8f, 0x92, 0xcb, 0x81, 0x56, 0x01, 0x63, 0x64, 0xa3, 0x38, 0x39 + ]), + fe([ + 0x8b, 0xa4, 0xd6, 0x50, 0xb4, 0xaa, 0x5d, 0x64, 0x64, 0x76, 0x2e, 0xa1, 0xa6, 0xb3, 0xb8, 0x7c, + 0x7a, 0x56, 0xf5, 0x5c, 0x4e, 0x84, 0x5c, 0xfb, 0xdd, 0xca, 0x48, 0x8b, 0x48, 0xb9, 0xba, 0x34 + ]) + ) ) v.append( - aff(fe([0xc5, 0xe3, 0xe8, 0xae, 0x17, 0x27, 0xe3, 0x64, 0x60, 0x71, 0x47, 0x29, 0x02, 0x0f, 0x92, 0x5d, - 0x10, 0x93, 0xc8, 0x0e, 0xa1, 0xed, 0xba, 0xa9, 0x96, 0x1c, 0xc5, 0x76, 0x30, 0xcd, 0xf9, 0x30]) , - fe([0x95, 0xb0, 0xbd, 0x8c, 0xbc, 0xa7, 0x4f, 0x7e, 0xfd, 0x4e, 0x3a, 0xbf, 0x5f, 0x04, 0x79, 0x80, - 0x2b, 0x5a, 0x9f, 0x4f, 0x68, 0x21, 0x19, 0x71, 0xc6, 0x20, 0x01, 0x42, 0xaa, 0xdf, 0xae, 0x2c])) + aff( + fe([ + 0xc5, 0xe3, 0xe8, 0xae, 0x17, 0x27, 0xe3, 0x64, 0x60, 0x71, 0x47, 0x29, 0x02, 0x0f, 0x92, 0x5d, + 0x10, 0x93, 0xc8, 0x0e, 0xa1, 0xed, 0xba, 0xa9, 0x96, 0x1c, 0xc5, 0x76, 0x30, 0xcd, 0xf9, 0x30 + ]), + fe([ + 0x95, 0xb0, 0xbd, 0x8c, 0xbc, 0xa7, 0x4f, 0x7e, 0xfd, 0x4e, 0x3a, 0xbf, 0x5f, 0x04, 0x79, 0x80, + 0x2b, 0x5a, 0x9f, 0x4f, 0x68, 0x21, 0x19, 0x71, 0xc6, 0x20, 0x01, 0x42, 0xaa, 0xdf, 0xae, 0x2c + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x90, 0x6e, 0x7e, 0x4b, 0x71, 0x93, 0xc0, 0x72, 0xed, 0xeb, 0x71, 0x24, 0x97, 0x26, 0x9c, 0xfe, - 0xcb, 0x3e, 0x59, 0x19, 0xa8, 0x0f, 0x75, 0x7d, 0xbe, 0x18, 0xe6, 0x96, 0x1e, 0x95, 0x70, 0x60]) , - fe([0x89, 0x66, 0x3e, 0x1d, 0x4c, 0x5f, 0xfe, 0xc0, 0x04, 0x43, 0xd6, 0x44, 0x19, 0xb5, 0xad, 0xc7, - 0x22, 0xdc, 0x71, 0x28, 0x64, 0xde, 0x41, 0x38, 0x27, 0x8f, 0x2c, 0x6b, 0x08, 0xb8, 0xb8, 0x7b])) + aff( + fe([ + 0x90, 0x6e, 0x7e, 0x4b, 0x71, 0x93, 0xc0, 0x72, 0xed, 0xeb, 0x71, 0x24, 0x97, 0x26, 0x9c, 0xfe, + 0xcb, 0x3e, 0x59, 0x19, 0xa8, 0x0f, 0x75, 0x7d, 0xbe, 0x18, 0xe6, 0x96, 0x1e, 0x95, 0x70, 0x60 + ]), + fe([ + 0x89, 0x66, 0x3e, 0x1d, 0x4c, 0x5f, 0xfe, 0xc0, 0x04, 0x43, 0xd6, 0x44, 0x19, 0xb5, 0xad, 0xc7, + 0x22, 0xdc, 0x71, 0x28, 0x64, 0xde, 0x41, 0x38, 0x27, 0x8f, 0x2c, 0x6b, 0x08, 0xb8, 0xb8, 0x7b + ]) + ) ) v.append( - aff(fe([0x3d, 0x70, 0x27, 0x9d, 0xd9, 0xaf, 0xb1, 0x27, 0xaf, 0xe3, 0x5d, 0x1e, 0x3a, 0x30, 0x54, 0x61, - 0x60, 0xe8, 0xc3, 0x26, 0x3a, 0xbc, 0x7e, 0xf5, 0x81, 0xdd, 0x64, 0x01, 0x04, 0xeb, 0xc0, 0x1e]) , - fe([0xda, 0x2c, 0xa4, 0xd1, 0xa1, 0xc3, 0x5c, 0x6e, 0x32, 0x07, 0x1f, 0xb8, 0x0e, 0x19, 0x9e, 0x99, - 0x29, 0x33, 0x9a, 0xae, 0x7a, 0xed, 0x68, 0x42, 0x69, 0x7c, 0x07, 0xb3, 0x38, 0x2c, 0xf6, 0x3d])) + aff( + fe([ + 0x3d, 0x70, 0x27, 0x9d, 0xd9, 0xaf, 0xb1, 0x27, 0xaf, 0xe3, 0x5d, 0x1e, 0x3a, 0x30, 0x54, 0x61, + 0x60, 0xe8, 0xc3, 0x26, 0x3a, 0xbc, 0x7e, 0xf5, 0x81, 0xdd, 0x64, 0x01, 0x04, 0xeb, 0xc0, 0x1e + ]), + fe([ + 0xda, 0x2c, 0xa4, 0xd1, 0xa1, 0xc3, 0x5c, 0x6e, 0x32, 0x07, 0x1f, 0xb8, 0x0e, 0x19, 0x9e, 0x99, + 0x29, 0x33, 0x9a, 0xae, 0x7a, 0xed, 0x68, 0x42, 0x69, 0x7c, 0x07, 0xb3, 0x38, 0x2c, 0xf6, 0x3d + ]) + ) ) v.append( - aff(fe([0x64, 0xaa, 0xb5, 0x88, 0x79, 0x65, 0x38, 0x8c, 0x94, 0xd6, 0x62, 0x37, 0x7d, 0x64, 0xcd, 0x3a, - 0xeb, 0xff, 0xe8, 0x81, 0x09, 0xc7, 0x6a, 0x50, 0x09, 0x0d, 0x28, 0x03, 0x0d, 0x9a, 0x93, 0x0a]) , - fe([0x42, 0xa3, 0xf1, 0xc5, 0xb4, 0x0f, 0xd8, 0xc8, 0x8d, 0x15, 0x31, 0xbd, 0xf8, 0x07, 0x8b, 0xcd, - 0x08, 0x8a, 0xfb, 0x18, 0x07, 0xfe, 0x8e, 0x52, 0x86, 0xef, 0xbe, 0xec, 0x49, 0x52, 0x99, 0x08])) + aff( + fe([ + 0x64, 0xaa, 0xb5, 0x88, 0x79, 0x65, 0x38, 0x8c, 0x94, 0xd6, 0x62, 0x37, 0x7d, 0x64, 0xcd, 0x3a, + 0xeb, 0xff, 0xe8, 0x81, 0x09, 0xc7, 0x6a, 0x50, 0x09, 0x0d, 0x28, 0x03, 0x0d, 0x9a, 0x93, 0x0a + ]), + fe([ + 0x42, 0xa3, 0xf1, 0xc5, 0xb4, 0x0f, 0xd8, 0xc8, 0x8d, 0x15, 0x31, 0xbd, 0xf8, 0x07, 0x8b, 0xcd, + 0x08, 0x8a, 0xfb, 0x18, 0x07, 0xfe, 0x8e, 0x52, 0x86, 0xef, 0xbe, 0xec, 0x49, 0x52, 0x99, 0x08 + ]) + ) ) v.append( - aff(fe([0x0f, 0xa9, 0xd5, 0x01, 0xaa, 0x48, 0x4f, 0x28, 0x66, 0x32, 0x1a, 0xba, 0x7c, 0xea, 0x11, 0x80, - 0x17, 0x18, 0x9b, 0x56, 0x88, 0x25, 0x06, 0x69, 0x12, 0x2c, 0xea, 0x56, 0x69, 0x41, 0x24, 0x19]) , - fe([0xde, 0x21, 0xf0, 0xda, 0x8a, 0xfb, 0xb1, 0xb8, 0xcd, 0xc8, 0x6a, 0x82, 0x19, 0x73, 0xdb, 0xc7, - 0xcf, 0x88, 0xeb, 0x96, 0xee, 0x6f, 0xfb, 0x06, 0xd2, 0xcd, 0x7d, 0x7b, 0x12, 0x28, 0x8e, 0x0c])) + aff( + fe([ + 0x0f, 0xa9, 0xd5, 0x01, 0xaa, 0x48, 0x4f, 0x28, 0x66, 0x32, 0x1a, 0xba, 0x7c, 0xea, 0x11, 0x80, + 0x17, 0x18, 0x9b, 0x56, 0x88, 0x25, 0x06, 0x69, 0x12, 0x2c, 0xea, 0x56, 0x69, 0x41, 0x24, 0x19 + ]), + fe([ + 0xde, 0x21, 0xf0, 0xda, 0x8a, 0xfb, 0xb1, 0xb8, 0xcd, 0xc8, 0x6a, 0x82, 0x19, 0x73, 0xdb, 0xc7, + 0xcf, 0x88, 0xeb, 0x96, 0xee, 0x6f, 0xfb, 0x06, 0xd2, 0xcd, 0x7d, 0x7b, 0x12, 0x28, 0x8e, 0x0c + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x93, 0x44, 0x97, 0xce, 0x28, 0xff, 0x3a, 0x40, 0xc4, 0xf5, 0xf6, 0x9b, 0xf4, 0x6b, 0x07, 0x84, - 0xfb, 0x98, 0xd8, 0xec, 0x8c, 0x03, 0x57, 0xec, 0x49, 0xed, 0x63, 0xb6, 0xaa, 0xff, 0x98, 0x28]) , - fe([0x3d, 0x16, 0x35, 0xf3, 0x46, 0xbc, 0xb3, 0xf4, 0xc6, 0xb6, 0x4f, 0xfa, 0xf4, 0xa0, 0x13, 0xe6, - 0x57, 0x45, 0x93, 0xb9, 0xbc, 0xd6, 0x59, 0xe7, 0x77, 0x94, 0x6c, 0xab, 0x96, 0x3b, 0x4f, 0x09])) + aff( + fe([ + 0x93, 0x44, 0x97, 0xce, 0x28, 0xff, 0x3a, 0x40, 0xc4, 0xf5, 0xf6, 0x9b, 0xf4, 0x6b, 0x07, 0x84, + 0xfb, 0x98, 0xd8, 0xec, 0x8c, 0x03, 0x57, 0xec, 0x49, 0xed, 0x63, 0xb6, 0xaa, 0xff, 0x98, 0x28 + ]), + fe([ + 0x3d, 0x16, 0x35, 0xf3, 0x46, 0xbc, 0xb3, 0xf4, 0xc6, 0xb6, 0x4f, 0xfa, 0xf4, 0xa0, 0x13, 0xe6, + 0x57, 0x45, 0x93, 0xb9, 0xbc, 0xd6, 0x59, 0xe7, 0x77, 0x94, 0x6c, 0xab, 0x96, 0x3b, 0x4f, 0x09 + ]) + ) ) v.append( - aff(fe([0x5a, 0xf7, 0x6b, 0x01, 0x12, 0x4f, 0x51, 0xc1, 0x70, 0x84, 0x94, 0x47, 0xb2, 0x01, 0x6c, 0x71, - 0xd7, 0xcc, 0x17, 0x66, 0x0f, 0x59, 0x5d, 0x5d, 0x10, 0x01, 0x57, 0x11, 0xf5, 0xdd, 0xe2, 0x34]) , - fe([0x26, 0xd9, 0x1f, 0x5c, 0x58, 0xac, 0x8b, 0x03, 0xd2, 0xc3, 0x85, 0x0f, 0x3a, 0xc3, 0x7f, 0x6d, - 0x8e, 0x86, 0xcd, 0x52, 0x74, 0x8f, 0x55, 0x77, 0x17, 0xb7, 0x8e, 0xb7, 0x88, 0xea, 0xda, 0x1b])) + aff( + fe([ + 0x5a, 0xf7, 0x6b, 0x01, 0x12, 0x4f, 0x51, 0xc1, 0x70, 0x84, 0x94, 0x47, 0xb2, 0x01, 0x6c, 0x71, + 0xd7, 0xcc, 0x17, 0x66, 0x0f, 0x59, 0x5d, 0x5d, 0x10, 0x01, 0x57, 0x11, 0xf5, 0xdd, 0xe2, 0x34 + ]), + fe([ + 0x26, 0xd9, 0x1f, 0x5c, 0x58, 0xac, 0x8b, 0x03, 0xd2, 0xc3, 0x85, 0x0f, 0x3a, 0xc3, 0x7f, 0x6d, + 0x8e, 0x86, 0xcd, 0x52, 0x74, 0x8f, 0x55, 0x77, 0x17, 0xb7, 0x8e, 0xb7, 0x88, 0xea, 0xda, 0x1b + ]) + ) ) v.append( - aff(fe([0xb6, 0xea, 0x0e, 0x40, 0x93, 0x20, 0x79, 0x35, 0x6a, 0x61, 0x84, 0x5a, 0x07, 0x6d, 0xf9, 0x77, - 0x6f, 0xed, 0x69, 0x1c, 0x0d, 0x25, 0x76, 0xcc, 0xf0, 0xdb, 0xbb, 0xc5, 0xad, 0xe2, 0x26, 0x57]) , - fe([0xcf, 0xe8, 0x0e, 0x6b, 0x96, 0x7d, 0xed, 0x27, 0xd1, 0x3c, 0xa9, 0xd9, 0x50, 0xa9, 0x98, 0x84, - 0x5e, 0x86, 0xef, 0xd6, 0xf0, 0xf8, 0x0e, 0x89, 0x05, 0x2f, 0xd9, 0x5f, 0x15, 0x5f, 0x73, 0x79])) + aff( + fe([ + 0xb6, 0xea, 0x0e, 0x40, 0x93, 0x20, 0x79, 0x35, 0x6a, 0x61, 0x84, 0x5a, 0x07, 0x6d, 0xf9, 0x77, + 0x6f, 0xed, 0x69, 0x1c, 0x0d, 0x25, 0x76, 0xcc, 0xf0, 0xdb, 0xbb, 0xc5, 0xad, 0xe2, 0x26, 0x57 + ]), + fe([ + 0xcf, 0xe8, 0x0e, 0x6b, 0x96, 0x7d, 0xed, 0x27, 0xd1, 0x3c, 0xa9, 0xd9, 0x50, 0xa9, 0x98, 0x84, + 0x5e, 0x86, 0xef, 0xd6, 0xf0, 0xf8, 0x0e, 0x89, 0x05, 0x2f, 0xd9, 0x5f, 0x15, 0x5f, 0x73, 0x79 + ]) + ) ) v.append( - aff(fe([0xc8, 0x5c, 0x16, 0xfe, 0xed, 0x9f, 0x26, 0x56, 0xf6, 0x4b, 0x9f, 0xa7, 0x0a, 0x85, 0xfe, 0xa5, - 0x8c, 0x87, 0xdd, 0x98, 0xce, 0x4e, 0xc3, 0x58, 0x55, 0xb2, 0x7b, 0x3d, 0xd8, 0x6b, 0xb5, 0x4c]) , - fe([0x65, 0x38, 0xa0, 0x15, 0xfa, 0xa7, 0xb4, 0x8f, 0xeb, 0xc4, 0x86, 0x9b, 0x30, 0xa5, 0x5e, 0x4d, - 0xea, 0x8a, 0x9a, 0x9f, 0x1a, 0xd8, 0x5b, 0x53, 0x14, 0x19, 0x25, 0x63, 0xb4, 0x6f, 0x1f, 0x5d])) + aff( + fe([ + 0xc8, 0x5c, 0x16, 0xfe, 0xed, 0x9f, 0x26, 0x56, 0xf6, 0x4b, 0x9f, 0xa7, 0x0a, 0x85, 0xfe, 0xa5, + 0x8c, 0x87, 0xdd, 0x98, 0xce, 0x4e, 0xc3, 0x58, 0x55, 0xb2, 0x7b, 0x3d, 0xd8, 0x6b, 0xb5, 0x4c + ]), + fe([ + 0x65, 0x38, 0xa0, 0x15, 0xfa, 0xa7, 0xb4, 0x8f, 0xeb, 0xc4, 0x86, 0x9b, 0x30, 0xa5, 0x5e, 0x4d, + 0xea, 0x8a, 0x9a, 0x9f, 0x1a, 0xd8, 0x5b, 0x53, 0x14, 0x19, 0x25, 0x63, 0xb4, 0x6f, 0x1f, 0x5d + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xac, 0x8f, 0xbc, 0x1e, 0x7d, 0x8b, 0x5a, 0x0b, 0x8d, 0xaf, 0x76, 0x2e, 0x71, 0xe3, 0x3b, 0x6f, - 0x53, 0x2f, 0x3e, 0x90, 0x95, 0xd4, 0x35, 0x14, 0x4f, 0x8c, 0x3c, 0xce, 0x57, 0x1c, 0x76, 0x49]) , - fe([0xa8, 0x50, 0xe1, 0x61, 0x6b, 0x57, 0x35, 0xeb, 0x44, 0x0b, 0x0c, 0x6e, 0xf9, 0x25, 0x80, 0x74, - 0xf2, 0x8f, 0x6f, 0x7a, 0x3e, 0x7f, 0x2d, 0xf3, 0x4e, 0x09, 0x65, 0x10, 0x5e, 0x03, 0x25, 0x32])) + aff( + fe([ + 0xac, 0x8f, 0xbc, 0x1e, 0x7d, 0x8b, 0x5a, 0x0b, 0x8d, 0xaf, 0x76, 0x2e, 0x71, 0xe3, 0x3b, 0x6f, + 0x53, 0x2f, 0x3e, 0x90, 0x95, 0xd4, 0x35, 0x14, 0x4f, 0x8c, 0x3c, 0xce, 0x57, 0x1c, 0x76, 0x49 + ]), + fe([ + 0xa8, 0x50, 0xe1, 0x61, 0x6b, 0x57, 0x35, 0xeb, 0x44, 0x0b, 0x0c, 0x6e, 0xf9, 0x25, 0x80, 0x74, + 0xf2, 0x8f, 0x6f, 0x7a, 0x3e, 0x7f, 0x2d, 0xf3, 0x4e, 0x09, 0x65, 0x10, 0x5e, 0x03, 0x25, 0x32 + ]) + ) ) v.append( - aff(fe([0xa9, 0x60, 0xdc, 0x0f, 0x64, 0xe5, 0x1d, 0xe2, 0x8d, 0x4f, 0x79, 0x2f, 0x0e, 0x24, 0x02, 0x00, - 0x05, 0x77, 0x43, 0x25, 0x3d, 0x6a, 0xc7, 0xb7, 0xbf, 0x04, 0x08, 0x65, 0xf4, 0x39, 0x4b, 0x65]) , - fe([0x96, 0x19, 0x12, 0x6b, 0x6a, 0xb7, 0xe3, 0xdc, 0x45, 0x9b, 0xdb, 0xb4, 0xa8, 0xae, 0xdc, 0xa8, - 0x14, 0x44, 0x65, 0x62, 0xce, 0x34, 0x9a, 0x84, 0x18, 0x12, 0x01, 0xf1, 0xe2, 0x7b, 0xce, 0x50])) + aff( + fe([ + 0xa9, 0x60, 0xdc, 0x0f, 0x64, 0xe5, 0x1d, 0xe2, 0x8d, 0x4f, 0x79, 0x2f, 0x0e, 0x24, 0x02, 0x00, + 0x05, 0x77, 0x43, 0x25, 0x3d, 0x6a, 0xc7, 0xb7, 0xbf, 0x04, 0x08, 0x65, 0xf4, 0x39, 0x4b, 0x65 + ]), + fe([ + 0x96, 0x19, 0x12, 0x6b, 0x6a, 0xb7, 0xe3, 0xdc, 0x45, 0x9b, 0xdb, 0xb4, 0xa8, 0xae, 0xdc, 0xa8, + 0x14, 0x44, 0x65, 0x62, 0xce, 0x34, 0x9a, 0x84, 0x18, 0x12, 0x01, 0xf1, 0xe2, 0x7b, 0xce, 0x50 + ]) + ) ) v.append( - aff(fe([0x41, 0x21, 0x30, 0x53, 0x1b, 0x47, 0x01, 0xb7, 0x18, 0xd8, 0x82, 0x57, 0xbd, 0xa3, 0x60, 0xf0, - 0x32, 0xf6, 0x5b, 0xf0, 0x30, 0x88, 0x91, 0x59, 0xfd, 0x90, 0xa2, 0xb9, 0x55, 0x93, 0x21, 0x34]) , - fe([0x97, 0x67, 0x9e, 0xeb, 0x6a, 0xf9, 0x6e, 0xd6, 0x73, 0xe8, 0x6b, 0x29, 0xec, 0x63, 0x82, 0x00, - 0xa8, 0x99, 0x1c, 0x1d, 0x30, 0xc8, 0x90, 0x52, 0x90, 0xb6, 0x6a, 0x80, 0x4e, 0xff, 0x4b, 0x51])) + aff( + fe([ + 0x41, 0x21, 0x30, 0x53, 0x1b, 0x47, 0x01, 0xb7, 0x18, 0xd8, 0x82, 0x57, 0xbd, 0xa3, 0x60, 0xf0, + 0x32, 0xf6, 0x5b, 0xf0, 0x30, 0x88, 0x91, 0x59, 0xfd, 0x90, 0xa2, 0xb9, 0x55, 0x93, 0x21, 0x34 + ]), + fe([ + 0x97, 0x67, 0x9e, 0xeb, 0x6a, 0xf9, 0x6e, 0xd6, 0x73, 0xe8, 0x6b, 0x29, 0xec, 0x63, 0x82, 0x00, + 0xa8, 0x99, 0x1c, 0x1d, 0x30, 0xc8, 0x90, 0x52, 0x90, 0xb6, 0x6a, 0x80, 0x4e, 0xff, 0x4b, 0x51 + ]) + ) ) v.append( - aff(fe([0x0f, 0x7d, 0x63, 0x8c, 0x6e, 0x5c, 0xde, 0x30, 0xdf, 0x65, 0xfa, 0x2e, 0xb0, 0xa3, 0x25, 0x05, - 0x54, 0xbd, 0x25, 0xba, 0x06, 0xae, 0xdf, 0x8b, 0xd9, 0x1b, 0xea, 0x38, 0xb3, 0x05, 0x16, 0x09]) , - fe([0xc7, 0x8c, 0xbf, 0x64, 0x28, 0xad, 0xf8, 0xa5, 0x5a, 0x6f, 0xc9, 0xba, 0xd5, 0x7f, 0xd5, 0xd6, - 0xbd, 0x66, 0x2f, 0x3d, 0xaa, 0x54, 0xf6, 0xba, 0x32, 0x22, 0x9a, 0x1e, 0x52, 0x05, 0xf4, 0x1d])) + aff( + fe([ + 0x0f, 0x7d, 0x63, 0x8c, 0x6e, 0x5c, 0xde, 0x30, 0xdf, 0x65, 0xfa, 0x2e, 0xb0, 0xa3, 0x25, 0x05, + 0x54, 0xbd, 0x25, 0xba, 0x06, 0xae, 0xdf, 0x8b, 0xd9, 0x1b, 0xea, 0x38, 0xb3, 0x05, 0x16, 0x09 + ]), + fe([ + 0xc7, 0x8c, 0xbf, 0x64, 0x28, 0xad, 0xf8, 0xa5, 0x5a, 0x6f, 0xc9, 0xba, 0xd5, 0x7f, 0xd5, 0xd6, + 0xbd, 0x66, 0x2f, 0x3d, 0xaa, 0x54, 0xf6, 0xba, 0x32, 0x22, 0x9a, 0x1e, 0x52, 0x05, 0xf4, 0x1d + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xaa, 0x1f, 0xbb, 0xeb, 0xfe, 0xe4, 0x87, 0xfc, 0xb1, 0x2c, 0xb7, 0x88, 0xf4, 0xc6, 0xb9, 0xf5, - 0x24, 0x46, 0xf2, 0xa5, 0x9f, 0x8f, 0x8a, 0x93, 0x70, 0x69, 0xd4, 0x56, 0xec, 0xfd, 0x06, 0x46]) , - fe([0x4e, 0x66, 0xcf, 0x4e, 0x34, 0xce, 0x0c, 0xd9, 0xa6, 0x50, 0xd6, 0x5e, 0x95, 0xaf, 0xe9, 0x58, - 0xfa, 0xee, 0x9b, 0xb8, 0xa5, 0x0f, 0x35, 0xe0, 0x43, 0x82, 0x6d, 0x65, 0xe6, 0xd9, 0x00, 0x0f])) + aff( + fe([ + 0xaa, 0x1f, 0xbb, 0xeb, 0xfe, 0xe4, 0x87, 0xfc, 0xb1, 0x2c, 0xb7, 0x88, 0xf4, 0xc6, 0xb9, 0xf5, + 0x24, 0x46, 0xf2, 0xa5, 0x9f, 0x8f, 0x8a, 0x93, 0x70, 0x69, 0xd4, 0x56, 0xec, 0xfd, 0x06, 0x46 + ]), + fe([ + 0x4e, 0x66, 0xcf, 0x4e, 0x34, 0xce, 0x0c, 0xd9, 0xa6, 0x50, 0xd6, 0x5e, 0x95, 0xaf, 0xe9, 0x58, + 0xfa, 0xee, 0x9b, 0xb8, 0xa5, 0x0f, 0x35, 0xe0, 0x43, 0x82, 0x6d, 0x65, 0xe6, 0xd9, 0x00, 0x0f + ]) + ) ) v.append( - aff(fe([0x7b, 0x75, 0x3a, 0xfc, 0x64, 0xd3, 0x29, 0x7e, 0xdd, 0x49, 0x9a, 0x59, 0x53, 0xbf, 0xb4, 0xa7, - 0x52, 0xb3, 0x05, 0xab, 0xc3, 0xaf, 0x16, 0x1a, 0x85, 0x42, 0x32, 0xa2, 0x86, 0xfa, 0x39, 0x43]) , - fe([0x0e, 0x4b, 0xa3, 0x63, 0x8a, 0xfe, 0xa5, 0x58, 0xf1, 0x13, 0xbd, 0x9d, 0xaa, 0x7f, 0x76, 0x40, - 0x70, 0x81, 0x10, 0x75, 0x99, 0xbb, 0xbe, 0x0b, 0x16, 0xe9, 0xba, 0x62, 0x34, 0xcc, 0x07, 0x6d])) + aff( + fe([ + 0x7b, 0x75, 0x3a, 0xfc, 0x64, 0xd3, 0x29, 0x7e, 0xdd, 0x49, 0x9a, 0x59, 0x53, 0xbf, 0xb4, 0xa7, + 0x52, 0xb3, 0x05, 0xab, 0xc3, 0xaf, 0x16, 0x1a, 0x85, 0x42, 0x32, 0xa2, 0x86, 0xfa, 0x39, 0x43 + ]), + fe([ + 0x0e, 0x4b, 0xa3, 0x63, 0x8a, 0xfe, 0xa5, 0x58, 0xf1, 0x13, 0xbd, 0x9d, 0xaa, 0x7f, 0x76, 0x40, + 0x70, 0x81, 0x10, 0x75, 0x99, 0xbb, 0xbe, 0x0b, 0x16, 0xe9, 0xba, 0x62, 0x34, 0xcc, 0x07, 0x6d + ]) + ) ) v.append( - aff(fe([0xc3, 0xf1, 0xc6, 0x93, 0x65, 0xee, 0x0b, 0xbc, 0xea, 0x14, 0xf0, 0xc1, 0xf8, 0x84, 0x89, 0xc2, - 0xc9, 0xd7, 0xea, 0x34, 0xca, 0xa7, 0xc4, 0x99, 0xd5, 0x50, 0x69, 0xcb, 0xd6, 0x21, 0x63, 0x7c]) , - fe([0x99, 0xeb, 0x7c, 0x31, 0x73, 0x64, 0x67, 0x7f, 0x0c, 0x66, 0xaa, 0x8c, 0x69, 0x91, 0xe2, 0x26, - 0xd3, 0x23, 0xe2, 0x76, 0x5d, 0x32, 0x52, 0xdf, 0x5d, 0xc5, 0x8f, 0xb7, 0x7c, 0x84, 0xb3, 0x70])) + aff( + fe([ + 0xc3, 0xf1, 0xc6, 0x93, 0x65, 0xee, 0x0b, 0xbc, 0xea, 0x14, 0xf0, 0xc1, 0xf8, 0x84, 0x89, 0xc2, + 0xc9, 0xd7, 0xea, 0x34, 0xca, 0xa7, 0xc4, 0x99, 0xd5, 0x50, 0x69, 0xcb, 0xd6, 0x21, 0x63, 0x7c + ]), + fe([ + 0x99, 0xeb, 0x7c, 0x31, 0x73, 0x64, 0x67, 0x7f, 0x0c, 0x66, 0xaa, 0x8c, 0x69, 0x91, 0xe2, 0x26, + 0xd3, 0x23, 0xe2, 0x76, 0x5d, 0x32, 0x52, 0xdf, 0x5d, 0xc5, 0x8f, 0xb7, 0x7c, 0x84, 0xb3, 0x70 + ]) + ) ) v.append( - aff(fe([0xeb, 0x01, 0xc7, 0x36, 0x97, 0x4e, 0xb6, 0xab, 0x5f, 0x0d, 0x2c, 0xba, 0x67, 0x64, 0x55, 0xde, - 0xbc, 0xff, 0xa6, 0xec, 0x04, 0xd3, 0x8d, 0x39, 0x56, 0x5e, 0xee, 0xf8, 0xe4, 0x2e, 0x33, 0x62]) , - fe([0x65, 0xef, 0xb8, 0x9f, 0xc8, 0x4b, 0xa7, 0xfd, 0x21, 0x49, 0x9b, 0x92, 0x35, 0x82, 0xd6, 0x0a, - 0x9b, 0xf2, 0x79, 0xf1, 0x47, 0x2f, 0x6a, 0x7e, 0x9f, 0xcf, 0x18, 0x02, 0x3c, 0xfb, 0x1b, 0x3e])) + aff( + fe([ + 0xeb, 0x01, 0xc7, 0x36, 0x97, 0x4e, 0xb6, 0xab, 0x5f, 0x0d, 0x2c, 0xba, 0x67, 0x64, 0x55, 0xde, + 0xbc, 0xff, 0xa6, 0xec, 0x04, 0xd3, 0x8d, 0x39, 0x56, 0x5e, 0xee, 0xf8, 0xe4, 0x2e, 0x33, 0x62 + ]), + fe([ + 0x65, 0xef, 0xb8, 0x9f, 0xc8, 0x4b, 0xa7, 0xfd, 0x21, 0x49, 0x9b, 0x92, 0x35, 0x82, 0xd6, 0x0a, + 0x9b, 0xf2, 0x79, 0xf1, 0x47, 0x2f, 0x6a, 0x7e, 0x9f, 0xcf, 0x18, 0x02, 0x3c, 0xfb, 0x1b, 0x3e + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x2f, 0x8b, 0xc8, 0x40, 0x51, 0xd1, 0xac, 0x1a, 0x0b, 0xe4, 0xa9, 0xa2, 0x42, 0x21, 0x19, 0x2f, - 0x7b, 0x97, 0xbf, 0xf7, 0x57, 0x6d, 0x3f, 0x3d, 0x4f, 0x0f, 0xe2, 0xb2, 0x81, 0x00, 0x9e, 0x7b]) , - fe([0x8c, 0x85, 0x2b, 0xc4, 0xfc, 0xf1, 0xab, 0xe8, 0x79, 0x22, 0xc4, 0x84, 0x17, 0x3a, 0xfa, 0x86, - 0xa6, 0x7d, 0xf9, 0xf3, 0x6f, 0x03, 0x57, 0x20, 0x4d, 0x79, 0xf9, 0x6e, 0x71, 0x54, 0x38, 0x09])) + aff( + fe([ + 0x2f, 0x8b, 0xc8, 0x40, 0x51, 0xd1, 0xac, 0x1a, 0x0b, 0xe4, 0xa9, 0xa2, 0x42, 0x21, 0x19, 0x2f, + 0x7b, 0x97, 0xbf, 0xf7, 0x57, 0x6d, 0x3f, 0x3d, 0x4f, 0x0f, 0xe2, 0xb2, 0x81, 0x00, 0x9e, 0x7b + ]), + fe([ + 0x8c, 0x85, 0x2b, 0xc4, 0xfc, 0xf1, 0xab, 0xe8, 0x79, 0x22, 0xc4, 0x84, 0x17, 0x3a, 0xfa, 0x86, + 0xa6, 0x7d, 0xf9, 0xf3, 0x6f, 0x03, 0x57, 0x20, 0x4d, 0x79, 0xf9, 0x6e, 0x71, 0x54, 0x38, 0x09 + ]) + ) ) v.append( - aff(fe([0x40, 0x29, 0x74, 0xa8, 0x2f, 0x5e, 0xf9, 0x79, 0xa4, 0xf3, 0x3e, 0xb9, 0xfd, 0x33, 0x31, 0xac, - 0x9a, 0x69, 0x88, 0x1e, 0x77, 0x21, 0x2d, 0xf3, 0x91, 0x52, 0x26, 0x15, 0xb2, 0xa6, 0xcf, 0x7e]) , - fe([0xc6, 0x20, 0x47, 0x6c, 0xa4, 0x7d, 0xcb, 0x63, 0xea, 0x5b, 0x03, 0xdf, 0x3e, 0x88, 0x81, 0x6d, - 0xce, 0x07, 0x42, 0x18, 0x60, 0x7e, 0x7b, 0x55, 0xfe, 0x6a, 0xf3, 0xda, 0x5c, 0x8b, 0x95, 0x10])) + aff( + fe([ + 0x40, 0x29, 0x74, 0xa8, 0x2f, 0x5e, 0xf9, 0x79, 0xa4, 0xf3, 0x3e, 0xb9, 0xfd, 0x33, 0x31, 0xac, + 0x9a, 0x69, 0x88, 0x1e, 0x77, 0x21, 0x2d, 0xf3, 0x91, 0x52, 0x26, 0x15, 0xb2, 0xa6, 0xcf, 0x7e + ]), + fe([ + 0xc6, 0x20, 0x47, 0x6c, 0xa4, 0x7d, 0xcb, 0x63, 0xea, 0x5b, 0x03, 0xdf, 0x3e, 0x88, 0x81, 0x6d, + 0xce, 0x07, 0x42, 0x18, 0x60, 0x7e, 0x7b, 0x55, 0xfe, 0x6a, 0xf3, 0xda, 0x5c, 0x8b, 0x95, 0x10 + ]) + ) ) v.append( - aff(fe([0x62, 0xe4, 0x0d, 0x03, 0xb4, 0xd7, 0xcd, 0xfa, 0xbd, 0x46, 0xdf, 0x93, 0x71, 0x10, 0x2c, 0xa8, - 0x3b, 0xb6, 0x09, 0x05, 0x70, 0x84, 0x43, 0x29, 0xa8, 0x59, 0xf5, 0x8e, 0x10, 0xe4, 0xd7, 0x20]) , - fe([0x57, 0x82, 0x1c, 0xab, 0xbf, 0x62, 0x70, 0xe8, 0xc4, 0xcf, 0xf0, 0x28, 0x6e, 0x16, 0x3c, 0x08, - 0x78, 0x89, 0x85, 0x46, 0x0f, 0xf6, 0x7f, 0xcf, 0xcb, 0x7e, 0xb8, 0x25, 0xe9, 0x5a, 0xfa, 0x03])) + aff( + fe([ + 0x62, 0xe4, 0x0d, 0x03, 0xb4, 0xd7, 0xcd, 0xfa, 0xbd, 0x46, 0xdf, 0x93, 0x71, 0x10, 0x2c, 0xa8, + 0x3b, 0xb6, 0x09, 0x05, 0x70, 0x84, 0x43, 0x29, 0xa8, 0x59, 0xf5, 0x8e, 0x10, 0xe4, 0xd7, 0x20 + ]), + fe([ + 0x57, 0x82, 0x1c, 0xab, 0xbf, 0x62, 0x70, 0xe8, 0xc4, 0xcf, 0xf0, 0x28, 0x6e, 0x16, 0x3c, 0x08, + 0x78, 0x89, 0x85, 0x46, 0x0f, 0xf6, 0x7f, 0xcf, 0xcb, 0x7e, 0xb8, 0x25, 0xe9, 0x5a, 0xfa, 0x03 + ]) + ) ) v.append( - aff(fe([0xfb, 0x95, 0x92, 0x63, 0x50, 0xfc, 0x62, 0xf0, 0xa4, 0x5e, 0x8c, 0x18, 0xc2, 0x17, 0x24, 0xb7, - 0x78, 0xc2, 0xa9, 0xe7, 0x6a, 0x32, 0xd6, 0x29, 0x85, 0xaf, 0xcb, 0x8d, 0x91, 0x13, 0xda, 0x6b]) , - fe([0x36, 0x0a, 0xc2, 0xb6, 0x4b, 0xa5, 0x5d, 0x07, 0x17, 0x41, 0x31, 0x5f, 0x62, 0x46, 0xf8, 0x92, - 0xf9, 0x66, 0x48, 0x73, 0xa6, 0x97, 0x0d, 0x7d, 0x88, 0xee, 0x62, 0xb1, 0x03, 0xa8, 0x3f, 0x2c])) + aff( + fe([ + 0xfb, 0x95, 0x92, 0x63, 0x50, 0xfc, 0x62, 0xf0, 0xa4, 0x5e, 0x8c, 0x18, 0xc2, 0x17, 0x24, 0xb7, + 0x78, 0xc2, 0xa9, 0xe7, 0x6a, 0x32, 0xd6, 0x29, 0x85, 0xaf, 0xcb, 0x8d, 0x91, 0x13, 0xda, 0x6b + ]), + fe([ + 0x36, 0x0a, 0xc2, 0xb6, 0x4b, 0xa5, 0x5d, 0x07, 0x17, 0x41, 0x31, 0x5f, 0x62, 0x46, 0xf8, 0x92, + 0xf9, 0x66, 0x48, 0x73, 0xa6, 0x97, 0x0d, 0x7d, 0x88, 0xee, 0x62, 0xb1, 0x03, 0xa8, 0x3f, 0x2c + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x4a, 0xb1, 0x70, 0x8a, 0xa9, 0xe8, 0x63, 0x79, 0x00, 0xe2, 0x25, 0x16, 0xca, 0x4b, 0x0f, 0xa4, - 0x66, 0xad, 0x19, 0x9f, 0x88, 0x67, 0x0c, 0x8b, 0xc2, 0x4a, 0x5b, 0x2b, 0x6d, 0x95, 0xaf, 0x19]) , - fe([0x8b, 0x9d, 0xb6, 0xcc, 0x60, 0xb4, 0x72, 0x4f, 0x17, 0x69, 0x5a, 0x4a, 0x68, 0x34, 0xab, 0xa1, - 0x45, 0x32, 0x3c, 0x83, 0x87, 0x72, 0x30, 0x54, 0x77, 0x68, 0xae, 0xfb, 0xb5, 0x8b, 0x22, 0x5e])) + aff( + fe([ + 0x4a, 0xb1, 0x70, 0x8a, 0xa9, 0xe8, 0x63, 0x79, 0x00, 0xe2, 0x25, 0x16, 0xca, 0x4b, 0x0f, 0xa4, + 0x66, 0xad, 0x19, 0x9f, 0x88, 0x67, 0x0c, 0x8b, 0xc2, 0x4a, 0x5b, 0x2b, 0x6d, 0x95, 0xaf, 0x19 + ]), + fe([ + 0x8b, 0x9d, 0xb6, 0xcc, 0x60, 0xb4, 0x72, 0x4f, 0x17, 0x69, 0x5a, 0x4a, 0x68, 0x34, 0xab, 0xa1, + 0x45, 0x32, 0x3c, 0x83, 0x87, 0x72, 0x30, 0x54, 0x77, 0x68, 0xae, 0xfb, 0xb5, 0x8b, 0x22, 0x5e + ]) + ) ) v.append( - aff(fe([0xf1, 0xb9, 0x87, 0x35, 0xc5, 0xbb, 0xb9, 0xcf, 0xf5, 0xd6, 0xcd, 0xd5, 0x0c, 0x7c, 0x0e, 0xe6, - 0x90, 0x34, 0xfb, 0x51, 0x42, 0x1e, 0x6d, 0xac, 0x9a, 0x46, 0xc4, 0x97, 0x29, 0x32, 0xbf, 0x45]) , - fe([0x66, 0x9e, 0xc6, 0x24, 0xc0, 0xed, 0xa5, 0x5d, 0x88, 0xd4, 0xf0, 0x73, 0x97, 0x7b, 0xea, 0x7f, - 0x42, 0xff, 0x21, 0xa0, 0x9b, 0x2f, 0x9a, 0xfd, 0x53, 0x57, 0x07, 0x84, 0x48, 0x88, 0x9d, 0x52])) + aff( + fe([ + 0xf1, 0xb9, 0x87, 0x35, 0xc5, 0xbb, 0xb9, 0xcf, 0xf5, 0xd6, 0xcd, 0xd5, 0x0c, 0x7c, 0x0e, 0xe6, + 0x90, 0x34, 0xfb, 0x51, 0x42, 0x1e, 0x6d, 0xac, 0x9a, 0x46, 0xc4, 0x97, 0x29, 0x32, 0xbf, 0x45 + ]), + fe([ + 0x66, 0x9e, 0xc6, 0x24, 0xc0, 0xed, 0xa5, 0x5d, 0x88, 0xd4, 0xf0, 0x73, 0x97, 0x7b, 0xea, 0x7f, + 0x42, 0xff, 0x21, 0xa0, 0x9b, 0x2f, 0x9a, 0xfd, 0x53, 0x57, 0x07, 0x84, 0x48, 0x88, 0x9d, 0x52 + ]) + ) ) v.append( - aff(fe([0xc6, 0x96, 0x48, 0x34, 0x2a, 0x06, 0xaf, 0x94, 0x3d, 0xf4, 0x1a, 0xcf, 0xf2, 0xc0, 0x21, 0xc2, - 0x42, 0x5e, 0xc8, 0x2f, 0x35, 0xa2, 0x3e, 0x29, 0xfa, 0x0c, 0x84, 0xe5, 0x89, 0x72, 0x7c, 0x06]) , - fe([0x32, 0x65, 0x03, 0xe5, 0x89, 0xa6, 0x6e, 0xb3, 0x5b, 0x8e, 0xca, 0xeb, 0xfe, 0x22, 0x56, 0x8b, - 0x5d, 0x14, 0x4b, 0x4d, 0xf9, 0xbe, 0xb5, 0xf5, 0xe6, 0x5c, 0x7b, 0x8b, 0xf4, 0x13, 0x11, 0x34])) + aff( + fe([ + 0xc6, 0x96, 0x48, 0x34, 0x2a, 0x06, 0xaf, 0x94, 0x3d, 0xf4, 0x1a, 0xcf, 0xf2, 0xc0, 0x21, 0xc2, + 0x42, 0x5e, 0xc8, 0x2f, 0x35, 0xa2, 0x3e, 0x29, 0xfa, 0x0c, 0x84, 0xe5, 0x89, 0x72, 0x7c, 0x06 + ]), + fe([ + 0x32, 0x65, 0x03, 0xe5, 0x89, 0xa6, 0x6e, 0xb3, 0x5b, 0x8e, 0xca, 0xeb, 0xfe, 0x22, 0x56, 0x8b, + 0x5d, 0x14, 0x4b, 0x4d, 0xf9, 0xbe, 0xb5, 0xf5, 0xe6, 0x5c, 0x7b, 0x8b, 0xf4, 0x13, 0x11, 0x34 + ]) + ) ) v.append( - aff(fe([0x07, 0xc6, 0x22, 0x15, 0xe2, 0x9c, 0x60, 0xa2, 0x19, 0xd9, 0x27, 0xae, 0x37, 0x4e, 0xa6, 0xc9, - 0x80, 0xa6, 0x91, 0x8f, 0x12, 0x49, 0xe5, 0x00, 0x18, 0x47, 0xd1, 0xd7, 0x28, 0x22, 0x63, 0x39]) , - fe([0xe8, 0xe2, 0x00, 0x7e, 0xf2, 0x9e, 0x1e, 0x99, 0x39, 0x95, 0x04, 0xbd, 0x1e, 0x67, 0x7b, 0xb2, - 0x26, 0xac, 0xe6, 0xaa, 0xe2, 0x46, 0xd5, 0xe4, 0xe8, 0x86, 0xbd, 0xab, 0x7c, 0x55, 0x59, 0x6f])) + aff( + fe([ + 0x07, 0xc6, 0x22, 0x15, 0xe2, 0x9c, 0x60, 0xa2, 0x19, 0xd9, 0x27, 0xae, 0x37, 0x4e, 0xa6, 0xc9, + 0x80, 0xa6, 0x91, 0x8f, 0x12, 0x49, 0xe5, 0x00, 0x18, 0x47, 0xd1, 0xd7, 0x28, 0x22, 0x63, 0x39 + ]), + fe([ + 0xe8, 0xe2, 0x00, 0x7e, 0xf2, 0x9e, 0x1e, 0x99, 0x39, 0x95, 0x04, 0xbd, 0x1e, 0x67, 0x7b, 0xb2, + 0x26, 0xac, 0xe6, 0xaa, 0xe2, 0x46, 0xd5, 0xe4, 0xe8, 0x86, 0xbd, 0xab, 0x7c, 0x55, 0x59, 0x6f + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x24, 0x64, 0x6e, 0x9b, 0x35, 0x71, 0x78, 0xce, 0x33, 0x03, 0x21, 0x33, 0x36, 0xf1, 0x73, 0x9b, - 0xb9, 0x15, 0x8b, 0x2c, 0x69, 0xcf, 0x4d, 0xed, 0x4f, 0x4d, 0x57, 0x14, 0x13, 0x82, 0xa4, 0x4d]) , - fe([0x65, 0x6e, 0x0a, 0xa4, 0x59, 0x07, 0x17, 0xf2, 0x6b, 0x4a, 0x1f, 0x6e, 0xf6, 0xb5, 0xbc, 0x62, - 0xe4, 0xb6, 0xda, 0xa2, 0x93, 0xbc, 0x29, 0x05, 0xd2, 0xd2, 0x73, 0x46, 0x03, 0x16, 0x40, 0x31])) + aff( + fe([ + 0x24, 0x64, 0x6e, 0x9b, 0x35, 0x71, 0x78, 0xce, 0x33, 0x03, 0x21, 0x33, 0x36, 0xf1, 0x73, 0x9b, + 0xb9, 0x15, 0x8b, 0x2c, 0x69, 0xcf, 0x4d, 0xed, 0x4f, 0x4d, 0x57, 0x14, 0x13, 0x82, 0xa4, 0x4d + ]), + fe([ + 0x65, 0x6e, 0x0a, 0xa4, 0x59, 0x07, 0x17, 0xf2, 0x6b, 0x4a, 0x1f, 0x6e, 0xf6, 0xb5, 0xbc, 0x62, + 0xe4, 0xb6, 0xda, 0xa2, 0x93, 0xbc, 0x29, 0x05, 0xd2, 0xd2, 0x73, 0x46, 0x03, 0x16, 0x40, 0x31 + ]) + ) ) v.append( - aff(fe([0x4c, 0x73, 0x6d, 0x15, 0xbd, 0xa1, 0x4d, 0x5c, 0x13, 0x0b, 0x24, 0x06, 0x98, 0x78, 0x1c, 0x5b, - 0xeb, 0x1f, 0x18, 0x54, 0x43, 0xd9, 0x55, 0x66, 0xda, 0x29, 0x21, 0xe8, 0xb8, 0x3c, 0x42, 0x22]) , - fe([0xb4, 0xcd, 0x08, 0x6f, 0x15, 0x23, 0x1a, 0x0b, 0x22, 0xed, 0xd1, 0xf1, 0xa7, 0xc7, 0x73, 0x45, - 0xf3, 0x9e, 0xce, 0x76, 0xb7, 0xf6, 0x39, 0xb6, 0x8e, 0x79, 0xbe, 0xe9, 0x9b, 0xcf, 0x7d, 0x62])) + aff( + fe([ + 0x4c, 0x73, 0x6d, 0x15, 0xbd, 0xa1, 0x4d, 0x5c, 0x13, 0x0b, 0x24, 0x06, 0x98, 0x78, 0x1c, 0x5b, + 0xeb, 0x1f, 0x18, 0x54, 0x43, 0xd9, 0x55, 0x66, 0xda, 0x29, 0x21, 0xe8, 0xb8, 0x3c, 0x42, 0x22 + ]), + fe([ + 0xb4, 0xcd, 0x08, 0x6f, 0x15, 0x23, 0x1a, 0x0b, 0x22, 0xed, 0xd1, 0xf1, 0xa7, 0xc7, 0x73, 0x45, + 0xf3, 0x9e, 0xce, 0x76, 0xb7, 0xf6, 0x39, 0xb6, 0x8e, 0x79, 0xbe, 0xe9, 0x9b, 0xcf, 0x7d, 0x62 + ]) + ) ) v.append( - aff(fe([0x92, 0x5b, 0xfc, 0x72, 0xfd, 0xba, 0xf1, 0xfd, 0xa6, 0x7c, 0x95, 0xe3, 0x61, 0x3f, 0xe9, 0x03, - 0xd4, 0x2b, 0xd4, 0x20, 0xd9, 0xdb, 0x4d, 0x32, 0x3e, 0xf5, 0x11, 0x64, 0xe3, 0xb4, 0xbe, 0x32]) , - fe([0x86, 0x17, 0x90, 0xe7, 0xc9, 0x1f, 0x10, 0xa5, 0x6a, 0x2d, 0x39, 0xd0, 0x3b, 0xc4, 0xa6, 0xe9, - 0x59, 0x13, 0xda, 0x1a, 0xe6, 0xa0, 0xb9, 0x3c, 0x50, 0xb8, 0x40, 0x7c, 0x15, 0x36, 0x5a, 0x42])) + aff( + fe([ + 0x92, 0x5b, 0xfc, 0x72, 0xfd, 0xba, 0xf1, 0xfd, 0xa6, 0x7c, 0x95, 0xe3, 0x61, 0x3f, 0xe9, 0x03, + 0xd4, 0x2b, 0xd4, 0x20, 0xd9, 0xdb, 0x4d, 0x32, 0x3e, 0xf5, 0x11, 0x64, 0xe3, 0xb4, 0xbe, 0x32 + ]), + fe([ + 0x86, 0x17, 0x90, 0xe7, 0xc9, 0x1f, 0x10, 0xa5, 0x6a, 0x2d, 0x39, 0xd0, 0x3b, 0xc4, 0xa6, 0xe9, + 0x59, 0x13, 0xda, 0x1a, 0xe6, 0xa0, 0xb9, 0x3c, 0x50, 0xb8, 0x40, 0x7c, 0x15, 0x36, 0x5a, 0x42 + ]) + ) ) v.append( - aff(fe([0xb4, 0x0b, 0x32, 0xab, 0xdc, 0x04, 0x51, 0x55, 0x21, 0x1e, 0x0b, 0x75, 0x99, 0x89, 0x73, 0x35, - 0x3a, 0x91, 0x2b, 0xfe, 0xe7, 0x49, 0xea, 0x76, 0xc1, 0xf9, 0x46, 0xb9, 0x53, 0x02, 0x23, 0x04]) , - fe([0xfc, 0x5a, 0x1e, 0x1d, 0x74, 0x58, 0x95, 0xa6, 0x8f, 0x7b, 0x97, 0x3e, 0x17, 0x3b, 0x79, 0x2d, - 0xa6, 0x57, 0xef, 0x45, 0x02, 0x0b, 0x4d, 0x6e, 0x9e, 0x93, 0x8d, 0x2f, 0xd9, 0x9d, 0xdb, 0x04])) + aff( + fe([ + 0xb4, 0x0b, 0x32, 0xab, 0xdc, 0x04, 0x51, 0x55, 0x21, 0x1e, 0x0b, 0x75, 0x99, 0x89, 0x73, 0x35, + 0x3a, 0x91, 0x2b, 0xfe, 0xe7, 0x49, 0xea, 0x76, 0xc1, 0xf9, 0x46, 0xb9, 0x53, 0x02, 0x23, 0x04 + ]), + fe([ + 0xfc, 0x5a, 0x1e, 0x1d, 0x74, 0x58, 0x95, 0xa6, 0x8f, 0x7b, 0x97, 0x3e, 0x17, 0x3b, 0x79, 0x2d, + 0xa6, 0x57, 0xef, 0x45, 0x02, 0x0b, 0x4d, 0x6e, 0x9e, 0x93, 0x8d, 0x2f, 0xd9, 0x9d, 0xdb, 0x04 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xc0, 0xd7, 0x56, 0x97, 0x58, 0x91, 0xde, 0x09, 0x4f, 0x9f, 0xbe, 0x63, 0xb0, 0x83, 0x86, 0x43, - 0x5d, 0xbc, 0xe0, 0xf3, 0xc0, 0x75, 0xbf, 0x8b, 0x8e, 0xaa, 0xf7, 0x8b, 0x64, 0x6e, 0xb0, 0x63]) , - fe([0x16, 0xae, 0x8b, 0xe0, 0x9b, 0x24, 0x68, 0x5c, 0x44, 0xc2, 0xd0, 0x08, 0xb7, 0x7b, 0x62, 0xfd, - 0x7f, 0xd8, 0xd4, 0xb7, 0x50, 0xfd, 0x2c, 0x1b, 0xbf, 0x41, 0x95, 0xd9, 0x8e, 0xd8, 0x17, 0x1b])) + aff( + fe([ + 0xc0, 0xd7, 0x56, 0x97, 0x58, 0x91, 0xde, 0x09, 0x4f, 0x9f, 0xbe, 0x63, 0xb0, 0x83, 0x86, 0x43, + 0x5d, 0xbc, 0xe0, 0xf3, 0xc0, 0x75, 0xbf, 0x8b, 0x8e, 0xaa, 0xf7, 0x8b, 0x64, 0x6e, 0xb0, 0x63 + ]), + fe([ + 0x16, 0xae, 0x8b, 0xe0, 0x9b, 0x24, 0x68, 0x5c, 0x44, 0xc2, 0xd0, 0x08, 0xb7, 0x7b, 0x62, 0xfd, + 0x7f, 0xd8, 0xd4, 0xb7, 0x50, 0xfd, 0x2c, 0x1b, 0xbf, 0x41, 0x95, 0xd9, 0x8e, 0xd8, 0x17, 0x1b + ]) + ) ) v.append( - aff(fe([0x86, 0x55, 0x37, 0x8e, 0xc3, 0x38, 0x48, 0x14, 0xb5, 0x97, 0xd2, 0xa7, 0x54, 0x45, 0xf1, 0x35, - 0x44, 0x38, 0x9e, 0xf1, 0x1b, 0xb6, 0x34, 0x00, 0x3c, 0x96, 0xee, 0x29, 0x00, 0xea, 0x2c, 0x0b]) , - fe([0xea, 0xda, 0x99, 0x9e, 0x19, 0x83, 0x66, 0x6d, 0xe9, 0x76, 0x87, 0x50, 0xd1, 0xfd, 0x3c, 0x60, - 0x87, 0xc6, 0x41, 0xd9, 0x8e, 0xdb, 0x5e, 0xde, 0xaa, 0x9a, 0xd3, 0x28, 0xda, 0x95, 0xea, 0x47])) + aff( + fe([ + 0x86, 0x55, 0x37, 0x8e, 0xc3, 0x38, 0x48, 0x14, 0xb5, 0x97, 0xd2, 0xa7, 0x54, 0x45, 0xf1, 0x35, + 0x44, 0x38, 0x9e, 0xf1, 0x1b, 0xb6, 0x34, 0x00, 0x3c, 0x96, 0xee, 0x29, 0x00, 0xea, 0x2c, 0x0b + ]), + fe([ + 0xea, 0xda, 0x99, 0x9e, 0x19, 0x83, 0x66, 0x6d, 0xe9, 0x76, 0x87, 0x50, 0xd1, 0xfd, 0x3c, 0x60, + 0x87, 0xc6, 0x41, 0xd9, 0x8e, 0xdb, 0x5e, 0xde, 0xaa, 0x9a, 0xd3, 0x28, 0xda, 0x95, 0xea, 0x47 + ]) + ) ) v.append( - aff(fe([0xd0, 0x80, 0xba, 0x19, 0xae, 0x1d, 0xa9, 0x79, 0xf6, 0x3f, 0xac, 0x5d, 0x6f, 0x96, 0x1f, 0x2a, - 0xce, 0x29, 0xb2, 0xff, 0x37, 0xf1, 0x94, 0x8f, 0x0c, 0xb5, 0x28, 0xba, 0x9a, 0x21, 0xf6, 0x66]) , - fe([0x02, 0xfb, 0x54, 0xb8, 0x05, 0xf3, 0x81, 0x52, 0x69, 0x34, 0x46, 0x9d, 0x86, 0x76, 0x8f, 0xd7, - 0xf8, 0x6a, 0x66, 0xff, 0xe6, 0xa7, 0x90, 0xf7, 0x5e, 0xcd, 0x6a, 0x9b, 0x55, 0xfc, 0x9d, 0x48])) + aff( + fe([ + 0xd0, 0x80, 0xba, 0x19, 0xae, 0x1d, 0xa9, 0x79, 0xf6, 0x3f, 0xac, 0x5d, 0x6f, 0x96, 0x1f, 0x2a, + 0xce, 0x29, 0xb2, 0xff, 0x37, 0xf1, 0x94, 0x8f, 0x0c, 0xb5, 0x28, 0xba, 0x9a, 0x21, 0xf6, 0x66 + ]), + fe([ + 0x02, 0xfb, 0x54, 0xb8, 0x05, 0xf3, 0x81, 0x52, 0x69, 0x34, 0x46, 0x9d, 0x86, 0x76, 0x8f, 0xd7, + 0xf8, 0x6a, 0x66, 0xff, 0xe6, 0xa7, 0x90, 0xf7, 0x5e, 0xcd, 0x6a, 0x9b, 0x55, 0xfc, 0x9d, 0x48 + ]) + ) ) v.append( - aff(fe([0xbd, 0xaa, 0x13, 0xe6, 0xcd, 0x45, 0x4a, 0xa4, 0x59, 0x0a, 0x64, 0xb1, 0x98, 0xd6, 0x34, 0x13, - 0x04, 0xe6, 0x97, 0x94, 0x06, 0xcb, 0xd4, 0x4e, 0xbb, 0x96, 0xcd, 0xd1, 0x57, 0xd1, 0xe3, 0x06]) , - fe([0x7a, 0x6c, 0x45, 0x27, 0xc4, 0x93, 0x7f, 0x7d, 0x7c, 0x62, 0x50, 0x38, 0x3a, 0x6b, 0xb5, 0x88, - 0xc6, 0xd9, 0xf1, 0x78, 0x19, 0xb9, 0x39, 0x93, 0x3d, 0xc9, 0xe0, 0x9c, 0x3c, 0xce, 0xf5, 0x72])) + aff( + fe([ + 0xbd, 0xaa, 0x13, 0xe6, 0xcd, 0x45, 0x4a, 0xa4, 0x59, 0x0a, 0x64, 0xb1, 0x98, 0xd6, 0x34, 0x13, + 0x04, 0xe6, 0x97, 0x94, 0x06, 0xcb, 0xd4, 0x4e, 0xbb, 0x96, 0xcd, 0xd1, 0x57, 0xd1, 0xe3, 0x06 + ]), + fe([ + 0x7a, 0x6c, 0x45, 0x27, 0xc4, 0x93, 0x7f, 0x7d, 0x7c, 0x62, 0x50, 0x38, 0x3a, 0x6b, 0xb5, 0x88, + 0xc6, 0xd9, 0xf1, 0x78, 0x19, 0xb9, 0x39, 0x93, 0x3d, 0xc9, 0xe0, 0x9c, 0x3c, 0xce, 0xf5, 0x72 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x24, 0xea, 0x23, 0x7d, 0x56, 0x2c, 0xe2, 0x59, 0x0e, 0x85, 0x60, 0x04, 0x88, 0x5a, 0x74, 0x1e, - 0x4b, 0xef, 0x13, 0xda, 0x4c, 0xff, 0x83, 0x45, 0x85, 0x3f, 0x08, 0x95, 0x2c, 0x20, 0x13, 0x1f]) , - fe([0x48, 0x5f, 0x27, 0x90, 0x5c, 0x02, 0x42, 0xad, 0x78, 0x47, 0x5c, 0xb5, 0x7e, 0x08, 0x85, 0x00, - 0xfa, 0x7f, 0xfd, 0xfd, 0xe7, 0x09, 0x11, 0xf2, 0x7e, 0x1b, 0x38, 0x6c, 0x35, 0x6d, 0x33, 0x66])) + aff( + fe([ + 0x24, 0xea, 0x23, 0x7d, 0x56, 0x2c, 0xe2, 0x59, 0x0e, 0x85, 0x60, 0x04, 0x88, 0x5a, 0x74, 0x1e, + 0x4b, 0xef, 0x13, 0xda, 0x4c, 0xff, 0x83, 0x45, 0x85, 0x3f, 0x08, 0x95, 0x2c, 0x20, 0x13, 0x1f + ]), + fe([ + 0x48, 0x5f, 0x27, 0x90, 0x5c, 0x02, 0x42, 0xad, 0x78, 0x47, 0x5c, 0xb5, 0x7e, 0x08, 0x85, 0x00, + 0xfa, 0x7f, 0xfd, 0xfd, 0xe7, 0x09, 0x11, 0xf2, 0x7e, 0x1b, 0x38, 0x6c, 0x35, 0x6d, 0x33, 0x66 + ]) + ) ) v.append( - aff(fe([0x93, 0x03, 0x36, 0x81, 0xac, 0xe4, 0x20, 0x09, 0x35, 0x4c, 0x45, 0xb2, 0x1e, 0x4c, 0x14, 0x21, - 0xe6, 0xe9, 0x8a, 0x7b, 0x8d, 0xfe, 0x1e, 0xc6, 0x3e, 0xc1, 0x35, 0xfa, 0xe7, 0x70, 0x4e, 0x1d]) , - fe([0x61, 0x2e, 0xc2, 0xdd, 0x95, 0x57, 0xd1, 0xab, 0x80, 0xe8, 0x63, 0x17, 0xb5, 0x48, 0xe4, 0x8a, - 0x11, 0x9e, 0x72, 0xbe, 0x85, 0x8d, 0x51, 0x0a, 0xf2, 0x9f, 0xe0, 0x1c, 0xa9, 0x07, 0x28, 0x7b])) + aff( + fe([ + 0x93, 0x03, 0x36, 0x81, 0xac, 0xe4, 0x20, 0x09, 0x35, 0x4c, 0x45, 0xb2, 0x1e, 0x4c, 0x14, 0x21, + 0xe6, 0xe9, 0x8a, 0x7b, 0x8d, 0xfe, 0x1e, 0xc6, 0x3e, 0xc1, 0x35, 0xfa, 0xe7, 0x70, 0x4e, 0x1d + ]), + fe([ + 0x61, 0x2e, 0xc2, 0xdd, 0x95, 0x57, 0xd1, 0xab, 0x80, 0xe8, 0x63, 0x17, 0xb5, 0x48, 0xe4, 0x8a, + 0x11, 0x9e, 0x72, 0xbe, 0x85, 0x8d, 0x51, 0x0a, 0xf2, 0x9f, 0xe0, 0x1c, 0xa9, 0x07, 0x28, 0x7b + ]) + ) ) v.append( - aff(fe([0xbb, 0x71, 0x14, 0x5e, 0x26, 0x8c, 0x3d, 0xc8, 0xe9, 0x7c, 0xd3, 0xd6, 0xd1, 0x2f, 0x07, 0x6d, - 0xe6, 0xdf, 0xfb, 0x79, 0xd6, 0x99, 0x59, 0x96, 0x48, 0x40, 0x0f, 0x3a, 0x7b, 0xb2, 0xa0, 0x72]) , - fe([0x4e, 0x3b, 0x69, 0xc8, 0x43, 0x75, 0x51, 0x6c, 0x79, 0x56, 0xe4, 0xcb, 0xf7, 0xa6, 0x51, 0xc2, - 0x2c, 0x42, 0x0b, 0xd4, 0x82, 0x20, 0x1c, 0x01, 0x08, 0x66, 0xd7, 0xbf, 0x04, 0x56, 0xfc, 0x02])) + aff( + fe([ + 0xbb, 0x71, 0x14, 0x5e, 0x26, 0x8c, 0x3d, 0xc8, 0xe9, 0x7c, 0xd3, 0xd6, 0xd1, 0x2f, 0x07, 0x6d, + 0xe6, 0xdf, 0xfb, 0x79, 0xd6, 0x99, 0x59, 0x96, 0x48, 0x40, 0x0f, 0x3a, 0x7b, 0xb2, 0xa0, 0x72 + ]), + fe([ + 0x4e, 0x3b, 0x69, 0xc8, 0x43, 0x75, 0x51, 0x6c, 0x79, 0x56, 0xe4, 0xcb, 0xf7, 0xa6, 0x51, 0xc2, + 0x2c, 0x42, 0x0b, 0xd4, 0x82, 0x20, 0x1c, 0x01, 0x08, 0x66, 0xd7, 0xbf, 0x04, 0x56, 0xfc, 0x02 + ]) + ) ) v.append( - aff(fe([0x24, 0xe8, 0xb7, 0x60, 0xae, 0x47, 0x80, 0xfc, 0xe5, 0x23, 0xe7, 0xc2, 0xc9, 0x85, 0xe6, 0x98, - 0xa0, 0x29, 0x4e, 0xe1, 0x84, 0x39, 0x2d, 0x95, 0x2c, 0xf3, 0x45, 0x3c, 0xff, 0xaf, 0x27, 0x4c]) , - fe([0x6b, 0xa6, 0xf5, 0x4b, 0x11, 0xbd, 0xba, 0x5b, 0x9e, 0xc4, 0xa4, 0x51, 0x1e, 0xbe, 0xd0, 0x90, - 0x3a, 0x9c, 0xc2, 0x26, 0xb6, 0x1e, 0xf1, 0x95, 0x7d, 0xc8, 0x6d, 0x52, 0xe6, 0x99, 0x2c, 0x5f])) + aff( + fe([ + 0x24, 0xe8, 0xb7, 0x60, 0xae, 0x47, 0x80, 0xfc, 0xe5, 0x23, 0xe7, 0xc2, 0xc9, 0x85, 0xe6, 0x98, + 0xa0, 0x29, 0x4e, 0xe1, 0x84, 0x39, 0x2d, 0x95, 0x2c, 0xf3, 0x45, 0x3c, 0xff, 0xaf, 0x27, 0x4c + ]), + fe([ + 0x6b, 0xa6, 0xf5, 0x4b, 0x11, 0xbd, 0xba, 0x5b, 0x9e, 0xc4, 0xa4, 0x51, 0x1e, 0xbe, 0xd0, 0x90, + 0x3a, 0x9c, 0xc2, 0x26, 0xb6, 0x1e, 0xf1, 0x95, 0x7d, 0xc8, 0x6d, 0x52, 0xe6, 0x99, 0x2c, 0x5f + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x85, 0xe0, 0x24, 0x32, 0xb4, 0xd1, 0xef, 0xfc, 0x69, 0xa2, 0xbf, 0x8f, 0x72, 0x2c, 0x95, 0xf6, - 0xe4, 0x6e, 0x7d, 0x90, 0xf7, 0x57, 0x81, 0xa0, 0xf7, 0xda, 0xef, 0x33, 0x07, 0xe3, 0x6b, 0x78]) , - fe([0x36, 0x27, 0x3e, 0xc6, 0x12, 0x07, 0xab, 0x4e, 0xbe, 0x69, 0x9d, 0xb3, 0xbe, 0x08, 0x7c, 0x2a, - 0x47, 0x08, 0xfd, 0xd4, 0xcd, 0x0e, 0x27, 0x34, 0x5b, 0x98, 0x34, 0x2f, 0x77, 0x5f, 0x3a, 0x65])) + aff( + fe([ + 0x85, 0xe0, 0x24, 0x32, 0xb4, 0xd1, 0xef, 0xfc, 0x69, 0xa2, 0xbf, 0x8f, 0x72, 0x2c, 0x95, 0xf6, + 0xe4, 0x6e, 0x7d, 0x90, 0xf7, 0x57, 0x81, 0xa0, 0xf7, 0xda, 0xef, 0x33, 0x07, 0xe3, 0x6b, 0x78 + ]), + fe([ + 0x36, 0x27, 0x3e, 0xc6, 0x12, 0x07, 0xab, 0x4e, 0xbe, 0x69, 0x9d, 0xb3, 0xbe, 0x08, 0x7c, 0x2a, + 0x47, 0x08, 0xfd, 0xd4, 0xcd, 0x0e, 0x27, 0x34, 0x5b, 0x98, 0x34, 0x2f, 0x77, 0x5f, 0x3a, 0x65 + ]) + ) ) v.append( - aff(fe([0x13, 0xaa, 0x2e, 0x4c, 0xf0, 0x22, 0xb8, 0x6c, 0xb3, 0x19, 0x4d, 0xeb, 0x6b, 0xd0, 0xa4, 0xc6, - 0x9c, 0xdd, 0xc8, 0x5b, 0x81, 0x57, 0x89, 0xdf, 0x33, 0xa9, 0x68, 0x49, 0x80, 0xe4, 0xfe, 0x21]) , - fe([0x00, 0x17, 0x90, 0x30, 0xe9, 0xd3, 0x60, 0x30, 0x31, 0xc2, 0x72, 0x89, 0x7a, 0x36, 0xa5, 0xbd, - 0x39, 0x83, 0x85, 0x50, 0xa1, 0x5d, 0x6c, 0x41, 0x1d, 0xb5, 0x2c, 0x07, 0x40, 0x77, 0x0b, 0x50])) + aff( + fe([ + 0x13, 0xaa, 0x2e, 0x4c, 0xf0, 0x22, 0xb8, 0x6c, 0xb3, 0x19, 0x4d, 0xeb, 0x6b, 0xd0, 0xa4, 0xc6, + 0x9c, 0xdd, 0xc8, 0x5b, 0x81, 0x57, 0x89, 0xdf, 0x33, 0xa9, 0x68, 0x49, 0x80, 0xe4, 0xfe, 0x21 + ]), + fe([ + 0x00, 0x17, 0x90, 0x30, 0xe9, 0xd3, 0x60, 0x30, 0x31, 0xc2, 0x72, 0x89, 0x7a, 0x36, 0xa5, 0xbd, + 0x39, 0x83, 0x85, 0x50, 0xa1, 0x5d, 0x6c, 0x41, 0x1d, 0xb5, 0x2c, 0x07, 0x40, 0x77, 0x0b, 0x50 + ]) + ) ) v.append( - aff(fe([0x64, 0x34, 0xec, 0xc0, 0x9e, 0x44, 0x41, 0xaf, 0xa0, 0x36, 0x05, 0x6d, 0xea, 0x30, 0x25, 0x46, - 0x35, 0x24, 0x9d, 0x86, 0xbd, 0x95, 0xf1, 0x6a, 0x46, 0xd7, 0x94, 0x54, 0xf9, 0x3b, 0xbd, 0x5d]) , - fe([0x77, 0x5b, 0xe2, 0x37, 0xc7, 0xe1, 0x7c, 0x13, 0x8c, 0x9f, 0x7b, 0x7b, 0x2a, 0xce, 0x42, 0xa3, - 0xb9, 0x2a, 0x99, 0xa8, 0xc0, 0xd8, 0x3c, 0x86, 0xb0, 0xfb, 0xe9, 0x76, 0x77, 0xf7, 0xf5, 0x56])) + aff( + fe([ + 0x64, 0x34, 0xec, 0xc0, 0x9e, 0x44, 0x41, 0xaf, 0xa0, 0x36, 0x05, 0x6d, 0xea, 0x30, 0x25, 0x46, + 0x35, 0x24, 0x9d, 0x86, 0xbd, 0x95, 0xf1, 0x6a, 0x46, 0xd7, 0x94, 0x54, 0xf9, 0x3b, 0xbd, 0x5d + ]), + fe([ + 0x77, 0x5b, 0xe2, 0x37, 0xc7, 0xe1, 0x7c, 0x13, 0x8c, 0x9f, 0x7b, 0x7b, 0x2a, 0xce, 0x42, 0xa3, + 0xb9, 0x2a, 0x99, 0xa8, 0xc0, 0xd8, 0x3c, 0x86, 0xb0, 0xfb, 0xe9, 0x76, 0x77, 0xf7, 0xf5, 0x56 + ]) + ) ) v.append( - aff(fe([0xdf, 0xb3, 0x46, 0x11, 0x6e, 0x13, 0xb7, 0x28, 0x4e, 0x56, 0xdd, 0xf1, 0xac, 0xad, 0x58, 0xc3, - 0xf8, 0x88, 0x94, 0x5e, 0x06, 0x98, 0xa1, 0xe4, 0x6a, 0xfb, 0x0a, 0x49, 0x5d, 0x8a, 0xfe, 0x77]) , - fe([0x46, 0x02, 0xf5, 0xa5, 0xaf, 0xc5, 0x75, 0x6d, 0xba, 0x45, 0x35, 0x0a, 0xfe, 0xc9, 0xac, 0x22, - 0x91, 0x8d, 0x21, 0x95, 0x33, 0x03, 0xc0, 0x8a, 0x16, 0xf3, 0x39, 0xe0, 0x01, 0x0f, 0x53, 0x3c])) + aff( + fe([ + 0xdf, 0xb3, 0x46, 0x11, 0x6e, 0x13, 0xb7, 0x28, 0x4e, 0x56, 0xdd, 0xf1, 0xac, 0xad, 0x58, 0xc3, + 0xf8, 0x88, 0x94, 0x5e, 0x06, 0x98, 0xa1, 0xe4, 0x6a, 0xfb, 0x0a, 0x49, 0x5d, 0x8a, 0xfe, 0x77 + ]), + fe([ + 0x46, 0x02, 0xf5, 0xa5, 0xaf, 0xc5, 0x75, 0x6d, 0xba, 0x45, 0x35, 0x0a, 0xfe, 0xc9, 0xac, 0x22, + 0x91, 0x8d, 0x21, 0x95, 0x33, 0x03, 0xc0, 0x8a, 0x16, 0xf3, 0x39, 0xe0, 0x01, 0x0f, 0x53, 0x3c + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x34, 0x75, 0x37, 0x1f, 0x34, 0x4e, 0xa9, 0x1d, 0x68, 0x67, 0xf8, 0x49, 0x98, 0x96, 0xfc, 0x4c, - 0x65, 0x97, 0xf7, 0x02, 0x4a, 0x52, 0x6c, 0x01, 0xbd, 0x48, 0xbb, 0x1b, 0xed, 0xa4, 0xe2, 0x53]) , - fe([0x59, 0xd5, 0x9b, 0x5a, 0xa2, 0x90, 0xd3, 0xb8, 0x37, 0x4c, 0x55, 0x82, 0x28, 0x08, 0x0f, 0x7f, - 0xaa, 0x81, 0x65, 0xe0, 0x0c, 0x52, 0xc9, 0xa3, 0x32, 0x27, 0x64, 0xda, 0xfd, 0x34, 0x23, 0x5a])) + aff( + fe([ + 0x34, 0x75, 0x37, 0x1f, 0x34, 0x4e, 0xa9, 0x1d, 0x68, 0x67, 0xf8, 0x49, 0x98, 0x96, 0xfc, 0x4c, + 0x65, 0x97, 0xf7, 0x02, 0x4a, 0x52, 0x6c, 0x01, 0xbd, 0x48, 0xbb, 0x1b, 0xed, 0xa4, 0xe2, 0x53 + ]), + fe([ + 0x59, 0xd5, 0x9b, 0x5a, 0xa2, 0x90, 0xd3, 0xb8, 0x37, 0x4c, 0x55, 0x82, 0x28, 0x08, 0x0f, 0x7f, + 0xaa, 0x81, 0x65, 0xe0, 0x0c, 0x52, 0xc9, 0xa3, 0x32, 0x27, 0x64, 0xda, 0xfd, 0x34, 0x23, 0x5a + ]) + ) ) v.append( - aff(fe([0xb5, 0xb0, 0x0c, 0x4d, 0xb3, 0x7b, 0x23, 0xc8, 0x1f, 0x8a, 0x39, 0x66, 0xe6, 0xba, 0x4c, 0x10, - 0x37, 0xca, 0x9c, 0x7c, 0x05, 0x9e, 0xff, 0xc0, 0xf8, 0x8e, 0xb1, 0x8f, 0x6f, 0x67, 0x18, 0x26]) , - fe([0x4b, 0x41, 0x13, 0x54, 0x23, 0x1a, 0xa4, 0x4e, 0xa9, 0x8b, 0x1e, 0x4b, 0xfc, 0x15, 0x24, 0xbb, - 0x7e, 0xcb, 0xb6, 0x1e, 0x1b, 0xf5, 0xf2, 0xc8, 0x56, 0xec, 0x32, 0xa2, 0x60, 0x5b, 0xa0, 0x2a])) + aff( + fe([ + 0xb5, 0xb0, 0x0c, 0x4d, 0xb3, 0x7b, 0x23, 0xc8, 0x1f, 0x8a, 0x39, 0x66, 0xe6, 0xba, 0x4c, 0x10, + 0x37, 0xca, 0x9c, 0x7c, 0x05, 0x9e, 0xff, 0xc0, 0xf8, 0x8e, 0xb1, 0x8f, 0x6f, 0x67, 0x18, 0x26 + ]), + fe([ + 0x4b, 0x41, 0x13, 0x54, 0x23, 0x1a, 0xa4, 0x4e, 0xa9, 0x8b, 0x1e, 0x4b, 0xfc, 0x15, 0x24, 0xbb, + 0x7e, 0xcb, 0xb6, 0x1e, 0x1b, 0xf5, 0xf2, 0xc8, 0x56, 0xec, 0x32, 0xa2, 0x60, 0x5b, 0xa0, 0x2a + ]) + ) ) v.append( - aff(fe([0xa4, 0x29, 0x47, 0x86, 0x2e, 0x92, 0x4f, 0x11, 0x4f, 0xf3, 0xb2, 0x5c, 0xd5, 0x3e, 0xa6, 0xb9, - 0xc8, 0xe2, 0x33, 0x11, 0x1f, 0x01, 0x8f, 0xb0, 0x9b, 0xc7, 0xa5, 0xff, 0x83, 0x0f, 0x1e, 0x28]) , - fe([0x1d, 0x29, 0x7a, 0xa1, 0xec, 0x8e, 0xb5, 0xad, 0xea, 0x02, 0x68, 0x60, 0x74, 0x29, 0x1c, 0xa5, - 0xcf, 0xc8, 0x3b, 0x7d, 0x8b, 0x2b, 0x7c, 0xad, 0xa4, 0x40, 0x17, 0x51, 0x59, 0x7c, 0x2e, 0x5d])) + aff( + fe([ + 0xa4, 0x29, 0x47, 0x86, 0x2e, 0x92, 0x4f, 0x11, 0x4f, 0xf3, 0xb2, 0x5c, 0xd5, 0x3e, 0xa6, 0xb9, + 0xc8, 0xe2, 0x33, 0x11, 0x1f, 0x01, 0x8f, 0xb0, 0x9b, 0xc7, 0xa5, 0xff, 0x83, 0x0f, 0x1e, 0x28 + ]), + fe([ + 0x1d, 0x29, 0x7a, 0xa1, 0xec, 0x8e, 0xb5, 0xad, 0xea, 0x02, 0x68, 0x60, 0x74, 0x29, 0x1c, 0xa5, + 0xcf, 0xc8, 0x3b, 0x7d, 0x8b, 0x2b, 0x7c, 0xad, 0xa4, 0x40, 0x17, 0x51, 0x59, 0x7c, 0x2e, 0x5d + ]) + ) ) v.append( - aff(fe([0x0a, 0x6c, 0x4f, 0xbc, 0x3e, 0x32, 0xe7, 0x4a, 0x1a, 0x13, 0xc1, 0x49, 0x38, 0xbf, 0xf7, 0xc2, - 0xd3, 0x8f, 0x6b, 0xad, 0x52, 0xf7, 0xcf, 0xbc, 0x27, 0xcb, 0x40, 0x67, 0x76, 0xcd, 0x6d, 0x56]) , - fe([0xe5, 0xb0, 0x27, 0xad, 0xbe, 0x9b, 0xf2, 0xb5, 0x63, 0xde, 0x3a, 0x23, 0x95, 0xb7, 0x0a, 0x7e, - 0xf3, 0x9e, 0x45, 0x6f, 0x19, 0x39, 0x75, 0x8f, 0x39, 0x3d, 0x0f, 0xc0, 0x9f, 0xf1, 0xe9, 0x51])) + aff( + fe([ + 0x0a, 0x6c, 0x4f, 0xbc, 0x3e, 0x32, 0xe7, 0x4a, 0x1a, 0x13, 0xc1, 0x49, 0x38, 0xbf, 0xf7, 0xc2, + 0xd3, 0x8f, 0x6b, 0xad, 0x52, 0xf7, 0xcf, 0xbc, 0x27, 0xcb, 0x40, 0x67, 0x76, 0xcd, 0x6d, 0x56 + ]), + fe([ + 0xe5, 0xb0, 0x27, 0xad, 0xbe, 0x9b, 0xf2, 0xb5, 0x63, 0xde, 0x3a, 0x23, 0x95, 0xb7, 0x0a, 0x7e, + 0xf3, 0x9e, 0x45, 0x6f, 0x19, 0x39, 0x75, 0x8f, 0x39, 0x3d, 0x0f, 0xc0, 0x9f, 0xf1, 0xe9, 0x51 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x88, 0xaa, 0x14, 0x24, 0x86, 0x94, 0x11, 0x12, 0x3e, 0x1a, 0xb5, 0xcc, 0xbb, 0xe0, 0x9c, 0xd5, - 0x9c, 0x6d, 0xba, 0x58, 0x72, 0x8d, 0xfb, 0x22, 0x7b, 0x9f, 0x7c, 0x94, 0x30, 0xb3, 0x51, 0x21]) , - fe([0xf6, 0x74, 0x3d, 0xf2, 0xaf, 0xd0, 0x1e, 0x03, 0x7c, 0x23, 0x6b, 0xc9, 0xfc, 0x25, 0x70, 0x90, - 0xdc, 0x9a, 0xa4, 0xfb, 0x49, 0xfc, 0x3d, 0x0a, 0x35, 0x38, 0x6f, 0xe4, 0x7e, 0x50, 0x01, 0x2a])) + aff( + fe([ + 0x88, 0xaa, 0x14, 0x24, 0x86, 0x94, 0x11, 0x12, 0x3e, 0x1a, 0xb5, 0xcc, 0xbb, 0xe0, 0x9c, 0xd5, + 0x9c, 0x6d, 0xba, 0x58, 0x72, 0x8d, 0xfb, 0x22, 0x7b, 0x9f, 0x7c, 0x94, 0x30, 0xb3, 0x51, 0x21 + ]), + fe([ + 0xf6, 0x74, 0x3d, 0xf2, 0xaf, 0xd0, 0x1e, 0x03, 0x7c, 0x23, 0x6b, 0xc9, 0xfc, 0x25, 0x70, 0x90, + 0xdc, 0x9a, 0xa4, 0xfb, 0x49, 0xfc, 0x3d, 0x0a, 0x35, 0x38, 0x6f, 0xe4, 0x7e, 0x50, 0x01, 0x2a + ]) + ) ) v.append( - aff(fe([0xd6, 0xe3, 0x96, 0x61, 0x3a, 0xfd, 0xef, 0x9b, 0x1f, 0x90, 0xa4, 0x24, 0x14, 0x5b, 0xc8, 0xde, - 0x50, 0xb1, 0x1d, 0xaf, 0xe8, 0x55, 0x8a, 0x87, 0x0d, 0xfe, 0xaa, 0x3b, 0x82, 0x2c, 0x8d, 0x7b]) , - fe([0x85, 0x0c, 0xaf, 0xf8, 0x83, 0x44, 0x49, 0xd9, 0x45, 0xcf, 0xf7, 0x48, 0xd9, 0x53, 0xb4, 0xf1, - 0x65, 0xa0, 0xe1, 0xc3, 0xb3, 0x15, 0xed, 0x89, 0x9b, 0x4f, 0x62, 0xb3, 0x57, 0xa5, 0x45, 0x1c])) + aff( + fe([ + 0xd6, 0xe3, 0x96, 0x61, 0x3a, 0xfd, 0xef, 0x9b, 0x1f, 0x90, 0xa4, 0x24, 0x14, 0x5b, 0xc8, 0xde, + 0x50, 0xb1, 0x1d, 0xaf, 0xe8, 0x55, 0x8a, 0x87, 0x0d, 0xfe, 0xaa, 0x3b, 0x82, 0x2c, 0x8d, 0x7b + ]), + fe([ + 0x85, 0x0c, 0xaf, 0xf8, 0x83, 0x44, 0x49, 0xd9, 0x45, 0xcf, 0xf7, 0x48, 0xd9, 0x53, 0xb4, 0xf1, + 0x65, 0xa0, 0xe1, 0xc3, 0xb3, 0x15, 0xed, 0x89, 0x9b, 0x4f, 0x62, 0xb3, 0x57, 0xa5, 0x45, 0x1c + ]) + ) ) v.append( - aff(fe([0x8f, 0x12, 0xea, 0xaf, 0xd1, 0x1f, 0x79, 0x10, 0x0b, 0xf6, 0xa3, 0x7b, 0xea, 0xac, 0x8b, 0x57, - 0x32, 0x62, 0xe7, 0x06, 0x12, 0x51, 0xa0, 0x3b, 0x43, 0x5e, 0xa4, 0x20, 0x78, 0x31, 0xce, 0x0d]) , - fe([0x84, 0x7c, 0xc2, 0xa6, 0x91, 0x23, 0xce, 0xbd, 0xdc, 0xf9, 0xce, 0xd5, 0x75, 0x30, 0x22, 0xe6, - 0xf9, 0x43, 0x62, 0x0d, 0xf7, 0x75, 0x9d, 0x7f, 0x8c, 0xff, 0x7d, 0xe4, 0x72, 0xac, 0x9f, 0x1c])) + aff( + fe([ + 0x8f, 0x12, 0xea, 0xaf, 0xd1, 0x1f, 0x79, 0x10, 0x0b, 0xf6, 0xa3, 0x7b, 0xea, 0xac, 0x8b, 0x57, + 0x32, 0x62, 0xe7, 0x06, 0x12, 0x51, 0xa0, 0x3b, 0x43, 0x5e, 0xa4, 0x20, 0x78, 0x31, 0xce, 0x0d + ]), + fe([ + 0x84, 0x7c, 0xc2, 0xa6, 0x91, 0x23, 0xce, 0xbd, 0xdc, 0xf9, 0xce, 0xd5, 0x75, 0x30, 0x22, 0xe6, + 0xf9, 0x43, 0x62, 0x0d, 0xf7, 0x75, 0x9d, 0x7f, 0x8c, 0xff, 0x7d, 0xe4, 0x72, 0xac, 0x9f, 0x1c + ]) + ) ) v.append( - aff(fe([0x88, 0xc1, 0x99, 0xd0, 0x3c, 0x1c, 0x5d, 0xb4, 0xef, 0x13, 0x0f, 0x90, 0xb9, 0x36, 0x2f, 0x95, - 0x95, 0xc6, 0xdc, 0xde, 0x0a, 0x51, 0xe2, 0x8d, 0xf3, 0xbc, 0x51, 0xec, 0xdf, 0xb1, 0xa2, 0x5f]) , - fe([0x2e, 0x68, 0xa1, 0x23, 0x7d, 0x9b, 0x40, 0x69, 0x85, 0x7b, 0x42, 0xbf, 0x90, 0x4b, 0xd6, 0x40, - 0x2f, 0xd7, 0x52, 0x52, 0xb2, 0x21, 0xde, 0x64, 0xbd, 0x88, 0xc3, 0x6d, 0xa5, 0xfa, 0x81, 0x3f])) + aff( + fe([ + 0x88, 0xc1, 0x99, 0xd0, 0x3c, 0x1c, 0x5d, 0xb4, 0xef, 0x13, 0x0f, 0x90, 0xb9, 0x36, 0x2f, 0x95, + 0x95, 0xc6, 0xdc, 0xde, 0x0a, 0x51, 0xe2, 0x8d, 0xf3, 0xbc, 0x51, 0xec, 0xdf, 0xb1, 0xa2, 0x5f + ]), + fe([ + 0x2e, 0x68, 0xa1, 0x23, 0x7d, 0x9b, 0x40, 0x69, 0x85, 0x7b, 0x42, 0xbf, 0x90, 0x4b, 0xd6, 0x40, + 0x2f, 0xd7, 0x52, 0x52, 0xb2, 0x21, 0xde, 0x64, 0xbd, 0x88, 0xc3, 0x6d, 0xa5, 0xfa, 0x81, 0x3f + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xfb, 0xfd, 0x47, 0x7b, 0x8a, 0x66, 0x9e, 0x79, 0x2e, 0x64, 0x82, 0xef, 0xf7, 0x21, 0xec, 0xf6, - 0xd8, 0x86, 0x09, 0x31, 0x7c, 0xdd, 0x03, 0x6a, 0x58, 0xa0, 0x77, 0xb7, 0x9b, 0x8c, 0x87, 0x1f]) , - fe([0x55, 0x47, 0xe4, 0xa8, 0x3d, 0x55, 0x21, 0x34, 0xab, 0x1d, 0xae, 0xe0, 0xf4, 0xea, 0xdb, 0xc5, - 0xb9, 0x58, 0xbf, 0xc4, 0x2a, 0x89, 0x31, 0x1a, 0xf4, 0x2d, 0xe1, 0xca, 0x37, 0x99, 0x47, 0x59])) + aff( + fe([ + 0xfb, 0xfd, 0x47, 0x7b, 0x8a, 0x66, 0x9e, 0x79, 0x2e, 0x64, 0x82, 0xef, 0xf7, 0x21, 0xec, 0xf6, + 0xd8, 0x86, 0x09, 0x31, 0x7c, 0xdd, 0x03, 0x6a, 0x58, 0xa0, 0x77, 0xb7, 0x9b, 0x8c, 0x87, 0x1f + ]), + fe([ + 0x55, 0x47, 0xe4, 0xa8, 0x3d, 0x55, 0x21, 0x34, 0xab, 0x1d, 0xae, 0xe0, 0xf4, 0xea, 0xdb, 0xc5, + 0xb9, 0x58, 0xbf, 0xc4, 0x2a, 0x89, 0x31, 0x1a, 0xf4, 0x2d, 0xe1, 0xca, 0x37, 0x99, 0x47, 0x59 + ]) + ) ) v.append( - aff(fe([0xc7, 0xca, 0x63, 0xc1, 0x49, 0xa9, 0x35, 0x45, 0x55, 0x7e, 0xda, 0x64, 0x32, 0x07, 0x50, 0xf7, - 0x32, 0xac, 0xde, 0x75, 0x58, 0x9b, 0x11, 0xb2, 0x3a, 0x1f, 0xf5, 0xf7, 0x79, 0x04, 0xe6, 0x08]) , - fe([0x46, 0xfa, 0x22, 0x4b, 0xfa, 0xe1, 0xfe, 0x96, 0xfc, 0x67, 0xba, 0x67, 0x97, 0xc4, 0xe7, 0x1b, - 0x86, 0x90, 0x5f, 0xee, 0xf4, 0x5b, 0x11, 0xb2, 0xcd, 0xad, 0xee, 0xc2, 0x48, 0x6c, 0x2b, 0x1b])) + aff( + fe([ + 0xc7, 0xca, 0x63, 0xc1, 0x49, 0xa9, 0x35, 0x45, 0x55, 0x7e, 0xda, 0x64, 0x32, 0x07, 0x50, 0xf7, + 0x32, 0xac, 0xde, 0x75, 0x58, 0x9b, 0x11, 0xb2, 0x3a, 0x1f, 0xf5, 0xf7, 0x79, 0x04, 0xe6, 0x08 + ]), + fe([ + 0x46, 0xfa, 0x22, 0x4b, 0xfa, 0xe1, 0xfe, 0x96, 0xfc, 0x67, 0xba, 0x67, 0x97, 0xc4, 0xe7, 0x1b, + 0x86, 0x90, 0x5f, 0xee, 0xf4, 0x5b, 0x11, 0xb2, 0xcd, 0xad, 0xee, 0xc2, 0x48, 0x6c, 0x2b, 0x1b + ]) + ) ) v.append( - aff(fe([0xe3, 0x39, 0x62, 0xb4, 0x4f, 0x31, 0x04, 0xc9, 0xda, 0xd5, 0x73, 0x51, 0x57, 0xc5, 0xb8, 0xf3, - 0xa3, 0x43, 0x70, 0xe4, 0x61, 0x81, 0x84, 0xe2, 0xbb, 0xbf, 0x4f, 0x9e, 0xa4, 0x5e, 0x74, 0x06]) , - fe([0x29, 0xac, 0xff, 0x27, 0xe0, 0x59, 0xbe, 0x39, 0x9c, 0x0d, 0x83, 0xd7, 0x10, 0x0b, 0x15, 0xb7, - 0xe1, 0xc2, 0x2c, 0x30, 0x73, 0x80, 0x3a, 0x7d, 0x5d, 0xab, 0x58, 0x6b, 0xc1, 0xf0, 0xf4, 0x22])) + aff( + fe([ + 0xe3, 0x39, 0x62, 0xb4, 0x4f, 0x31, 0x04, 0xc9, 0xda, 0xd5, 0x73, 0x51, 0x57, 0xc5, 0xb8, 0xf3, + 0xa3, 0x43, 0x70, 0xe4, 0x61, 0x81, 0x84, 0xe2, 0xbb, 0xbf, 0x4f, 0x9e, 0xa4, 0x5e, 0x74, 0x06 + ]), + fe([ + 0x29, 0xac, 0xff, 0x27, 0xe0, 0x59, 0xbe, 0x39, 0x9c, 0x0d, 0x83, 0xd7, 0x10, 0x0b, 0x15, 0xb7, + 0xe1, 0xc2, 0x2c, 0x30, 0x73, 0x80, 0x3a, 0x7d, 0x5d, 0xab, 0x58, 0x6b, 0xc1, 0xf0, 0xf4, 0x22 + ]) + ) ) v.append( - aff(fe([0xfe, 0x7f, 0xfb, 0x35, 0x7d, 0xc6, 0x01, 0x23, 0x28, 0xc4, 0x02, 0xac, 0x1f, 0x42, 0xb4, 0x9d, - 0xfc, 0x00, 0x94, 0xa5, 0xee, 0xca, 0xda, 0x97, 0x09, 0x41, 0x77, 0x87, 0x5d, 0x7b, 0x87, 0x78]) , - fe([0xf5, 0xfb, 0x90, 0x2d, 0x81, 0x19, 0x9e, 0x2f, 0x6d, 0x85, 0x88, 0x8c, 0x40, 0x5c, 0x77, 0x41, - 0x4d, 0x01, 0x19, 0x76, 0x60, 0xe8, 0x4c, 0x48, 0xe4, 0x33, 0x83, 0x32, 0x6c, 0xb4, 0x41, 0x03])) + aff( + fe([ + 0xfe, 0x7f, 0xfb, 0x35, 0x7d, 0xc6, 0x01, 0x23, 0x28, 0xc4, 0x02, 0xac, 0x1f, 0x42, 0xb4, 0x9d, + 0xfc, 0x00, 0x94, 0xa5, 0xee, 0xca, 0xda, 0x97, 0x09, 0x41, 0x77, 0x87, 0x5d, 0x7b, 0x87, 0x78 + ]), + fe([ + 0xf5, 0xfb, 0x90, 0x2d, 0x81, 0x19, 0x9e, 0x2f, 0x6d, 0x85, 0x88, 0x8c, 0x40, 0x5c, 0x77, 0x41, + 0x4d, 0x01, 0x19, 0x76, 0x60, 0xe8, 0x4c, 0x48, 0xe4, 0x33, 0x83, 0x32, 0x6c, 0xb4, 0x41, 0x03 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xff, 0x10, 0xc2, 0x09, 0x4f, 0x6e, 0xf4, 0xd2, 0xdf, 0x7e, 0xca, 0x7b, 0x1c, 0x1d, 0xba, 0xa3, - 0xb6, 0xda, 0x67, 0x33, 0xd4, 0x87, 0x36, 0x4b, 0x11, 0x20, 0x05, 0xa6, 0x29, 0xc1, 0x87, 0x17]) , - fe([0xf6, 0x96, 0xca, 0x2f, 0xda, 0x38, 0xa7, 0x1b, 0xfc, 0xca, 0x7d, 0xfe, 0x08, 0x89, 0xe2, 0x47, - 0x2b, 0x6a, 0x5d, 0x4b, 0xfa, 0xa1, 0xb4, 0xde, 0xb6, 0xc2, 0x31, 0x51, 0xf5, 0xe0, 0xa4, 0x0b])) + aff( + fe([ + 0xff, 0x10, 0xc2, 0x09, 0x4f, 0x6e, 0xf4, 0xd2, 0xdf, 0x7e, 0xca, 0x7b, 0x1c, 0x1d, 0xba, 0xa3, + 0xb6, 0xda, 0x67, 0x33, 0xd4, 0x87, 0x36, 0x4b, 0x11, 0x20, 0x05, 0xa6, 0x29, 0xc1, 0x87, 0x17 + ]), + fe([ + 0xf6, 0x96, 0xca, 0x2f, 0xda, 0x38, 0xa7, 0x1b, 0xfc, 0xca, 0x7d, 0xfe, 0x08, 0x89, 0xe2, 0x47, + 0x2b, 0x6a, 0x5d, 0x4b, 0xfa, 0xa1, 0xb4, 0xde, 0xb6, 0xc2, 0x31, 0x51, 0xf5, 0xe0, 0xa4, 0x0b + ]) + ) ) v.append( - aff(fe([0x5c, 0xe5, 0xc6, 0x04, 0x8e, 0x2b, 0x57, 0xbe, 0x38, 0x85, 0x23, 0xcb, 0xb7, 0xbe, 0x4f, 0xa9, - 0xd3, 0x6e, 0x12, 0xaa, 0xd5, 0xb2, 0x2e, 0x93, 0x29, 0x9a, 0x4a, 0x88, 0x18, 0x43, 0xf5, 0x01]) , - fe([0x50, 0xfc, 0xdb, 0xa2, 0x59, 0x21, 0x8d, 0xbd, 0x7e, 0x33, 0xae, 0x2f, 0x87, 0x1a, 0xd0, 0x97, - 0xc7, 0x0d, 0x4d, 0x63, 0x01, 0xef, 0x05, 0x84, 0xec, 0x40, 0xdd, 0xa8, 0x0a, 0x4f, 0x70, 0x0b])) + aff( + fe([ + 0x5c, 0xe5, 0xc6, 0x04, 0x8e, 0x2b, 0x57, 0xbe, 0x38, 0x85, 0x23, 0xcb, 0xb7, 0xbe, 0x4f, 0xa9, + 0xd3, 0x6e, 0x12, 0xaa, 0xd5, 0xb2, 0x2e, 0x93, 0x29, 0x9a, 0x4a, 0x88, 0x18, 0x43, 0xf5, 0x01 + ]), + fe([ + 0x50, 0xfc, 0xdb, 0xa2, 0x59, 0x21, 0x8d, 0xbd, 0x7e, 0x33, 0xae, 0x2f, 0x87, 0x1a, 0xd0, 0x97, + 0xc7, 0x0d, 0x4d, 0x63, 0x01, 0xef, 0x05, 0x84, 0xec, 0x40, 0xdd, 0xa8, 0x0a, 0x4f, 0x70, 0x0b + ]) + ) ) v.append( - aff(fe([0x41, 0x69, 0x01, 0x67, 0x5c, 0xd3, 0x8a, 0xc5, 0xcf, 0x3f, 0xd1, 0x57, 0xd1, 0x67, 0x3e, 0x01, - 0x39, 0xb5, 0xcb, 0x81, 0x56, 0x96, 0x26, 0xb6, 0xc2, 0xe7, 0x5c, 0xfb, 0x63, 0x97, 0x58, 0x06]) , - fe([0x0c, 0x0e, 0xf3, 0xba, 0xf0, 0xe5, 0xba, 0xb2, 0x57, 0x77, 0xc6, 0x20, 0x9b, 0x89, 0x24, 0xbe, - 0xf2, 0x9c, 0x8a, 0xba, 0x69, 0xc1, 0xf1, 0xb0, 0x4f, 0x2a, 0x05, 0x9a, 0xee, 0x10, 0x7e, 0x36])) + aff( + fe([ + 0x41, 0x69, 0x01, 0x67, 0x5c, 0xd3, 0x8a, 0xc5, 0xcf, 0x3f, 0xd1, 0x57, 0xd1, 0x67, 0x3e, 0x01, + 0x39, 0xb5, 0xcb, 0x81, 0x56, 0x96, 0x26, 0xb6, 0xc2, 0xe7, 0x5c, 0xfb, 0x63, 0x97, 0x58, 0x06 + ]), + fe([ + 0x0c, 0x0e, 0xf3, 0xba, 0xf0, 0xe5, 0xba, 0xb2, 0x57, 0x77, 0xc6, 0x20, 0x9b, 0x89, 0x24, 0xbe, + 0xf2, 0x9c, 0x8a, 0xba, 0x69, 0xc1, 0xf1, 0xb0, 0x4f, 0x2a, 0x05, 0x9a, 0xee, 0x10, 0x7e, 0x36 + ]) + ) ) v.append( - aff(fe([0x3f, 0x26, 0xe9, 0x40, 0xe9, 0x03, 0xad, 0x06, 0x69, 0x91, 0xe0, 0xd1, 0x89, 0x60, 0x84, 0x79, - 0xde, 0x27, 0x6d, 0xe6, 0x76, 0xbd, 0xea, 0xe6, 0xae, 0x48, 0xc3, 0x67, 0xc0, 0x57, 0xcd, 0x2f]) , - fe([0x7f, 0xc1, 0xdc, 0xb9, 0xc7, 0xbc, 0x86, 0x3d, 0x55, 0x4b, 0x28, 0x7a, 0xfb, 0x4d, 0xc7, 0xf8, - 0xbc, 0x67, 0x2a, 0x60, 0x4d, 0x8f, 0x07, 0x0b, 0x1a, 0x17, 0xbf, 0xfa, 0xac, 0xa7, 0x3d, 0x1a])) + aff( + fe([ + 0x3f, 0x26, 0xe9, 0x40, 0xe9, 0x03, 0xad, 0x06, 0x69, 0x91, 0xe0, 0xd1, 0x89, 0x60, 0x84, 0x79, + 0xde, 0x27, 0x6d, 0xe6, 0x76, 0xbd, 0xea, 0xe6, 0xae, 0x48, 0xc3, 0x67, 0xc0, 0x57, 0xcd, 0x2f + ]), + fe([ + 0x7f, 0xc1, 0xdc, 0xb9, 0xc7, 0xbc, 0x86, 0x3d, 0x55, 0x4b, 0x28, 0x7a, 0xfb, 0x4d, 0xc7, 0xf8, + 0xbc, 0x67, 0x2a, 0x60, 0x4d, 0x8f, 0x07, 0x0b, 0x1a, 0x17, 0xbf, 0xfa, 0xac, 0xa7, 0x3d, 0x1a + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x91, 0x3f, 0xed, 0x5e, 0x18, 0x78, 0x3f, 0x23, 0x2c, 0x0d, 0x8c, 0x44, 0x00, 0xe8, 0xfb, 0xe9, - 0x8e, 0xd6, 0xd1, 0x36, 0x58, 0x57, 0x9e, 0xae, 0x4b, 0x5c, 0x0b, 0x07, 0xbc, 0x6b, 0x55, 0x2b]) , - fe([0x6f, 0x4d, 0x17, 0xd7, 0xe1, 0x84, 0xd9, 0x78, 0xb1, 0x90, 0xfd, 0x2e, 0xb3, 0xb5, 0x19, 0x3f, - 0x1b, 0xfa, 0xc0, 0x68, 0xb3, 0xdd, 0x00, 0x2e, 0x89, 0xbd, 0x7e, 0x80, 0x32, 0x13, 0xa0, 0x7b])) + aff( + fe([ + 0x91, 0x3f, 0xed, 0x5e, 0x18, 0x78, 0x3f, 0x23, 0x2c, 0x0d, 0x8c, 0x44, 0x00, 0xe8, 0xfb, 0xe9, + 0x8e, 0xd6, 0xd1, 0x36, 0x58, 0x57, 0x9e, 0xae, 0x4b, 0x5c, 0x0b, 0x07, 0xbc, 0x6b, 0x55, 0x2b + ]), + fe([ + 0x6f, 0x4d, 0x17, 0xd7, 0xe1, 0x84, 0xd9, 0x78, 0xb1, 0x90, 0xfd, 0x2e, 0xb3, 0xb5, 0x19, 0x3f, + 0x1b, 0xfa, 0xc0, 0x68, 0xb3, 0xdd, 0x00, 0x2e, 0x89, 0xbd, 0x7e, 0x80, 0x32, 0x13, 0xa0, 0x7b + ]) + ) ) v.append( - aff(fe([0x1a, 0x6f, 0x40, 0xaf, 0x44, 0x44, 0xb0, 0x43, 0x8f, 0x0d, 0xd0, 0x1e, 0xc4, 0x0b, 0x19, 0x5d, - 0x8e, 0xfe, 0xc1, 0xf3, 0xc5, 0x5c, 0x91, 0xf8, 0x04, 0x4e, 0xbe, 0x90, 0xb4, 0x47, 0x5c, 0x3f]) , - fe([0xb0, 0x3b, 0x2c, 0xf3, 0xfe, 0x32, 0x71, 0x07, 0x3f, 0xaa, 0xba, 0x45, 0x60, 0xa8, 0x8d, 0xea, - 0x54, 0xcb, 0x39, 0x10, 0xb4, 0xf2, 0x8b, 0xd2, 0x14, 0x82, 0x42, 0x07, 0x8e, 0xe9, 0x7c, 0x53])) + aff( + fe([ + 0x1a, 0x6f, 0x40, 0xaf, 0x44, 0x44, 0xb0, 0x43, 0x8f, 0x0d, 0xd0, 0x1e, 0xc4, 0x0b, 0x19, 0x5d, + 0x8e, 0xfe, 0xc1, 0xf3, 0xc5, 0x5c, 0x91, 0xf8, 0x04, 0x4e, 0xbe, 0x90, 0xb4, 0x47, 0x5c, 0x3f + ]), + fe([ + 0xb0, 0x3b, 0x2c, 0xf3, 0xfe, 0x32, 0x71, 0x07, 0x3f, 0xaa, 0xba, 0x45, 0x60, 0xa8, 0x8d, 0xea, + 0x54, 0xcb, 0x39, 0x10, 0xb4, 0xf2, 0x8b, 0xd2, 0x14, 0x82, 0x42, 0x07, 0x8e, 0xe9, 0x7c, 0x53 + ]) + ) ) v.append( - aff(fe([0xb0, 0xae, 0xc1, 0x8d, 0xc9, 0x8f, 0xb9, 0x7a, 0x77, 0xef, 0xba, 0x79, 0xa0, 0x3c, 0xa8, 0xf5, - 0x6a, 0xe2, 0x3f, 0x5d, 0x00, 0xe3, 0x4b, 0x45, 0x24, 0x7b, 0x43, 0x78, 0x55, 0x1d, 0x2b, 0x1e]) , - fe([0x01, 0xb8, 0xd6, 0x16, 0x67, 0xa0, 0x15, 0xb9, 0xe1, 0x58, 0xa4, 0xa7, 0x31, 0x37, 0x77, 0x2f, - 0x8b, 0x12, 0x9f, 0xf4, 0x3f, 0xc7, 0x36, 0x66, 0xd2, 0xa8, 0x56, 0xf7, 0x7f, 0x74, 0xc6, 0x41])) + aff( + fe([ + 0xb0, 0xae, 0xc1, 0x8d, 0xc9, 0x8f, 0xb9, 0x7a, 0x77, 0xef, 0xba, 0x79, 0xa0, 0x3c, 0xa8, 0xf5, + 0x6a, 0xe2, 0x3f, 0x5d, 0x00, 0xe3, 0x4b, 0x45, 0x24, 0x7b, 0x43, 0x78, 0x55, 0x1d, 0x2b, 0x1e + ]), + fe([ + 0x01, 0xb8, 0xd6, 0x16, 0x67, 0xa0, 0x15, 0xb9, 0xe1, 0x58, 0xa4, 0xa7, 0x31, 0x37, 0x77, 0x2f, + 0x8b, 0x12, 0x9f, 0xf4, 0x3f, 0xc7, 0x36, 0x66, 0xd2, 0xa8, 0x56, 0xf7, 0x7f, 0x74, 0xc6, 0x41 + ]) + ) ) v.append( - aff(fe([0x5d, 0xf8, 0xb4, 0xa8, 0x30, 0xdd, 0xcc, 0x38, 0xa5, 0xd3, 0xca, 0xd8, 0xd1, 0xf8, 0xb2, 0x31, - 0x91, 0xd4, 0x72, 0x05, 0x57, 0x4a, 0x3b, 0x82, 0x4a, 0xc6, 0x68, 0x20, 0xe2, 0x18, 0x41, 0x61]) , - fe([0x19, 0xd4, 0x8d, 0x47, 0x29, 0x12, 0x65, 0xb0, 0x11, 0x78, 0x47, 0xb5, 0xcb, 0xa3, 0xa5, 0xfa, - 0x05, 0x85, 0x54, 0xa9, 0x33, 0x97, 0x8d, 0x2b, 0xc2, 0xfe, 0x99, 0x35, 0x28, 0xe5, 0xeb, 0x63])) + aff( + fe([ + 0x5d, 0xf8, 0xb4, 0xa8, 0x30, 0xdd, 0xcc, 0x38, 0xa5, 0xd3, 0xca, 0xd8, 0xd1, 0xf8, 0xb2, 0x31, + 0x91, 0xd4, 0x72, 0x05, 0x57, 0x4a, 0x3b, 0x82, 0x4a, 0xc6, 0x68, 0x20, 0xe2, 0x18, 0x41, 0x61 + ]), + fe([ + 0x19, 0xd4, 0x8d, 0x47, 0x29, 0x12, 0x65, 0xb0, 0x11, 0x78, 0x47, 0xb5, 0xcb, 0xa3, 0xa5, 0xfa, + 0x05, 0x85, 0x54, 0xa9, 0x33, 0x97, 0x8d, 0x2b, 0xc2, 0xfe, 0x99, 0x35, 0x28, 0xe5, 0xeb, 0x63 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xb1, 0x3f, 0x3f, 0xef, 0xd8, 0xf4, 0xfc, 0xb3, 0xa0, 0x60, 0x50, 0x06, 0x2b, 0x29, 0x52, 0x70, - 0x15, 0x0b, 0x24, 0x24, 0xf8, 0x5f, 0x79, 0x18, 0xcc, 0xff, 0x89, 0x99, 0x84, 0xa1, 0xae, 0x13]) , - fe([0x44, 0x1f, 0xb8, 0xc2, 0x01, 0xc1, 0x30, 0x19, 0x55, 0x05, 0x60, 0x10, 0xa4, 0x6c, 0x2d, 0x67, - 0x70, 0xe5, 0x25, 0x1b, 0xf2, 0xbf, 0xdd, 0xfb, 0x70, 0x2b, 0xa1, 0x8c, 0x9c, 0x94, 0x84, 0x08])) + aff( + fe([ + 0xb1, 0x3f, 0x3f, 0xef, 0xd8, 0xf4, 0xfc, 0xb3, 0xa0, 0x60, 0x50, 0x06, 0x2b, 0x29, 0x52, 0x70, + 0x15, 0x0b, 0x24, 0x24, 0xf8, 0x5f, 0x79, 0x18, 0xcc, 0xff, 0x89, 0x99, 0x84, 0xa1, 0xae, 0x13 + ]), + fe([ + 0x44, 0x1f, 0xb8, 0xc2, 0x01, 0xc1, 0x30, 0x19, 0x55, 0x05, 0x60, 0x10, 0xa4, 0x6c, 0x2d, 0x67, + 0x70, 0xe5, 0x25, 0x1b, 0xf2, 0xbf, 0xdd, 0xfb, 0x70, 0x2b, 0xa1, 0x8c, 0x9c, 0x94, 0x84, 0x08 + ]) + ) ) v.append( - aff(fe([0xe7, 0xc4, 0x43, 0x4d, 0xc9, 0x2b, 0x69, 0x5d, 0x1d, 0x3c, 0xaf, 0xbb, 0x43, 0x38, 0x4e, 0x98, - 0x3d, 0xed, 0x0d, 0x21, 0x03, 0xfd, 0xf0, 0x99, 0x47, 0x04, 0xb0, 0x98, 0x69, 0x55, 0x72, 0x0f]) , - fe([0x5e, 0xdf, 0x15, 0x53, 0x3b, 0x86, 0x80, 0xb0, 0xf1, 0x70, 0x68, 0x8f, 0x66, 0x7c, 0x0e, 0x49, - 0x1a, 0xd8, 0x6b, 0xfe, 0x4e, 0xef, 0xca, 0x47, 0xd4, 0x03, 0xc1, 0x37, 0x50, 0x9c, 0xc1, 0x16])) + aff( + fe([ + 0xe7, 0xc4, 0x43, 0x4d, 0xc9, 0x2b, 0x69, 0x5d, 0x1d, 0x3c, 0xaf, 0xbb, 0x43, 0x38, 0x4e, 0x98, + 0x3d, 0xed, 0x0d, 0x21, 0x03, 0xfd, 0xf0, 0x99, 0x47, 0x04, 0xb0, 0x98, 0x69, 0x55, 0x72, 0x0f + ]), + fe([ + 0x5e, 0xdf, 0x15, 0x53, 0x3b, 0x86, 0x80, 0xb0, 0xf1, 0x70, 0x68, 0x8f, 0x66, 0x7c, 0x0e, 0x49, + 0x1a, 0xd8, 0x6b, 0xfe, 0x4e, 0xef, 0xca, 0x47, 0xd4, 0x03, 0xc1, 0x37, 0x50, 0x9c, 0xc1, 0x16 + ]) + ) ) v.append( - aff(fe([0xcd, 0x24, 0xc6, 0x3e, 0x0c, 0x82, 0x9b, 0x91, 0x2b, 0x61, 0x4a, 0xb2, 0x0f, 0x88, 0x55, 0x5f, - 0x5a, 0x57, 0xff, 0xe5, 0x74, 0x0b, 0x13, 0x43, 0x00, 0xd8, 0x6b, 0xcf, 0xd2, 0x15, 0x03, 0x2c]) , - fe([0xdc, 0xff, 0x15, 0x61, 0x2f, 0x4a, 0x2f, 0x62, 0xf2, 0x04, 0x2f, 0xb5, 0x0c, 0xb7, 0x1e, 0x3f, - 0x74, 0x1a, 0x0f, 0xd7, 0xea, 0xcd, 0xd9, 0x7d, 0xf6, 0x12, 0x0e, 0x2f, 0xdb, 0x5a, 0x3b, 0x16])) + aff( + fe([ + 0xcd, 0x24, 0xc6, 0x3e, 0x0c, 0x82, 0x9b, 0x91, 0x2b, 0x61, 0x4a, 0xb2, 0x0f, 0x88, 0x55, 0x5f, + 0x5a, 0x57, 0xff, 0xe5, 0x74, 0x0b, 0x13, 0x43, 0x00, 0xd8, 0x6b, 0xcf, 0xd2, 0x15, 0x03, 0x2c + ]), + fe([ + 0xdc, 0xff, 0x15, 0x61, 0x2f, 0x4a, 0x2f, 0x62, 0xf2, 0x04, 0x2f, 0xb5, 0x0c, 0xb7, 0x1e, 0x3f, + 0x74, 0x1a, 0x0f, 0xd7, 0xea, 0xcd, 0xd9, 0x7d, 0xf6, 0x12, 0x0e, 0x2f, 0xdb, 0x5a, 0x3b, 0x16 + ]) + ) ) v.append( - aff(fe([0x1b, 0x37, 0x47, 0xe3, 0xf5, 0x9e, 0xea, 0x2c, 0x2a, 0xe7, 0x82, 0x36, 0xf4, 0x1f, 0x81, 0x47, - 0x92, 0x4b, 0x69, 0x0e, 0x11, 0x8c, 0x5d, 0x53, 0x5b, 0x81, 0x27, 0x08, 0xbc, 0xa0, 0xae, 0x25]) , - fe([0x69, 0x32, 0xa1, 0x05, 0x11, 0x42, 0x00, 0xd2, 0x59, 0xac, 0x4d, 0x62, 0x8b, 0x13, 0xe2, 0x50, - 0x5d, 0xa0, 0x9d, 0x9b, 0xfd, 0xbb, 0x12, 0x41, 0x75, 0x41, 0x9e, 0xcc, 0xdc, 0xc7, 0xdc, 0x5d])) + aff( + fe([ + 0x1b, 0x37, 0x47, 0xe3, 0xf5, 0x9e, 0xea, 0x2c, 0x2a, 0xe7, 0x82, 0x36, 0xf4, 0x1f, 0x81, 0x47, + 0x92, 0x4b, 0x69, 0x0e, 0x11, 0x8c, 0x5d, 0x53, 0x5b, 0x81, 0x27, 0x08, 0xbc, 0xa0, 0xae, 0x25 + ]), + fe([ + 0x69, 0x32, 0xa1, 0x05, 0x11, 0x42, 0x00, 0xd2, 0x59, 0xac, 0x4d, 0x62, 0x8b, 0x13, 0xe2, 0x50, + 0x5d, 0xa0, 0x9d, 0x9b, 0xfd, 0xbb, 0x12, 0x41, 0x75, 0x41, 0x9e, 0xcc, 0xdc, 0xc7, 0xdc, 0x5d + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xd9, 0xe3, 0x38, 0x06, 0x46, 0x70, 0x82, 0x5e, 0x28, 0x49, 0x79, 0xff, 0x25, 0xd2, 0x4e, 0x29, - 0x8d, 0x06, 0xb0, 0x23, 0xae, 0x9b, 0x66, 0xe4, 0x7d, 0xc0, 0x70, 0x91, 0xa3, 0xfc, 0xec, 0x4e]) , - fe([0x62, 0x12, 0x37, 0x6a, 0x30, 0xf6, 0x1e, 0xfb, 0x14, 0x5c, 0x0d, 0x0e, 0xb7, 0x81, 0x6a, 0xe7, - 0x08, 0x05, 0xac, 0xaa, 0x38, 0x46, 0xe2, 0x73, 0xea, 0x4b, 0x07, 0x81, 0x43, 0x7c, 0x9e, 0x5e])) + aff( + fe([ + 0xd9, 0xe3, 0x38, 0x06, 0x46, 0x70, 0x82, 0x5e, 0x28, 0x49, 0x79, 0xff, 0x25, 0xd2, 0x4e, 0x29, + 0x8d, 0x06, 0xb0, 0x23, 0xae, 0x9b, 0x66, 0xe4, 0x7d, 0xc0, 0x70, 0x91, 0xa3, 0xfc, 0xec, 0x4e + ]), + fe([ + 0x62, 0x12, 0x37, 0x6a, 0x30, 0xf6, 0x1e, 0xfb, 0x14, 0x5c, 0x0d, 0x0e, 0xb7, 0x81, 0x6a, 0xe7, + 0x08, 0x05, 0xac, 0xaa, 0x38, 0x46, 0xe2, 0x73, 0xea, 0x4b, 0x07, 0x81, 0x43, 0x7c, 0x9e, 0x5e + ]) + ) ) v.append( - aff(fe([0xfc, 0xf9, 0x21, 0x4f, 0x2e, 0x76, 0x9b, 0x1f, 0x28, 0x60, 0x77, 0x43, 0x32, 0x9d, 0xbe, 0x17, - 0x30, 0x2a, 0xc6, 0x18, 0x92, 0x66, 0x62, 0x30, 0x98, 0x40, 0x11, 0xa6, 0x7f, 0x18, 0x84, 0x28]) , - fe([0x3f, 0xab, 0xd3, 0xf4, 0x8a, 0x76, 0xa1, 0x3c, 0xca, 0x2d, 0x49, 0xc3, 0xea, 0x08, 0x0b, 0x85, - 0x17, 0x2a, 0xc3, 0x6c, 0x08, 0xfd, 0x57, 0x9f, 0x3d, 0x5f, 0xdf, 0x67, 0x68, 0x42, 0x00, 0x32])) + aff( + fe([ + 0xfc, 0xf9, 0x21, 0x4f, 0x2e, 0x76, 0x9b, 0x1f, 0x28, 0x60, 0x77, 0x43, 0x32, 0x9d, 0xbe, 0x17, + 0x30, 0x2a, 0xc6, 0x18, 0x92, 0x66, 0x62, 0x30, 0x98, 0x40, 0x11, 0xa6, 0x7f, 0x18, 0x84, 0x28 + ]), + fe([ + 0x3f, 0xab, 0xd3, 0xf4, 0x8a, 0x76, 0xa1, 0x3c, 0xca, 0x2d, 0x49, 0xc3, 0xea, 0x08, 0x0b, 0x85, + 0x17, 0x2a, 0xc3, 0x6c, 0x08, 0xfd, 0x57, 0x9f, 0x3d, 0x5f, 0xdf, 0x67, 0x68, 0x42, 0x00, 0x32 + ]) + ) ) v.append( - aff(fe([0x51, 0x60, 0x1b, 0x06, 0x4f, 0x8a, 0x21, 0xba, 0x38, 0xa8, 0xba, 0xd6, 0x40, 0xf6, 0xe9, 0x9b, - 0x76, 0x4d, 0x56, 0x21, 0x5b, 0x0a, 0x9b, 0x2e, 0x4f, 0x3d, 0x81, 0x32, 0x08, 0x9f, 0x97, 0x5b]) , - fe([0xe5, 0x44, 0xec, 0x06, 0x9d, 0x90, 0x79, 0x9f, 0xd3, 0xe0, 0x79, 0xaf, 0x8f, 0x10, 0xfd, 0xdd, - 0x04, 0xae, 0x27, 0x97, 0x46, 0x33, 0x79, 0xea, 0xb8, 0x4e, 0xca, 0x5a, 0x59, 0x57, 0xe1, 0x0e])) + aff( + fe([ + 0x51, 0x60, 0x1b, 0x06, 0x4f, 0x8a, 0x21, 0xba, 0x38, 0xa8, 0xba, 0xd6, 0x40, 0xf6, 0xe9, 0x9b, + 0x76, 0x4d, 0x56, 0x21, 0x5b, 0x0a, 0x9b, 0x2e, 0x4f, 0x3d, 0x81, 0x32, 0x08, 0x9f, 0x97, 0x5b + ]), + fe([ + 0xe5, 0x44, 0xec, 0x06, 0x9d, 0x90, 0x79, 0x9f, 0xd3, 0xe0, 0x79, 0xaf, 0x8f, 0x10, 0xfd, 0xdd, + 0x04, 0xae, 0x27, 0x97, 0x46, 0x33, 0x79, 0xea, 0xb8, 0x4e, 0xca, 0x5a, 0x59, 0x57, 0xe1, 0x0e + ]) + ) ) v.append( - aff(fe([0x1a, 0xda, 0xf3, 0xa5, 0x41, 0x43, 0x28, 0xfc, 0x7e, 0xe7, 0x71, 0xea, 0xc6, 0x3b, 0x59, 0xcc, - 0x2e, 0xd3, 0x40, 0xec, 0xb3, 0x13, 0x6f, 0x44, 0xcd, 0x13, 0xb2, 0x37, 0xf2, 0x6e, 0xd9, 0x1c]) , - fe([0xe3, 0xdb, 0x60, 0xcd, 0x5c, 0x4a, 0x18, 0x0f, 0xef, 0x73, 0x36, 0x71, 0x8c, 0xf6, 0x11, 0xb4, - 0xd8, 0xce, 0x17, 0x5e, 0x4f, 0x26, 0x77, 0x97, 0x5f, 0xcb, 0xef, 0x91, 0xeb, 0x6a, 0x62, 0x7a])) + aff( + fe([ + 0x1a, 0xda, 0xf3, 0xa5, 0x41, 0x43, 0x28, 0xfc, 0x7e, 0xe7, 0x71, 0xea, 0xc6, 0x3b, 0x59, 0xcc, + 0x2e, 0xd3, 0x40, 0xec, 0xb3, 0x13, 0x6f, 0x44, 0xcd, 0x13, 0xb2, 0x37, 0xf2, 0x6e, 0xd9, 0x1c + ]), + fe([ + 0xe3, 0xdb, 0x60, 0xcd, 0x5c, 0x4a, 0x18, 0x0f, 0xef, 0x73, 0x36, 0x71, 0x8c, 0xf6, 0x11, 0xb4, + 0xd8, 0xce, 0x17, 0x5e, 0x4f, 0x26, 0x77, 0x97, 0x5f, 0xcb, 0xef, 0x91, 0xeb, 0x6a, 0x62, 0x7a + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x18, 0x4a, 0xa2, 0x97, 0x08, 0x81, 0x2d, 0x83, 0xc4, 0xcc, 0xf0, 0x83, 0x7e, 0xec, 0x0d, 0x95, - 0x4c, 0x5b, 0xfb, 0xfa, 0x98, 0x80, 0x4a, 0x66, 0x56, 0x0c, 0x51, 0xb3, 0xf2, 0x04, 0x5d, 0x27]) , - fe([0x3b, 0xb9, 0xb8, 0x06, 0x5a, 0x2e, 0xfe, 0xc3, 0x82, 0x37, 0x9c, 0xa3, 0x11, 0x1f, 0x9c, 0xa6, - 0xda, 0x63, 0x48, 0x9b, 0xad, 0xde, 0x2d, 0xa6, 0xbc, 0x6e, 0x32, 0xda, 0x27, 0x65, 0xdd, 0x57])) + aff( + fe([ + 0x18, 0x4a, 0xa2, 0x97, 0x08, 0x81, 0x2d, 0x83, 0xc4, 0xcc, 0xf0, 0x83, 0x7e, 0xec, 0x0d, 0x95, + 0x4c, 0x5b, 0xfb, 0xfa, 0x98, 0x80, 0x4a, 0x66, 0x56, 0x0c, 0x51, 0xb3, 0xf2, 0x04, 0x5d, 0x27 + ]), + fe([ + 0x3b, 0xb9, 0xb8, 0x06, 0x5a, 0x2e, 0xfe, 0xc3, 0x82, 0x37, 0x9c, 0xa3, 0x11, 0x1f, 0x9c, 0xa6, + 0xda, 0x63, 0x48, 0x9b, 0xad, 0xde, 0x2d, 0xa6, 0xbc, 0x6e, 0x32, 0xda, 0x27, 0x65, 0xdd, 0x57 + ]) + ) ) v.append( - aff(fe([0x84, 0x4f, 0x37, 0x31, 0x7d, 0x2e, 0xbc, 0xad, 0x87, 0x07, 0x2a, 0x6b, 0x37, 0xfc, 0x5f, 0xeb, - 0x4e, 0x75, 0x35, 0xa6, 0xde, 0xab, 0x0a, 0x19, 0x3a, 0xb7, 0xb1, 0xef, 0x92, 0x6a, 0x3b, 0x3c]) , - fe([0x3b, 0xb2, 0x94, 0x6d, 0x39, 0x60, 0xac, 0xee, 0xe7, 0x81, 0x1a, 0x3b, 0x76, 0x87, 0x5c, 0x05, - 0x94, 0x2a, 0x45, 0xb9, 0x80, 0xe9, 0x22, 0xb1, 0x07, 0xcb, 0x40, 0x9e, 0x70, 0x49, 0x6d, 0x12])) + aff( + fe([ + 0x84, 0x4f, 0x37, 0x31, 0x7d, 0x2e, 0xbc, 0xad, 0x87, 0x07, 0x2a, 0x6b, 0x37, 0xfc, 0x5f, 0xeb, + 0x4e, 0x75, 0x35, 0xa6, 0xde, 0xab, 0x0a, 0x19, 0x3a, 0xb7, 0xb1, 0xef, 0x92, 0x6a, 0x3b, 0x3c + ]), + fe([ + 0x3b, 0xb2, 0x94, 0x6d, 0x39, 0x60, 0xac, 0xee, 0xe7, 0x81, 0x1a, 0x3b, 0x76, 0x87, 0x5c, 0x05, + 0x94, 0x2a, 0x45, 0xb9, 0x80, 0xe9, 0x22, 0xb1, 0x07, 0xcb, 0x40, 0x9e, 0x70, 0x49, 0x6d, 0x12 + ]) + ) ) v.append( - aff(fe([0xfd, 0x18, 0x78, 0x84, 0xa8, 0x4c, 0x7d, 0x6e, 0x59, 0xa6, 0xe5, 0x74, 0xf1, 0x19, 0xa6, 0x84, - 0x2e, 0x51, 0xc1, 0x29, 0x13, 0xf2, 0x14, 0x6b, 0x5d, 0x53, 0x51, 0xf7, 0xef, 0xbf, 0x01, 0x22]) , - fe([0xa4, 0x4b, 0x62, 0x4c, 0xe6, 0xfd, 0x72, 0x07, 0xf2, 0x81, 0xfc, 0xf2, 0xbd, 0x12, 0x7c, 0x68, - 0x76, 0x2a, 0xba, 0xf5, 0x65, 0xb1, 0x1f, 0x17, 0x0a, 0x38, 0xb0, 0xbf, 0xc0, 0xf8, 0xf4, 0x2a])) + aff( + fe([ + 0xfd, 0x18, 0x78, 0x84, 0xa8, 0x4c, 0x7d, 0x6e, 0x59, 0xa6, 0xe5, 0x74, 0xf1, 0x19, 0xa6, 0x84, + 0x2e, 0x51, 0xc1, 0x29, 0x13, 0xf2, 0x14, 0x6b, 0x5d, 0x53, 0x51, 0xf7, 0xef, 0xbf, 0x01, 0x22 + ]), + fe([ + 0xa4, 0x4b, 0x62, 0x4c, 0xe6, 0xfd, 0x72, 0x07, 0xf2, 0x81, 0xfc, 0xf2, 0xbd, 0x12, 0x7c, 0x68, + 0x76, 0x2a, 0xba, 0xf5, 0x65, 0xb1, 0x1f, 0x17, 0x0a, 0x38, 0xb0, 0xbf, 0xc0, 0xf8, 0xf4, 0x2a + ]) + ) ) v.append( - aff(fe([0x55, 0x60, 0x55, 0x5b, 0xe4, 0x1d, 0x71, 0x4c, 0x9d, 0x5b, 0x9f, 0x70, 0xa6, 0x85, 0x9a, 0x2c, - 0xa0, 0xe2, 0x32, 0x48, 0xce, 0x9e, 0x2a, 0xa5, 0x07, 0x3b, 0xc7, 0x6c, 0x86, 0x77, 0xde, 0x3c]) , - fe([0xf7, 0x18, 0x7a, 0x96, 0x7e, 0x43, 0x57, 0xa9, 0x55, 0xfc, 0x4e, 0xb6, 0x72, 0x00, 0xf2, 0xe4, - 0xd7, 0x52, 0xd3, 0xd3, 0xb6, 0x85, 0xf6, 0x71, 0xc7, 0x44, 0x3f, 0x7f, 0xd7, 0xb3, 0xf2, 0x79])) + aff( + fe([ + 0x55, 0x60, 0x55, 0x5b, 0xe4, 0x1d, 0x71, 0x4c, 0x9d, 0x5b, 0x9f, 0x70, 0xa6, 0x85, 0x9a, 0x2c, + 0xa0, 0xe2, 0x32, 0x48, 0xce, 0x9e, 0x2a, 0xa5, 0x07, 0x3b, 0xc7, 0x6c, 0x86, 0x77, 0xde, 0x3c + ]), + fe([ + 0xf7, 0x18, 0x7a, 0x96, 0x7e, 0x43, 0x57, 0xa9, 0x55, 0xfc, 0x4e, 0xb6, 0x72, 0x00, 0xf2, 0xe4, + 0xd7, 0x52, 0xd3, 0xd3, 0xb6, 0x85, 0xf6, 0x71, 0xc7, 0x44, 0x3f, 0x7f, 0xd7, 0xb3, 0xf2, 0x79 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x46, 0xca, 0xa7, 0x55, 0x7b, 0x79, 0xf3, 0xca, 0x5a, 0x65, 0xf6, 0xed, 0x50, 0x14, 0x7b, 0xe4, - 0xc4, 0x2a, 0x65, 0x9e, 0xe2, 0xf9, 0xca, 0xa7, 0x22, 0x26, 0x53, 0xcb, 0x21, 0x5b, 0xa7, 0x31]) , - fe([0x90, 0xd7, 0xc5, 0x26, 0x08, 0xbd, 0xb0, 0x53, 0x63, 0x58, 0xc3, 0x31, 0x5e, 0x75, 0x46, 0x15, - 0x91, 0xa6, 0xf8, 0x2f, 0x1a, 0x08, 0x65, 0x88, 0x2f, 0x98, 0x04, 0xf1, 0x7c, 0x6e, 0x00, 0x77])) + aff( + fe([ + 0x46, 0xca, 0xa7, 0x55, 0x7b, 0x79, 0xf3, 0xca, 0x5a, 0x65, 0xf6, 0xed, 0x50, 0x14, 0x7b, 0xe4, + 0xc4, 0x2a, 0x65, 0x9e, 0xe2, 0xf9, 0xca, 0xa7, 0x22, 0x26, 0x53, 0xcb, 0x21, 0x5b, 0xa7, 0x31 + ]), + fe([ + 0x90, 0xd7, 0xc5, 0x26, 0x08, 0xbd, 0xb0, 0x53, 0x63, 0x58, 0xc3, 0x31, 0x5e, 0x75, 0x46, 0x15, + 0x91, 0xa6, 0xf8, 0x2f, 0x1a, 0x08, 0x65, 0x88, 0x2f, 0x98, 0x04, 0xf1, 0x7c, 0x6e, 0x00, 0x77 + ]) + ) ) v.append( - aff(fe([0x81, 0x21, 0x61, 0x09, 0xf6, 0x4e, 0xf1, 0x92, 0xee, 0x63, 0x61, 0x73, 0x87, 0xc7, 0x54, 0x0e, - 0x42, 0x4b, 0xc9, 0x47, 0xd1, 0xb8, 0x7e, 0x91, 0x75, 0x37, 0x99, 0x28, 0xb8, 0xdd, 0x7f, 0x50]) , - fe([0x89, 0x8f, 0xc0, 0xbe, 0x5d, 0xd6, 0x9f, 0xa0, 0xf0, 0x9d, 0x81, 0xce, 0x3a, 0x7b, 0x98, 0x58, - 0xbb, 0xd7, 0x78, 0xc8, 0x3f, 0x13, 0xf1, 0x74, 0x19, 0xdf, 0xf8, 0x98, 0x89, 0x5d, 0xfa, 0x5f])) + aff( + fe([ + 0x81, 0x21, 0x61, 0x09, 0xf6, 0x4e, 0xf1, 0x92, 0xee, 0x63, 0x61, 0x73, 0x87, 0xc7, 0x54, 0x0e, + 0x42, 0x4b, 0xc9, 0x47, 0xd1, 0xb8, 0x7e, 0x91, 0x75, 0x37, 0x99, 0x28, 0xb8, 0xdd, 0x7f, 0x50 + ]), + fe([ + 0x89, 0x8f, 0xc0, 0xbe, 0x5d, 0xd6, 0x9f, 0xa0, 0xf0, 0x9d, 0x81, 0xce, 0x3a, 0x7b, 0x98, 0x58, + 0xbb, 0xd7, 0x78, 0xc8, 0x3f, 0x13, 0xf1, 0x74, 0x19, 0xdf, 0xf8, 0x98, 0x89, 0x5d, 0xfa, 0x5f + ]) + ) ) v.append( - aff(fe([0x9e, 0x35, 0x85, 0x94, 0x47, 0x1f, 0x90, 0x15, 0x26, 0xd0, 0x84, 0xed, 0x8a, 0x80, 0xf7, 0x63, - 0x42, 0x86, 0x27, 0xd7, 0xf4, 0x75, 0x58, 0xdc, 0x9c, 0xc0, 0x22, 0x7e, 0x20, 0x35, 0xfd, 0x1f]) , - fe([0x68, 0x0e, 0x6f, 0x97, 0xba, 0x70, 0xbb, 0xa3, 0x0e, 0xe5, 0x0b, 0x12, 0xf4, 0xa2, 0xdc, 0x47, - 0xf8, 0xe6, 0xd0, 0x23, 0x6c, 0x33, 0xa8, 0x99, 0x46, 0x6e, 0x0f, 0x44, 0xba, 0x76, 0x48, 0x0f])) + aff( + fe([ + 0x9e, 0x35, 0x85, 0x94, 0x47, 0x1f, 0x90, 0x15, 0x26, 0xd0, 0x84, 0xed, 0x8a, 0x80, 0xf7, 0x63, + 0x42, 0x86, 0x27, 0xd7, 0xf4, 0x75, 0x58, 0xdc, 0x9c, 0xc0, 0x22, 0x7e, 0x20, 0x35, 0xfd, 0x1f + ]), + fe([ + 0x68, 0x0e, 0x6f, 0x97, 0xba, 0x70, 0xbb, 0xa3, 0x0e, 0xe5, 0x0b, 0x12, 0xf4, 0xa2, 0xdc, 0x47, + 0xf8, 0xe6, 0xd0, 0x23, 0x6c, 0x33, 0xa8, 0x99, 0x46, 0x6e, 0x0f, 0x44, 0xba, 0x76, 0x48, 0x0f + ]) + ) ) v.append( - aff(fe([0xa3, 0x2a, 0x61, 0x37, 0xe2, 0x59, 0x12, 0x0e, 0x27, 0xba, 0x64, 0x43, 0xae, 0xc0, 0x42, 0x69, - 0x79, 0xa4, 0x1e, 0x29, 0x8b, 0x15, 0xeb, 0xf8, 0xaf, 0xd4, 0xa2, 0x68, 0x33, 0xb5, 0x7a, 0x24]) , - fe([0x2c, 0x19, 0x33, 0xdd, 0x1b, 0xab, 0xec, 0x01, 0xb0, 0x23, 0xf8, 0x42, 0x2b, 0x06, 0x88, 0xea, - 0x3d, 0x2d, 0x00, 0x2a, 0x78, 0x45, 0x4d, 0x38, 0xed, 0x2e, 0x2e, 0x44, 0x49, 0xed, 0xcb, 0x33])) + aff( + fe([ + 0xa3, 0x2a, 0x61, 0x37, 0xe2, 0x59, 0x12, 0x0e, 0x27, 0xba, 0x64, 0x43, 0xae, 0xc0, 0x42, 0x69, + 0x79, 0xa4, 0x1e, 0x29, 0x8b, 0x15, 0xeb, 0xf8, 0xaf, 0xd4, 0xa2, 0x68, 0x33, 0xb5, 0x7a, 0x24 + ]), + fe([ + 0x2c, 0x19, 0x33, 0xdd, 0x1b, 0xab, 0xec, 0x01, 0xb0, 0x23, 0xf8, 0x42, 0x2b, 0x06, 0x88, 0xea, + 0x3d, 0x2d, 0x00, 0x2a, 0x78, 0x45, 0x4d, 0x38, 0xed, 0x2e, 0x2e, 0x44, 0x49, 0xed, 0xcb, 0x33 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xa0, 0x68, 0xe8, 0x41, 0x8f, 0x91, 0xf8, 0x11, 0x13, 0x90, 0x2e, 0xa7, 0xab, 0x30, 0xef, 0xad, - 0xa0, 0x61, 0x00, 0x88, 0xef, 0xdb, 0xce, 0x5b, 0x5c, 0xbb, 0x62, 0xc8, 0x56, 0xf9, 0x00, 0x73]) , - fe([0x3f, 0x60, 0xc1, 0x82, 0x2d, 0xa3, 0x28, 0x58, 0x24, 0x9e, 0x9f, 0xe3, 0x70, 0xcc, 0x09, 0x4e, - 0x1a, 0x3f, 0x11, 0x11, 0x15, 0x07, 0x3c, 0xa4, 0x41, 0xe0, 0x65, 0xa3, 0x0a, 0x41, 0x6d, 0x11])) + aff( + fe([ + 0xa0, 0x68, 0xe8, 0x41, 0x8f, 0x91, 0xf8, 0x11, 0x13, 0x90, 0x2e, 0xa7, 0xab, 0x30, 0xef, 0xad, + 0xa0, 0x61, 0x00, 0x88, 0xef, 0xdb, 0xce, 0x5b, 0x5c, 0xbb, 0x62, 0xc8, 0x56, 0xf9, 0x00, 0x73 + ]), + fe([ + 0x3f, 0x60, 0xc1, 0x82, 0x2d, 0xa3, 0x28, 0x58, 0x24, 0x9e, 0x9f, 0xe3, 0x70, 0xcc, 0x09, 0x4e, + 0x1a, 0x3f, 0x11, 0x11, 0x15, 0x07, 0x3c, 0xa4, 0x41, 0xe0, 0x65, 0xa3, 0x0a, 0x41, 0x6d, 0x11 + ]) + ) ) v.append( - aff(fe([0x31, 0x40, 0x01, 0x52, 0x56, 0x94, 0x5b, 0x28, 0x8a, 0xaa, 0x52, 0xee, 0xd8, 0x0a, 0x05, 0x8d, - 0xcd, 0xb5, 0xaa, 0x2e, 0x38, 0xaa, 0xb7, 0x87, 0xf7, 0x2b, 0xfb, 0x04, 0xcb, 0x84, 0x3d, 0x54]) , - fe([0x20, 0xef, 0x59, 0xde, 0xa4, 0x2b, 0x93, 0x6e, 0x2e, 0xec, 0x42, 0x9a, 0xd4, 0x2d, 0xf4, 0x46, - 0x58, 0x27, 0x2b, 0x18, 0x8f, 0x83, 0x3d, 0x69, 0x9e, 0xd4, 0x3e, 0xb6, 0xc5, 0xfd, 0x58, 0x03])) + aff( + fe([ + 0x31, 0x40, 0x01, 0x52, 0x56, 0x94, 0x5b, 0x28, 0x8a, 0xaa, 0x52, 0xee, 0xd8, 0x0a, 0x05, 0x8d, + 0xcd, 0xb5, 0xaa, 0x2e, 0x38, 0xaa, 0xb7, 0x87, 0xf7, 0x2b, 0xfb, 0x04, 0xcb, 0x84, 0x3d, 0x54 + ]), + fe([ + 0x20, 0xef, 0x59, 0xde, 0xa4, 0x2b, 0x93, 0x6e, 0x2e, 0xec, 0x42, 0x9a, 0xd4, 0x2d, 0xf4, 0x46, + 0x58, 0x27, 0x2b, 0x18, 0x8f, 0x83, 0x3d, 0x69, 0x9e, 0xd4, 0x3e, 0xb6, 0xc5, 0xfd, 0x58, 0x03 + ]) + ) ) v.append( - aff(fe([0x33, 0x89, 0xc9, 0x63, 0x62, 0x1c, 0x17, 0xb4, 0x60, 0xc4, 0x26, 0x68, 0x09, 0xc3, 0x2e, 0x37, - 0x0f, 0x7b, 0xb4, 0x9c, 0xb6, 0xf9, 0xfb, 0xd4, 0x51, 0x78, 0xc8, 0x63, 0xea, 0x77, 0x47, 0x07]) , - fe([0x32, 0xb4, 0x18, 0x47, 0x79, 0xcb, 0xd4, 0x5a, 0x07, 0x14, 0x0f, 0xa0, 0xd5, 0xac, 0xd0, 0x41, - 0x40, 0xab, 0x61, 0x23, 0xe5, 0x2a, 0x2a, 0x6f, 0xf7, 0xa8, 0xd4, 0x76, 0xef, 0xe7, 0x45, 0x6c])) + aff( + fe([ + 0x33, 0x89, 0xc9, 0x63, 0x62, 0x1c, 0x17, 0xb4, 0x60, 0xc4, 0x26, 0x68, 0x09, 0xc3, 0x2e, 0x37, + 0x0f, 0x7b, 0xb4, 0x9c, 0xb6, 0xf9, 0xfb, 0xd4, 0x51, 0x78, 0xc8, 0x63, 0xea, 0x77, 0x47, 0x07 + ]), + fe([ + 0x32, 0xb4, 0x18, 0x47, 0x79, 0xcb, 0xd4, 0x5a, 0x07, 0x14, 0x0f, 0xa0, 0xd5, 0xac, 0xd0, 0x41, + 0x40, 0xab, 0x61, 0x23, 0xe5, 0x2a, 0x2a, 0x6f, 0xf7, 0xa8, 0xd4, 0x76, 0xef, 0xe7, 0x45, 0x6c + ]) + ) ) v.append( - aff(fe([0xa1, 0x5e, 0x60, 0x4f, 0xfb, 0xe1, 0x70, 0x6a, 0x1f, 0x55, 0x4f, 0x09, 0xb4, 0x95, 0x33, 0x36, - 0xc6, 0x81, 0x01, 0x18, 0x06, 0x25, 0x27, 0xa4, 0xb4, 0x24, 0xa4, 0x86, 0x03, 0x4c, 0xac, 0x02]) , - fe([0x77, 0x38, 0xde, 0xd7, 0x60, 0x48, 0x07, 0xf0, 0x74, 0xa8, 0xff, 0x54, 0xe5, 0x30, 0x43, 0xff, - 0x77, 0xfb, 0x21, 0x07, 0xff, 0xb2, 0x07, 0x6b, 0xe4, 0xe5, 0x30, 0xfc, 0x19, 0x6c, 0xa3, 0x01])) + aff( + fe([ + 0xa1, 0x5e, 0x60, 0x4f, 0xfb, 0xe1, 0x70, 0x6a, 0x1f, 0x55, 0x4f, 0x09, 0xb4, 0x95, 0x33, 0x36, + 0xc6, 0x81, 0x01, 0x18, 0x06, 0x25, 0x27, 0xa4, 0xb4, 0x24, 0xa4, 0x86, 0x03, 0x4c, 0xac, 0x02 + ]), + fe([ + 0x77, 0x38, 0xde, 0xd7, 0x60, 0x48, 0x07, 0xf0, 0x74, 0xa8, 0xff, 0x54, 0xe5, 0x30, 0x43, 0xff, + 0x77, 0xfb, 0x21, 0x07, 0xff, 0xb2, 0x07, 0x6b, 0xe4, 0xe5, 0x30, 0xfc, 0x19, 0x6c, 0xa3, 0x01 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x13, 0xc5, 0x2c, 0xac, 0xd3, 0x83, 0x82, 0x7c, 0x29, 0xf7, 0x05, 0xa5, 0x00, 0xb6, 0x1f, 0x86, - 0x55, 0xf4, 0xd6, 0x2f, 0x0c, 0x99, 0xd0, 0x65, 0x9b, 0x6b, 0x46, 0x0d, 0x43, 0xf8, 0x16, 0x28]) , - fe([0x1e, 0x7f, 0xb4, 0x74, 0x7e, 0xb1, 0x89, 0x4f, 0x18, 0x5a, 0xab, 0x64, 0x06, 0xdf, 0x45, 0x87, - 0xe0, 0x6a, 0xc6, 0xf0, 0x0e, 0xc9, 0x24, 0x35, 0x38, 0xea, 0x30, 0x54, 0xb4, 0xc4, 0x52, 0x54])) + aff( + fe([ + 0x13, 0xc5, 0x2c, 0xac, 0xd3, 0x83, 0x82, 0x7c, 0x29, 0xf7, 0x05, 0xa5, 0x00, 0xb6, 0x1f, 0x86, + 0x55, 0xf4, 0xd6, 0x2f, 0x0c, 0x99, 0xd0, 0x65, 0x9b, 0x6b, 0x46, 0x0d, 0x43, 0xf8, 0x16, 0x28 + ]), + fe([ + 0x1e, 0x7f, 0xb4, 0x74, 0x7e, 0xb1, 0x89, 0x4f, 0x18, 0x5a, 0xab, 0x64, 0x06, 0xdf, 0x45, 0x87, + 0xe0, 0x6a, 0xc6, 0xf0, 0x0e, 0xc9, 0x24, 0x35, 0x38, 0xea, 0x30, 0x54, 0xb4, 0xc4, 0x52, 0x54 + ]) + ) ) v.append( - aff(fe([0xe9, 0x9f, 0xdc, 0x3f, 0xc1, 0x89, 0x44, 0x74, 0x27, 0xe4, 0xc1, 0x90, 0xff, 0x4a, 0xa7, 0x3c, - 0xee, 0xcd, 0xf4, 0x1d, 0x25, 0x94, 0x7f, 0x63, 0x16, 0x48, 0xbc, 0x64, 0xfe, 0x95, 0xc4, 0x0c]) , - fe([0x8b, 0x19, 0x75, 0x6e, 0x03, 0x06, 0x5e, 0x6a, 0x6f, 0x1a, 0x8c, 0xe3, 0xd3, 0x28, 0xf2, 0xe0, - 0xb9, 0x7a, 0x43, 0x69, 0xe6, 0xd3, 0xc0, 0xfe, 0x7e, 0x97, 0xab, 0x6c, 0x7b, 0x8e, 0x13, 0x42])) + aff( + fe([ + 0xe9, 0x9f, 0xdc, 0x3f, 0xc1, 0x89, 0x44, 0x74, 0x27, 0xe4, 0xc1, 0x90, 0xff, 0x4a, 0xa7, 0x3c, + 0xee, 0xcd, 0xf4, 0x1d, 0x25, 0x94, 0x7f, 0x63, 0x16, 0x48, 0xbc, 0x64, 0xfe, 0x95, 0xc4, 0x0c + ]), + fe([ + 0x8b, 0x19, 0x75, 0x6e, 0x03, 0x06, 0x5e, 0x6a, 0x6f, 0x1a, 0x8c, 0xe3, 0xd3, 0x28, 0xf2, 0xe0, + 0xb9, 0x7a, 0x43, 0x69, 0xe6, 0xd3, 0xc0, 0xfe, 0x7e, 0x97, 0xab, 0x6c, 0x7b, 0x8e, 0x13, 0x42 + ]) + ) ) v.append( - aff(fe([0xd4, 0xca, 0x70, 0x3d, 0xab, 0xfb, 0x5f, 0x5e, 0x00, 0x0c, 0xcc, 0x77, 0x22, 0xf8, 0x78, 0x55, - 0xae, 0x62, 0x35, 0xfb, 0x9a, 0xc6, 0x03, 0xe4, 0x0c, 0xee, 0xab, 0xc7, 0xc0, 0x89, 0x87, 0x54]) , - fe([0x32, 0xad, 0xae, 0x85, 0x58, 0x43, 0xb8, 0xb1, 0xe6, 0x3e, 0x00, 0x9c, 0x78, 0x88, 0x56, 0xdb, - 0x9c, 0xfc, 0x79, 0xf6, 0xf9, 0x41, 0x5f, 0xb7, 0xbc, 0x11, 0xf9, 0x20, 0x36, 0x1c, 0x53, 0x2b])) + aff( + fe([ + 0xd4, 0xca, 0x70, 0x3d, 0xab, 0xfb, 0x5f, 0x5e, 0x00, 0x0c, 0xcc, 0x77, 0x22, 0xf8, 0x78, 0x55, + 0xae, 0x62, 0x35, 0xfb, 0x9a, 0xc6, 0x03, 0xe4, 0x0c, 0xee, 0xab, 0xc7, 0xc0, 0x89, 0x87, 0x54 + ]), + fe([ + 0x32, 0xad, 0xae, 0x85, 0x58, 0x43, 0xb8, 0xb1, 0xe6, 0x3e, 0x00, 0x9c, 0x78, 0x88, 0x56, 0xdb, + 0x9c, 0xfc, 0x79, 0xf6, 0xf9, 0x41, 0x5f, 0xb7, 0xbc, 0x11, 0xf9, 0x20, 0x36, 0x1c, 0x53, 0x2b + ]) + ) ) v.append( - aff(fe([0x5a, 0x20, 0x5b, 0xa1, 0xa5, 0x44, 0x91, 0x24, 0x02, 0x63, 0x12, 0x64, 0xb8, 0x55, 0xf6, 0xde, - 0x2c, 0xdb, 0x47, 0xb8, 0xc6, 0x0a, 0xc3, 0x00, 0x78, 0x93, 0xd8, 0xf5, 0xf5, 0x18, 0x28, 0x0a]) , - fe([0xd6, 0x1b, 0x9a, 0x6c, 0xe5, 0x46, 0xea, 0x70, 0x96, 0x8d, 0x4e, 0x2a, 0x52, 0x21, 0x26, 0x4b, - 0xb1, 0xbb, 0x0f, 0x7c, 0xa9, 0x9b, 0x04, 0xbb, 0x51, 0x08, 0xf1, 0x9a, 0xa4, 0x76, 0x7c, 0x18])) + aff( + fe([ + 0x5a, 0x20, 0x5b, 0xa1, 0xa5, 0x44, 0x91, 0x24, 0x02, 0x63, 0x12, 0x64, 0xb8, 0x55, 0xf6, 0xde, + 0x2c, 0xdb, 0x47, 0xb8, 0xc6, 0x0a, 0xc3, 0x00, 0x78, 0x93, 0xd8, 0xf5, 0xf5, 0x18, 0x28, 0x0a + ]), + fe([ + 0xd6, 0x1b, 0x9a, 0x6c, 0xe5, 0x46, 0xea, 0x70, 0x96, 0x8d, 0x4e, 0x2a, 0x52, 0x21, 0x26, 0x4b, + 0xb1, 0xbb, 0x0f, 0x7c, 0xa9, 0x9b, 0x04, 0xbb, 0x51, 0x08, 0xf1, 0x9a, 0xa4, 0x76, 0x7c, 0x18 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xfa, 0x94, 0xf7, 0x40, 0xd0, 0xd7, 0xeb, 0xa9, 0x82, 0x36, 0xd5, 0x15, 0xb9, 0x33, 0x7a, 0xbf, - 0x8a, 0xf2, 0x63, 0xaa, 0x37, 0xf5, 0x59, 0xac, 0xbd, 0xbb, 0x32, 0x36, 0xbe, 0x73, 0x99, 0x38]) , - fe([0x2c, 0xb3, 0xda, 0x7a, 0xd8, 0x3d, 0x99, 0xca, 0xd2, 0xf4, 0xda, 0x99, 0x8e, 0x4f, 0x98, 0xb7, - 0xf4, 0xae, 0x3e, 0x9f, 0x8e, 0x35, 0x60, 0xa4, 0x33, 0x75, 0xa4, 0x04, 0x93, 0xb1, 0x6b, 0x4d])) + aff( + fe([ + 0xfa, 0x94, 0xf7, 0x40, 0xd0, 0xd7, 0xeb, 0xa9, 0x82, 0x36, 0xd5, 0x15, 0xb9, 0x33, 0x7a, 0xbf, + 0x8a, 0xf2, 0x63, 0xaa, 0x37, 0xf5, 0x59, 0xac, 0xbd, 0xbb, 0x32, 0x36, 0xbe, 0x73, 0x99, 0x38 + ]), + fe([ + 0x2c, 0xb3, 0xda, 0x7a, 0xd8, 0x3d, 0x99, 0xca, 0xd2, 0xf4, 0xda, 0x99, 0x8e, 0x4f, 0x98, 0xb7, + 0xf4, 0xae, 0x3e, 0x9f, 0x8e, 0x35, 0x60, 0xa4, 0x33, 0x75, 0xa4, 0x04, 0x93, 0xb1, 0x6b, 0x4d + ]) + ) ) v.append( - aff(fe([0x97, 0x9d, 0xa8, 0xcd, 0x97, 0x7b, 0x9d, 0xb9, 0xe7, 0xa5, 0xef, 0xfd, 0xa8, 0x42, 0x6b, 0xc3, - 0x62, 0x64, 0x7d, 0xa5, 0x1b, 0xc9, 0x9e, 0xd2, 0x45, 0xb9, 0xee, 0x03, 0xb0, 0xbf, 0xc0, 0x68]) , - fe([0xed, 0xb7, 0x84, 0x2c, 0xf6, 0xd3, 0xa1, 0x6b, 0x24, 0x6d, 0x87, 0x56, 0x97, 0x59, 0x79, 0x62, - 0x9f, 0xac, 0xed, 0xf3, 0xc9, 0x89, 0x21, 0x2e, 0x04, 0xb3, 0xcc, 0x2f, 0xbe, 0xd6, 0x0a, 0x4b])) + aff( + fe([ + 0x97, 0x9d, 0xa8, 0xcd, 0x97, 0x7b, 0x9d, 0xb9, 0xe7, 0xa5, 0xef, 0xfd, 0xa8, 0x42, 0x6b, 0xc3, + 0x62, 0x64, 0x7d, 0xa5, 0x1b, 0xc9, 0x9e, 0xd2, 0x45, 0xb9, 0xee, 0x03, 0xb0, 0xbf, 0xc0, 0x68 + ]), + fe([ + 0xed, 0xb7, 0x84, 0x2c, 0xf6, 0xd3, 0xa1, 0x6b, 0x24, 0x6d, 0x87, 0x56, 0x97, 0x59, 0x79, 0x62, + 0x9f, 0xac, 0xed, 0xf3, 0xc9, 0x89, 0x21, 0x2e, 0x04, 0xb3, 0xcc, 0x2f, 0xbe, 0xd6, 0x0a, 0x4b + ]) + ) ) v.append( - aff(fe([0x39, 0x61, 0x05, 0xed, 0x25, 0x89, 0x8b, 0x5d, 0x1b, 0xcb, 0x0c, 0x55, 0xf4, 0x6a, 0x00, 0x8a, - 0x46, 0xe8, 0x1e, 0xc6, 0x83, 0xc8, 0x5a, 0x76, 0xdb, 0xcc, 0x19, 0x7a, 0xcc, 0x67, 0x46, 0x0b]) , - fe([0x53, 0xcf, 0xc2, 0xa1, 0xad, 0x6a, 0xf3, 0xcd, 0x8f, 0xc9, 0xde, 0x1c, 0xf8, 0x6c, 0x8f, 0xf8, - 0x76, 0x42, 0xe7, 0xfe, 0xb2, 0x72, 0x21, 0x0a, 0x66, 0x74, 0x8f, 0xb7, 0xeb, 0xe4, 0x6f, 0x01])) + aff( + fe([ + 0x39, 0x61, 0x05, 0xed, 0x25, 0x89, 0x8b, 0x5d, 0x1b, 0xcb, 0x0c, 0x55, 0xf4, 0x6a, 0x00, 0x8a, + 0x46, 0xe8, 0x1e, 0xc6, 0x83, 0xc8, 0x5a, 0x76, 0xdb, 0xcc, 0x19, 0x7a, 0xcc, 0x67, 0x46, 0x0b + ]), + fe([ + 0x53, 0xcf, 0xc2, 0xa1, 0xad, 0x6a, 0xf3, 0xcd, 0x8f, 0xc9, 0xde, 0x1c, 0xf8, 0x6c, 0x8f, 0xf8, + 0x76, 0x42, 0xe7, 0xfe, 0xb2, 0x72, 0x21, 0x0a, 0x66, 0x74, 0x8f, 0xb7, 0xeb, 0xe4, 0x6f, 0x01 + ]) + ) ) v.append( - aff(fe([0x22, 0x8c, 0x6b, 0xbe, 0xfc, 0x4d, 0x70, 0x62, 0x6e, 0x52, 0x77, 0x99, 0x88, 0x7e, 0x7b, 0x57, - 0x7a, 0x0d, 0xfe, 0xdc, 0x72, 0x92, 0xf1, 0x68, 0x1d, 0x97, 0xd7, 0x7c, 0x8d, 0x53, 0x10, 0x37]) , - fe([0x53, 0x88, 0x77, 0x02, 0xca, 0x27, 0xa8, 0xe5, 0x45, 0xe2, 0xa8, 0x48, 0x2a, 0xab, 0x18, 0xca, - 0xea, 0x2d, 0x2a, 0x54, 0x17, 0x37, 0x32, 0x09, 0xdc, 0xe0, 0x4a, 0xb7, 0x7d, 0x82, 0x10, 0x7d])) + aff( + fe([ + 0x22, 0x8c, 0x6b, 0xbe, 0xfc, 0x4d, 0x70, 0x62, 0x6e, 0x52, 0x77, 0x99, 0x88, 0x7e, 0x7b, 0x57, + 0x7a, 0x0d, 0xfe, 0xdc, 0x72, 0x92, 0xf1, 0x68, 0x1d, 0x97, 0xd7, 0x7c, 0x8d, 0x53, 0x10, 0x37 + ]), + fe([ + 0x53, 0x88, 0x77, 0x02, 0xca, 0x27, 0xa8, 0xe5, 0x45, 0xe2, 0xa8, 0x48, 0x2a, 0xab, 0x18, 0xca, + 0xea, 0x2d, 0x2a, 0x54, 0x17, 0x37, 0x32, 0x09, 0xdc, 0xe0, 0x4a, 0xb7, 0x7d, 0x82, 0x10, 0x7d + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x8a, 0x64, 0x1e, 0x14, 0x0a, 0x57, 0xd4, 0xda, 0x5c, 0x96, 0x9b, 0x01, 0x4c, 0x67, 0xbf, 0x8b, - 0x30, 0xfe, 0x08, 0xdb, 0x0d, 0xd5, 0xa8, 0xd7, 0x09, 0x11, 0x85, 0xa2, 0xd3, 0x45, 0xfb, 0x7e]) , - fe([0xda, 0x8c, 0xc2, 0xd0, 0xac, 0x18, 0xe8, 0x52, 0x36, 0xd4, 0x21, 0xa3, 0xdd, 0x57, 0x22, 0x79, - 0xb7, 0xf8, 0x71, 0x9d, 0xc6, 0x91, 0x70, 0x86, 0x56, 0xbf, 0xa1, 0x11, 0x8b, 0x19, 0xe1, 0x0f])) + aff( + fe([ + 0x8a, 0x64, 0x1e, 0x14, 0x0a, 0x57, 0xd4, 0xda, 0x5c, 0x96, 0x9b, 0x01, 0x4c, 0x67, 0xbf, 0x8b, + 0x30, 0xfe, 0x08, 0xdb, 0x0d, 0xd5, 0xa8, 0xd7, 0x09, 0x11, 0x85, 0xa2, 0xd3, 0x45, 0xfb, 0x7e + ]), + fe([ + 0xda, 0x8c, 0xc2, 0xd0, 0xac, 0x18, 0xe8, 0x52, 0x36, 0xd4, 0x21, 0xa3, 0xdd, 0x57, 0x22, 0x79, + 0xb7, 0xf8, 0x71, 0x9d, 0xc6, 0x91, 0x70, 0x86, 0x56, 0xbf, 0xa1, 0x11, 0x8b, 0x19, 0xe1, 0x0f + ]) + ) ) v.append( - aff(fe([0x18, 0x32, 0x98, 0x2c, 0x8f, 0x91, 0xae, 0x12, 0xf0, 0x8c, 0xea, 0xf3, 0x3c, 0xb9, 0x5d, 0xe4, - 0x69, 0xed, 0xb2, 0x47, 0x18, 0xbd, 0xce, 0x16, 0x52, 0x5c, 0x23, 0xe2, 0xa5, 0x25, 0x52, 0x5d]) , - fe([0xb9, 0xb1, 0xe7, 0x5d, 0x4e, 0xbc, 0xee, 0xbb, 0x40, 0x81, 0x77, 0x82, 0x19, 0xab, 0xb5, 0xc6, - 0xee, 0xab, 0x5b, 0x6b, 0x63, 0x92, 0x8a, 0x34, 0x8d, 0xcd, 0xee, 0x4f, 0x49, 0xe5, 0xc9, 0x7e])) + aff( + fe([ + 0x18, 0x32, 0x98, 0x2c, 0x8f, 0x91, 0xae, 0x12, 0xf0, 0x8c, 0xea, 0xf3, 0x3c, 0xb9, 0x5d, 0xe4, + 0x69, 0xed, 0xb2, 0x47, 0x18, 0xbd, 0xce, 0x16, 0x52, 0x5c, 0x23, 0xe2, 0xa5, 0x25, 0x52, 0x5d + ]), + fe([ + 0xb9, 0xb1, 0xe7, 0x5d, 0x4e, 0xbc, 0xee, 0xbb, 0x40, 0x81, 0x77, 0x82, 0x19, 0xab, 0xb5, 0xc6, + 0xee, 0xab, 0x5b, 0x6b, 0x63, 0x92, 0x8a, 0x34, 0x8d, 0xcd, 0xee, 0x4f, 0x49, 0xe5, 0xc9, 0x7e + ]) + ) ) v.append( - aff(fe([0x21, 0xac, 0x8b, 0x22, 0xcd, 0xc3, 0x9a, 0xe9, 0x5e, 0x78, 0xbd, 0xde, 0xba, 0xad, 0xab, 0xbf, - 0x75, 0x41, 0x09, 0xc5, 0x58, 0xa4, 0x7d, 0x92, 0xb0, 0x7f, 0xf2, 0xa1, 0xd1, 0xc0, 0xb3, 0x6d]) , - fe([0x62, 0x4f, 0xd0, 0x75, 0x77, 0xba, 0x76, 0x77, 0xd7, 0xb8, 0xd8, 0x92, 0x6f, 0x98, 0x34, 0x3d, - 0xd6, 0x4e, 0x1c, 0x0f, 0xf0, 0x8f, 0x2e, 0xf1, 0xb3, 0xbd, 0xb1, 0xb9, 0xec, 0x99, 0xb4, 0x07])) + aff( + fe([ + 0x21, 0xac, 0x8b, 0x22, 0xcd, 0xc3, 0x9a, 0xe9, 0x5e, 0x78, 0xbd, 0xde, 0xba, 0xad, 0xab, 0xbf, + 0x75, 0x41, 0x09, 0xc5, 0x58, 0xa4, 0x7d, 0x92, 0xb0, 0x7f, 0xf2, 0xa1, 0xd1, 0xc0, 0xb3, 0x6d + ]), + fe([ + 0x62, 0x4f, 0xd0, 0x75, 0x77, 0xba, 0x76, 0x77, 0xd7, 0xb8, 0xd8, 0x92, 0x6f, 0x98, 0x34, 0x3d, + 0xd6, 0x4e, 0x1c, 0x0f, 0xf0, 0x8f, 0x2e, 0xf1, 0xb3, 0xbd, 0xb1, 0xb9, 0xec, 0x99, 0xb4, 0x07 + ]) + ) ) v.append( - aff(fe([0x60, 0x57, 0x2e, 0x9a, 0x72, 0x1d, 0x6b, 0x6e, 0x58, 0x33, 0x24, 0x8c, 0x48, 0x39, 0x46, 0x8e, - 0x89, 0x6a, 0x88, 0x51, 0x23, 0x62, 0xb5, 0x32, 0x09, 0x36, 0xe3, 0x57, 0xf5, 0x98, 0xde, 0x6f]) , - fe([0x8b, 0x2c, 0x00, 0x48, 0x4a, 0xf9, 0x5b, 0x87, 0x69, 0x52, 0xe5, 0x5b, 0xd1, 0xb1, 0xe5, 0x25, - 0x25, 0xe0, 0x9c, 0xc2, 0x13, 0x44, 0xe8, 0xb9, 0x0a, 0x70, 0xad, 0xbd, 0x0f, 0x51, 0x94, 0x69])) + aff( + fe([ + 0x60, 0x57, 0x2e, 0x9a, 0x72, 0x1d, 0x6b, 0x6e, 0x58, 0x33, 0x24, 0x8c, 0x48, 0x39, 0x46, 0x8e, + 0x89, 0x6a, 0x88, 0x51, 0x23, 0x62, 0xb5, 0x32, 0x09, 0x36, 0xe3, 0x57, 0xf5, 0x98, 0xde, 0x6f + ]), + fe([ + 0x8b, 0x2c, 0x00, 0x48, 0x4a, 0xf9, 0x5b, 0x87, 0x69, 0x52, 0xe5, 0x5b, 0xd1, 0xb1, 0xe5, 0x25, + 0x25, 0xe0, 0x9c, 0xc2, 0x13, 0x44, 0xe8, 0xb9, 0x0a, 0x70, 0xad, 0xbd, 0x0f, 0x51, 0x94, 0x69 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xa2, 0xdc, 0xab, 0xa9, 0x25, 0x2d, 0xac, 0x5f, 0x03, 0x33, 0x08, 0xe7, 0x7e, 0xfe, 0x95, 0x36, - 0x3c, 0x5b, 0x3a, 0xd3, 0x05, 0x82, 0x1c, 0x95, 0x2d, 0xd8, 0x77, 0x7e, 0x02, 0xd9, 0x5b, 0x70]) , - fe([0xc2, 0xfe, 0x1b, 0x0c, 0x67, 0xcd, 0xd6, 0xe0, 0x51, 0x8e, 0x2c, 0xe0, 0x79, 0x88, 0xf0, 0xcf, - 0x41, 0x4a, 0xad, 0x23, 0xd4, 0x46, 0xca, 0x94, 0xa1, 0xc3, 0xeb, 0x28, 0x06, 0xfa, 0x17, 0x14])) + aff( + fe([ + 0xa2, 0xdc, 0xab, 0xa9, 0x25, 0x2d, 0xac, 0x5f, 0x03, 0x33, 0x08, 0xe7, 0x7e, 0xfe, 0x95, 0x36, + 0x3c, 0x5b, 0x3a, 0xd3, 0x05, 0x82, 0x1c, 0x95, 0x2d, 0xd8, 0x77, 0x7e, 0x02, 0xd9, 0x5b, 0x70 + ]), + fe([ + 0xc2, 0xfe, 0x1b, 0x0c, 0x67, 0xcd, 0xd6, 0xe0, 0x51, 0x8e, 0x2c, 0xe0, 0x79, 0x88, 0xf0, 0xcf, + 0x41, 0x4a, 0xad, 0x23, 0xd4, 0x46, 0xca, 0x94, 0xa1, 0xc3, 0xeb, 0x28, 0x06, 0xfa, 0x17, 0x14 + ]) + ) ) v.append( - aff(fe([0x7b, 0xaa, 0x70, 0x0a, 0x4b, 0xfb, 0xf5, 0xbf, 0x80, 0xc5, 0xcf, 0x08, 0x7a, 0xdd, 0xa1, 0xf4, - 0x9d, 0x54, 0x50, 0x53, 0x23, 0x77, 0x23, 0xf5, 0x34, 0xa5, 0x22, 0xd1, 0x0d, 0x96, 0x2e, 0x47]) , - fe([0xcc, 0xb7, 0x32, 0x89, 0x57, 0xd0, 0x98, 0x75, 0xe4, 0x37, 0x99, 0xa9, 0xe8, 0xba, 0xed, 0xba, - 0xeb, 0xc7, 0x4f, 0x15, 0x76, 0x07, 0x0c, 0x4c, 0xef, 0x9f, 0x52, 0xfc, 0x04, 0x5d, 0x58, 0x10])) + aff( + fe([ + 0x7b, 0xaa, 0x70, 0x0a, 0x4b, 0xfb, 0xf5, 0xbf, 0x80, 0xc5, 0xcf, 0x08, 0x7a, 0xdd, 0xa1, 0xf4, + 0x9d, 0x54, 0x50, 0x53, 0x23, 0x77, 0x23, 0xf5, 0x34, 0xa5, 0x22, 0xd1, 0x0d, 0x96, 0x2e, 0x47 + ]), + fe([ + 0xcc, 0xb7, 0x32, 0x89, 0x57, 0xd0, 0x98, 0x75, 0xe4, 0x37, 0x99, 0xa9, 0xe8, 0xba, 0xed, 0xba, + 0xeb, 0xc7, 0x4f, 0x15, 0x76, 0x07, 0x0c, 0x4c, 0xef, 0x9f, 0x52, 0xfc, 0x04, 0x5d, 0x58, 0x10 + ]) + ) ) v.append( - aff(fe([0xce, 0x82, 0xf0, 0x8f, 0x79, 0x02, 0xa8, 0xd1, 0xda, 0x14, 0x09, 0x48, 0xee, 0x8a, 0x40, 0x98, - 0x76, 0x60, 0x54, 0x5a, 0xde, 0x03, 0x24, 0xf5, 0xe6, 0x2f, 0xe1, 0x03, 0xbf, 0x68, 0x82, 0x7f]) , - fe([0x64, 0xe9, 0x28, 0xc7, 0xa4, 0xcf, 0x2a, 0xf9, 0x90, 0x64, 0x72, 0x2c, 0x8b, 0xeb, 0xec, 0xa0, - 0xf2, 0x7d, 0x35, 0xb5, 0x90, 0x4d, 0x7f, 0x5b, 0x4a, 0x49, 0xe4, 0xb8, 0x3b, 0xc8, 0xa1, 0x2f])) + aff( + fe([ + 0xce, 0x82, 0xf0, 0x8f, 0x79, 0x02, 0xa8, 0xd1, 0xda, 0x14, 0x09, 0x48, 0xee, 0x8a, 0x40, 0x98, + 0x76, 0x60, 0x54, 0x5a, 0xde, 0x03, 0x24, 0xf5, 0xe6, 0x2f, 0xe1, 0x03, 0xbf, 0x68, 0x82, 0x7f + ]), + fe([ + 0x64, 0xe9, 0x28, 0xc7, 0xa4, 0xcf, 0x2a, 0xf9, 0x90, 0x64, 0x72, 0x2c, 0x8b, 0xeb, 0xec, 0xa0, + 0xf2, 0x7d, 0x35, 0xb5, 0x90, 0x4d, 0x7f, 0x5b, 0x4a, 0x49, 0xe4, 0xb8, 0x3b, 0xc8, 0xa1, 0x2f + ]) + ) ) v.append( - aff(fe([0x8b, 0xc5, 0xcc, 0x3d, 0x69, 0xa6, 0xa1, 0x18, 0x44, 0xbc, 0x4d, 0x77, 0x37, 0xc7, 0x86, 0xec, - 0x0c, 0xc9, 0xd6, 0x44, 0xa9, 0x23, 0x27, 0xb9, 0x03, 0x34, 0xa7, 0x0a, 0xd5, 0xc7, 0x34, 0x37]) , - fe([0xf9, 0x7e, 0x3e, 0x66, 0xee, 0xf9, 0x99, 0x28, 0xff, 0xad, 0x11, 0xd8, 0xe2, 0x66, 0xc5, 0xcd, - 0x0f, 0x0d, 0x0b, 0x6a, 0xfc, 0x7c, 0x24, 0xa8, 0x4f, 0xa8, 0x5e, 0x80, 0x45, 0x8b, 0x6c, 0x41])) + aff( + fe([ + 0x8b, 0xc5, 0xcc, 0x3d, 0x69, 0xa6, 0xa1, 0x18, 0x44, 0xbc, 0x4d, 0x77, 0x37, 0xc7, 0x86, 0xec, + 0x0c, 0xc9, 0xd6, 0x44, 0xa9, 0x23, 0x27, 0xb9, 0x03, 0x34, 0xa7, 0x0a, 0xd5, 0xc7, 0x34, 0x37 + ]), + fe([ + 0xf9, 0x7e, 0x3e, 0x66, 0xee, 0xf9, 0x99, 0x28, 0xff, 0xad, 0x11, 0xd8, 0xe2, 0x66, 0xc5, 0xcd, + 0x0f, 0x0d, 0x0b, 0x6a, 0xfc, 0x7c, 0x24, 0xa8, 0x4f, 0xa8, 0x5e, 0x80, 0x45, 0x8b, 0x6c, 0x41 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xef, 0x1e, 0xec, 0xf7, 0x8d, 0x77, 0xf2, 0xea, 0xdb, 0x60, 0x03, 0x21, 0xc0, 0xff, 0x5e, 0x67, - 0xc3, 0x71, 0x0b, 0x21, 0xb4, 0x41, 0xa0, 0x68, 0x38, 0xc6, 0x01, 0xa3, 0xd3, 0x51, 0x3c, 0x3c]) , - fe([0x92, 0xf8, 0xd6, 0x4b, 0xef, 0x42, 0x13, 0xb2, 0x4a, 0xc4, 0x2e, 0x72, 0x3f, 0xc9, 0x11, 0xbd, - 0x74, 0x02, 0x0e, 0xf5, 0x13, 0x9d, 0x83, 0x1a, 0x1b, 0xd5, 0x54, 0xde, 0xc4, 0x1e, 0x16, 0x6c])) + aff( + fe([ + 0xef, 0x1e, 0xec, 0xf7, 0x8d, 0x77, 0xf2, 0xea, 0xdb, 0x60, 0x03, 0x21, 0xc0, 0xff, 0x5e, 0x67, + 0xc3, 0x71, 0x0b, 0x21, 0xb4, 0x41, 0xa0, 0x68, 0x38, 0xc6, 0x01, 0xa3, 0xd3, 0x51, 0x3c, 0x3c + ]), + fe([ + 0x92, 0xf8, 0xd6, 0x4b, 0xef, 0x42, 0x13, 0xb2, 0x4a, 0xc4, 0x2e, 0x72, 0x3f, 0xc9, 0x11, 0xbd, + 0x74, 0x02, 0x0e, 0xf5, 0x13, 0x9d, 0x83, 0x1a, 0x1b, 0xd5, 0x54, 0xde, 0xc4, 0x1e, 0x16, 0x6c + ]) + ) ) v.append( - aff(fe([0x27, 0x52, 0xe4, 0x63, 0xaa, 0x94, 0xe6, 0xc3, 0x28, 0x9c, 0xc6, 0x56, 0xac, 0xfa, 0xb6, 0xbd, - 0xe2, 0xcc, 0x76, 0xc6, 0x27, 0x27, 0xa2, 0x8e, 0x78, 0x2b, 0x84, 0x72, 0x10, 0xbd, 0x4e, 0x2a]) , - fe([0xea, 0xa7, 0x23, 0xef, 0x04, 0x61, 0x80, 0x50, 0xc9, 0x6e, 0xa5, 0x96, 0xd1, 0xd1, 0xc8, 0xc3, - 0x18, 0xd7, 0x2d, 0xfd, 0x26, 0xbd, 0xcb, 0x7b, 0x92, 0x51, 0x0e, 0x4a, 0x65, 0x57, 0xb8, 0x49])) + aff( + fe([ + 0x27, 0x52, 0xe4, 0x63, 0xaa, 0x94, 0xe6, 0xc3, 0x28, 0x9c, 0xc6, 0x56, 0xac, 0xfa, 0xb6, 0xbd, + 0xe2, 0xcc, 0x76, 0xc6, 0x27, 0x27, 0xa2, 0x8e, 0x78, 0x2b, 0x84, 0x72, 0x10, 0xbd, 0x4e, 0x2a + ]), + fe([ + 0xea, 0xa7, 0x23, 0xef, 0x04, 0x61, 0x80, 0x50, 0xc9, 0x6e, 0xa5, 0x96, 0xd1, 0xd1, 0xc8, 0xc3, + 0x18, 0xd7, 0x2d, 0xfd, 0x26, 0xbd, 0xcb, 0x7b, 0x92, 0x51, 0x0e, 0x4a, 0x65, 0x57, 0xb8, 0x49 + ]) + ) ) v.append( - aff(fe([0xab, 0x55, 0x36, 0xc3, 0xec, 0x63, 0x55, 0x11, 0x55, 0xf6, 0xa5, 0xc7, 0x01, 0x5f, 0xfe, 0x79, - 0xd8, 0x0a, 0xf7, 0x03, 0xd8, 0x98, 0x99, 0xf5, 0xd0, 0x00, 0x54, 0x6b, 0x66, 0x28, 0xf5, 0x25]) , - fe([0x7a, 0x8d, 0xa1, 0x5d, 0x70, 0x5d, 0x51, 0x27, 0xee, 0x30, 0x65, 0x56, 0x95, 0x46, 0xde, 0xbd, - 0x03, 0x75, 0xb4, 0x57, 0x59, 0x89, 0xeb, 0x02, 0x9e, 0xcc, 0x89, 0x19, 0xa7, 0xcb, 0x17, 0x67])) + aff( + fe([ + 0xab, 0x55, 0x36, 0xc3, 0xec, 0x63, 0x55, 0x11, 0x55, 0xf6, 0xa5, 0xc7, 0x01, 0x5f, 0xfe, 0x79, + 0xd8, 0x0a, 0xf7, 0x03, 0xd8, 0x98, 0x99, 0xf5, 0xd0, 0x00, 0x54, 0x6b, 0x66, 0x28, 0xf5, 0x25 + ]), + fe([ + 0x7a, 0x8d, 0xa1, 0x5d, 0x70, 0x5d, 0x51, 0x27, 0xee, 0x30, 0x65, 0x56, 0x95, 0x46, 0xde, 0xbd, + 0x03, 0x75, 0xb4, 0x57, 0x59, 0x89, 0xeb, 0x02, 0x9e, 0xcc, 0x89, 0x19, 0xa7, 0xcb, 0x17, 0x67 + ]) + ) ) v.append( - aff(fe([0x6a, 0xeb, 0xfc, 0x9a, 0x9a, 0x10, 0xce, 0xdb, 0x3a, 0x1c, 0x3c, 0x6a, 0x9d, 0xea, 0x46, 0xbc, - 0x45, 0x49, 0xac, 0xe3, 0x41, 0x12, 0x7c, 0xf0, 0xf7, 0x4f, 0xf9, 0xf7, 0xff, 0x2c, 0x89, 0x04]) , - fe([0x30, 0x31, 0x54, 0x1a, 0x46, 0xca, 0xe6, 0xc6, 0xcb, 0xe2, 0xc3, 0xc1, 0x8b, 0x75, 0x81, 0xbe, - 0xee, 0xf8, 0xa3, 0x11, 0x1c, 0x25, 0xa3, 0xa7, 0x35, 0x51, 0x55, 0xe2, 0x25, 0xaa, 0xe2, 0x3a])) + aff( + fe([ + 0x6a, 0xeb, 0xfc, 0x9a, 0x9a, 0x10, 0xce, 0xdb, 0x3a, 0x1c, 0x3c, 0x6a, 0x9d, 0xea, 0x46, 0xbc, + 0x45, 0x49, 0xac, 0xe3, 0x41, 0x12, 0x7c, 0xf0, 0xf7, 0x4f, 0xf9, 0xf7, 0xff, 0x2c, 0x89, 0x04 + ]), + fe([ + 0x30, 0x31, 0x54, 0x1a, 0x46, 0xca, 0xe6, 0xc6, 0xcb, 0xe2, 0xc3, 0xc1, 0x8b, 0x75, 0x81, 0xbe, + 0xee, 0xf8, 0xa3, 0x11, 0x1c, 0x25, 0xa3, 0xa7, 0x35, 0x51, 0x55, 0xe2, 0x25, 0xaa, 0xe2, 0x3a + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xb4, 0x48, 0x10, 0x9f, 0x8a, 0x09, 0x76, 0xfa, 0xf0, 0x7a, 0xb0, 0x70, 0xf7, 0x83, 0x80, 0x52, - 0x84, 0x2b, 0x26, 0xa2, 0xc4, 0x5d, 0x4f, 0xba, 0xb1, 0xc8, 0x40, 0x0d, 0x78, 0x97, 0xc4, 0x60]) , - fe([0xd4, 0xb1, 0x6c, 0x08, 0xc7, 0x40, 0x38, 0x73, 0x5f, 0x0b, 0xf3, 0x76, 0x5d, 0xb2, 0xa5, 0x2f, - 0x57, 0x57, 0x07, 0xed, 0x08, 0xa2, 0x6c, 0x4f, 0x08, 0x02, 0xb5, 0x0e, 0xee, 0x44, 0xfa, 0x22])) + aff( + fe([ + 0xb4, 0x48, 0x10, 0x9f, 0x8a, 0x09, 0x76, 0xfa, 0xf0, 0x7a, 0xb0, 0x70, 0xf7, 0x83, 0x80, 0x52, + 0x84, 0x2b, 0x26, 0xa2, 0xc4, 0x5d, 0x4f, 0xba, 0xb1, 0xc8, 0x40, 0x0d, 0x78, 0x97, 0xc4, 0x60 + ]), + fe([ + 0xd4, 0xb1, 0x6c, 0x08, 0xc7, 0x40, 0x38, 0x73, 0x5f, 0x0b, 0xf3, 0x76, 0x5d, 0xb2, 0xa5, 0x2f, + 0x57, 0x57, 0x07, 0xed, 0x08, 0xa2, 0x6c, 0x4f, 0x08, 0x02, 0xb5, 0x0e, 0xee, 0x44, 0xfa, 0x22 + ]) + ) ) v.append( - aff(fe([0x0f, 0x00, 0x3f, 0xa6, 0x04, 0x19, 0x56, 0x65, 0x31, 0x7f, 0x8b, 0xeb, 0x0d, 0xe1, 0x47, 0x89, - 0x97, 0x16, 0x53, 0xfa, 0x81, 0xa7, 0xaa, 0xb2, 0xbf, 0x67, 0xeb, 0x72, 0x60, 0x81, 0x0d, 0x48]) , - fe([0x7e, 0x13, 0x33, 0xcd, 0xa8, 0x84, 0x56, 0x1e, 0x67, 0xaf, 0x6b, 0x43, 0xac, 0x17, 0xaf, 0x16, - 0xc0, 0x52, 0x99, 0x49, 0x5b, 0x87, 0x73, 0x7e, 0xb5, 0x43, 0xda, 0x6b, 0x1d, 0x0f, 0x2d, 0x55])) + aff( + fe([ + 0x0f, 0x00, 0x3f, 0xa6, 0x04, 0x19, 0x56, 0x65, 0x31, 0x7f, 0x8b, 0xeb, 0x0d, 0xe1, 0x47, 0x89, + 0x97, 0x16, 0x53, 0xfa, 0x81, 0xa7, 0xaa, 0xb2, 0xbf, 0x67, 0xeb, 0x72, 0x60, 0x81, 0x0d, 0x48 + ]), + fe([ + 0x7e, 0x13, 0x33, 0xcd, 0xa8, 0x84, 0x56, 0x1e, 0x67, 0xaf, 0x6b, 0x43, 0xac, 0x17, 0xaf, 0x16, + 0xc0, 0x52, 0x99, 0x49, 0x5b, 0x87, 0x73, 0x7e, 0xb5, 0x43, 0xda, 0x6b, 0x1d, 0x0f, 0x2d, 0x55 + ]) + ) ) v.append( - aff(fe([0xe9, 0x58, 0x1f, 0xff, 0x84, 0x3f, 0x93, 0x1c, 0xcb, 0xe1, 0x30, 0x69, 0xa5, 0x75, 0x19, 0x7e, - 0x14, 0x5f, 0xf8, 0xfc, 0x09, 0xdd, 0xa8, 0x78, 0x9d, 0xca, 0x59, 0x8b, 0xd1, 0x30, 0x01, 0x13]) , - fe([0xff, 0x76, 0x03, 0xc5, 0x4b, 0x89, 0x99, 0x70, 0x00, 0x59, 0x70, 0x9c, 0xd5, 0xd9, 0x11, 0x89, - 0x5a, 0x46, 0xfe, 0xef, 0xdc, 0xd9, 0x55, 0x2b, 0x45, 0xa7, 0xb0, 0x2d, 0xfb, 0x24, 0xc2, 0x29])) + aff( + fe([ + 0xe9, 0x58, 0x1f, 0xff, 0x84, 0x3f, 0x93, 0x1c, 0xcb, 0xe1, 0x30, 0x69, 0xa5, 0x75, 0x19, 0x7e, + 0x14, 0x5f, 0xf8, 0xfc, 0x09, 0xdd, 0xa8, 0x78, 0x9d, 0xca, 0x59, 0x8b, 0xd1, 0x30, 0x01, 0x13 + ]), + fe([ + 0xff, 0x76, 0x03, 0xc5, 0x4b, 0x89, 0x99, 0x70, 0x00, 0x59, 0x70, 0x9c, 0xd5, 0xd9, 0x11, 0x89, + 0x5a, 0x46, 0xfe, 0xef, 0xdc, 0xd9, 0x55, 0x2b, 0x45, 0xa7, 0xb0, 0x2d, 0xfb, 0x24, 0xc2, 0x29 + ]) + ) ) v.append( - aff(fe([0x38, 0x06, 0xf8, 0x0b, 0xac, 0x82, 0xc4, 0x97, 0x2b, 0x90, 0xe0, 0xf7, 0xa8, 0xab, 0x6c, 0x08, - 0x80, 0x66, 0x90, 0x46, 0xf7, 0x26, 0x2d, 0xf8, 0xf1, 0xc4, 0x6b, 0x4a, 0x82, 0x98, 0x8e, 0x37]) , - fe([0x8e, 0xb4, 0xee, 0xb8, 0xd4, 0x3f, 0xb2, 0x1b, 0xe0, 0x0a, 0x3d, 0x75, 0x34, 0x28, 0xa2, 0x8e, - 0xc4, 0x92, 0x7b, 0xfe, 0x60, 0x6e, 0x6d, 0xb8, 0x31, 0x1d, 0x62, 0x0d, 0x78, 0x14, 0x42, 0x11])) + aff( + fe([ + 0x38, 0x06, 0xf8, 0x0b, 0xac, 0x82, 0xc4, 0x97, 0x2b, 0x90, 0xe0, 0xf7, 0xa8, 0xab, 0x6c, 0x08, + 0x80, 0x66, 0x90, 0x46, 0xf7, 0x26, 0x2d, 0xf8, 0xf1, 0xc4, 0x6b, 0x4a, 0x82, 0x98, 0x8e, 0x37 + ]), + fe([ + 0x8e, 0xb4, 0xee, 0xb8, 0xd4, 0x3f, 0xb2, 0x1b, 0xe0, 0x0a, 0x3d, 0x75, 0x34, 0x28, 0xa2, 0x8e, + 0xc4, 0x92, 0x7b, 0xfe, 0x60, 0x6e, 0x6d, 0xb8, 0x31, 0x1d, 0x62, 0x0d, 0x78, 0x14, 0x42, 0x11 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x5e, 0xa8, 0xd8, 0x04, 0x9b, 0x73, 0xc9, 0xc9, 0xdc, 0x0d, 0x73, 0xbf, 0x0a, 0x0a, 0x73, 0xff, - 0x18, 0x1f, 0x9c, 0x51, 0xaa, 0xc6, 0xf1, 0x83, 0x25, 0xfd, 0xab, 0xa3, 0x11, 0xd3, 0x01, 0x24]) , - fe([0x4d, 0xe3, 0x7e, 0x38, 0x62, 0x5e, 0x64, 0xbb, 0x2b, 0x53, 0xb5, 0x03, 0x68, 0xc4, 0xf2, 0x2b, - 0x5a, 0x03, 0x32, 0x99, 0x4a, 0x41, 0x9a, 0xe1, 0x1a, 0xae, 0x8c, 0x48, 0xf3, 0x24, 0x32, 0x65])) + aff( + fe([ + 0x5e, 0xa8, 0xd8, 0x04, 0x9b, 0x73, 0xc9, 0xc9, 0xdc, 0x0d, 0x73, 0xbf, 0x0a, 0x0a, 0x73, 0xff, + 0x18, 0x1f, 0x9c, 0x51, 0xaa, 0xc6, 0xf1, 0x83, 0x25, 0xfd, 0xab, 0xa3, 0x11, 0xd3, 0x01, 0x24 + ]), + fe([ + 0x4d, 0xe3, 0x7e, 0x38, 0x62, 0x5e, 0x64, 0xbb, 0x2b, 0x53, 0xb5, 0x03, 0x68, 0xc4, 0xf2, 0x2b, + 0x5a, 0x03, 0x32, 0x99, 0x4a, 0x41, 0x9a, 0xe1, 0x1a, 0xae, 0x8c, 0x48, 0xf3, 0x24, 0x32, 0x65 + ]) + ) ) v.append( - aff(fe([0xe8, 0xdd, 0xad, 0x3a, 0x8c, 0xea, 0xf4, 0xb3, 0xb2, 0xe5, 0x73, 0xf2, 0xed, 0x8b, 0xbf, 0xed, - 0xb1, 0x0c, 0x0c, 0xfb, 0x2b, 0xf1, 0x01, 0x48, 0xe8, 0x26, 0x03, 0x8e, 0x27, 0x4d, 0x96, 0x72]) , - fe([0xc8, 0x09, 0x3b, 0x60, 0xc9, 0x26, 0x4d, 0x7c, 0xf2, 0x9c, 0xd4, 0xa1, 0x3b, 0x26, 0xc2, 0x04, - 0x33, 0x44, 0x76, 0x3c, 0x02, 0xbb, 0x11, 0x42, 0x0c, 0x22, 0xb7, 0xc6, 0xe1, 0xac, 0xb4, 0x0e])) + aff( + fe([ + 0xe8, 0xdd, 0xad, 0x3a, 0x8c, 0xea, 0xf4, 0xb3, 0xb2, 0xe5, 0x73, 0xf2, 0xed, 0x8b, 0xbf, 0xed, + 0xb1, 0x0c, 0x0c, 0xfb, 0x2b, 0xf1, 0x01, 0x48, 0xe8, 0x26, 0x03, 0x8e, 0x27, 0x4d, 0x96, 0x72 + ]), + fe([ + 0xc8, 0x09, 0x3b, 0x60, 0xc9, 0x26, 0x4d, 0x7c, 0xf2, 0x9c, 0xd4, 0xa1, 0x3b, 0x26, 0xc2, 0x04, + 0x33, 0x44, 0x76, 0x3c, 0x02, 0xbb, 0x11, 0x42, 0x0c, 0x22, 0xb7, 0xc6, 0xe1, 0xac, 0xb4, 0x0e + ]) + ) ) v.append( - aff(fe([0x6f, 0x85, 0xe7, 0xef, 0xde, 0x67, 0x30, 0xfc, 0xbf, 0x5a, 0xe0, 0x7b, 0x7a, 0x2a, 0x54, 0x6b, - 0x5d, 0x62, 0x85, 0xa1, 0xf8, 0x16, 0x88, 0xec, 0x61, 0xb9, 0x96, 0xb5, 0xef, 0x2d, 0x43, 0x4d]) , - fe([0x7c, 0x31, 0x33, 0xcc, 0xe4, 0xcf, 0x6c, 0xff, 0x80, 0x47, 0x77, 0xd1, 0xd8, 0xe9, 0x69, 0x97, - 0x98, 0x7f, 0x20, 0x57, 0x1d, 0x1d, 0x4f, 0x08, 0x27, 0xc8, 0x35, 0x57, 0x40, 0xc6, 0x21, 0x0c])) + aff( + fe([ + 0x6f, 0x85, 0xe7, 0xef, 0xde, 0x67, 0x30, 0xfc, 0xbf, 0x5a, 0xe0, 0x7b, 0x7a, 0x2a, 0x54, 0x6b, + 0x5d, 0x62, 0x85, 0xa1, 0xf8, 0x16, 0x88, 0xec, 0x61, 0xb9, 0x96, 0xb5, 0xef, 0x2d, 0x43, 0x4d + ]), + fe([ + 0x7c, 0x31, 0x33, 0xcc, 0xe4, 0xcf, 0x6c, 0xff, 0x80, 0x47, 0x77, 0xd1, 0xd8, 0xe9, 0x69, 0x97, + 0x98, 0x7f, 0x20, 0x57, 0x1d, 0x1d, 0x4f, 0x08, 0x27, 0xc8, 0x35, 0x57, 0x40, 0xc6, 0x21, 0x0c + ]) + ) ) v.append( - aff(fe([0xd2, 0x8e, 0x9b, 0xfa, 0x42, 0x8e, 0xdf, 0x8f, 0xc7, 0x86, 0xf9, 0xa4, 0xca, 0x70, 0x00, 0x9d, - 0x21, 0xbf, 0xec, 0x57, 0x62, 0x30, 0x58, 0x8c, 0x0d, 0x35, 0xdb, 0x5d, 0x8b, 0x6a, 0xa0, 0x5a]) , - fe([0xc1, 0x58, 0x7c, 0x0d, 0x20, 0xdd, 0x11, 0x26, 0x5f, 0x89, 0x3b, 0x97, 0x58, 0xf8, 0x8b, 0xe3, - 0xdf, 0x32, 0xe2, 0xfc, 0xd8, 0x67, 0xf2, 0xa5, 0x37, 0x1e, 0x6d, 0xec, 0x7c, 0x27, 0x20, 0x79])) + aff( + fe([ + 0xd2, 0x8e, 0x9b, 0xfa, 0x42, 0x8e, 0xdf, 0x8f, 0xc7, 0x86, 0xf9, 0xa4, 0xca, 0x70, 0x00, 0x9d, + 0x21, 0xbf, 0xec, 0x57, 0x62, 0x30, 0x58, 0x8c, 0x0d, 0x35, 0xdb, 0x5d, 0x8b, 0x6a, 0xa0, 0x5a + ]), + fe([ + 0xc1, 0x58, 0x7c, 0x0d, 0x20, 0xdd, 0x11, 0x26, 0x5f, 0x89, 0x3b, 0x97, 0x58, 0xf8, 0x8b, 0xe3, + 0xdf, 0x32, 0xe2, 0xfc, 0xd8, 0x67, 0xf2, 0xa5, 0x37, 0x1e, 0x6d, 0xec, 0x7c, 0x27, 0x20, 0x79 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xd0, 0xe9, 0xc0, 0xfa, 0x95, 0x45, 0x23, 0x96, 0xf1, 0x2c, 0x79, 0x25, 0x14, 0xce, 0x40, 0x14, - 0x44, 0x2c, 0x36, 0x50, 0xd9, 0x63, 0x56, 0xb7, 0x56, 0x3b, 0x9e, 0xa7, 0xef, 0x89, 0xbb, 0x0e]) , - fe([0xce, 0x7f, 0xdc, 0x0a, 0xcc, 0x82, 0x1c, 0x0a, 0x78, 0x71, 0xe8, 0x74, 0x8d, 0x01, 0x30, 0x0f, - 0xa7, 0x11, 0x4c, 0xdf, 0x38, 0xd7, 0xa7, 0x0d, 0xf8, 0x48, 0x52, 0x00, 0x80, 0x7b, 0x5f, 0x0e])) + aff( + fe([ + 0xd0, 0xe9, 0xc0, 0xfa, 0x95, 0x45, 0x23, 0x96, 0xf1, 0x2c, 0x79, 0x25, 0x14, 0xce, 0x40, 0x14, + 0x44, 0x2c, 0x36, 0x50, 0xd9, 0x63, 0x56, 0xb7, 0x56, 0x3b, 0x9e, 0xa7, 0xef, 0x89, 0xbb, 0x0e + ]), + fe([ + 0xce, 0x7f, 0xdc, 0x0a, 0xcc, 0x82, 0x1c, 0x0a, 0x78, 0x71, 0xe8, 0x74, 0x8d, 0x01, 0x30, 0x0f, + 0xa7, 0x11, 0x4c, 0xdf, 0x38, 0xd7, 0xa7, 0x0d, 0xf8, 0x48, 0x52, 0x00, 0x80, 0x7b, 0x5f, 0x0e + ]) + ) ) v.append( - aff(fe([0x25, 0x83, 0xe6, 0x94, 0x7b, 0x81, 0xb2, 0x91, 0xae, 0x0e, 0x05, 0xc9, 0xa3, 0x68, 0x2d, 0xd9, - 0x88, 0x25, 0x19, 0x2a, 0x61, 0x61, 0x21, 0x97, 0x15, 0xa1, 0x35, 0xa5, 0x46, 0xc8, 0xa2, 0x0e]) , - fe([0x1b, 0x03, 0x0d, 0x8b, 0x5a, 0x1b, 0x97, 0x4b, 0xf2, 0x16, 0x31, 0x3d, 0x1f, 0x33, 0xa0, 0x50, - 0x3a, 0x18, 0xbe, 0x13, 0xa1, 0x76, 0xc1, 0xba, 0x1b, 0xf1, 0x05, 0x7b, 0x33, 0xa8, 0x82, 0x3b])) + aff( + fe([ + 0x25, 0x83, 0xe6, 0x94, 0x7b, 0x81, 0xb2, 0x91, 0xae, 0x0e, 0x05, 0xc9, 0xa3, 0x68, 0x2d, 0xd9, + 0x88, 0x25, 0x19, 0x2a, 0x61, 0x61, 0x21, 0x97, 0x15, 0xa1, 0x35, 0xa5, 0x46, 0xc8, 0xa2, 0x0e + ]), + fe([ + 0x1b, 0x03, 0x0d, 0x8b, 0x5a, 0x1b, 0x97, 0x4b, 0xf2, 0x16, 0x31, 0x3d, 0x1f, 0x33, 0xa0, 0x50, + 0x3a, 0x18, 0xbe, 0x13, 0xa1, 0x76, 0xc1, 0xba, 0x1b, 0xf1, 0x05, 0x7b, 0x33, 0xa8, 0x82, 0x3b + ]) + ) ) v.append( - aff(fe([0xba, 0x36, 0x7b, 0x6d, 0xa9, 0xea, 0x14, 0x12, 0xc5, 0xfa, 0x91, 0x00, 0xba, 0x9b, 0x99, 0xcc, - 0x56, 0x02, 0xe9, 0xa0, 0x26, 0x40, 0x66, 0x8c, 0xc4, 0xf8, 0x85, 0x33, 0x68, 0xe7, 0x03, 0x20]) , - fe([0x50, 0x5b, 0xff, 0xa9, 0xb2, 0xf1, 0xf1, 0x78, 0xcf, 0x14, 0xa4, 0xa9, 0xfc, 0x09, 0x46, 0x94, - 0x54, 0x65, 0x0d, 0x9c, 0x5f, 0x72, 0x21, 0xe2, 0x97, 0xa5, 0x2d, 0x81, 0xce, 0x4a, 0x5f, 0x79])) + aff( + fe([ + 0xba, 0x36, 0x7b, 0x6d, 0xa9, 0xea, 0x14, 0x12, 0xc5, 0xfa, 0x91, 0x00, 0xba, 0x9b, 0x99, 0xcc, + 0x56, 0x02, 0xe9, 0xa0, 0x26, 0x40, 0x66, 0x8c, 0xc4, 0xf8, 0x85, 0x33, 0x68, 0xe7, 0x03, 0x20 + ]), + fe([ + 0x50, 0x5b, 0xff, 0xa9, 0xb2, 0xf1, 0xf1, 0x78, 0xcf, 0x14, 0xa4, 0xa9, 0xfc, 0x09, 0x46, 0x94, + 0x54, 0x65, 0x0d, 0x9c, 0x5f, 0x72, 0x21, 0xe2, 0x97, 0xa5, 0x2d, 0x81, 0xce, 0x4a, 0x5f, 0x79 + ]) + ) ) v.append( - aff(fe([0x3d, 0x5f, 0x5c, 0xd2, 0xbc, 0x7d, 0x77, 0x0e, 0x2a, 0x6d, 0x22, 0x45, 0x84, 0x06, 0xc4, 0xdd, - 0xc6, 0xa6, 0xc6, 0xd7, 0x49, 0xad, 0x6d, 0x87, 0x91, 0x0e, 0x3a, 0x67, 0x1d, 0x2c, 0x1d, 0x56]) , - fe([0xfe, 0x7a, 0x74, 0xcf, 0xd4, 0xd2, 0xe5, 0x19, 0xde, 0xd0, 0xdb, 0x70, 0x23, 0x69, 0xe6, 0x6d, - 0xec, 0xec, 0xcc, 0x09, 0x33, 0x6a, 0x77, 0xdc, 0x6b, 0x22, 0x76, 0x5d, 0x92, 0x09, 0xac, 0x2d])) + aff( + fe([ + 0x3d, 0x5f, 0x5c, 0xd2, 0xbc, 0x7d, 0x77, 0x0e, 0x2a, 0x6d, 0x22, 0x45, 0x84, 0x06, 0xc4, 0xdd, + 0xc6, 0xa6, 0xc6, 0xd7, 0x49, 0xad, 0x6d, 0x87, 0x91, 0x0e, 0x3a, 0x67, 0x1d, 0x2c, 0x1d, 0x56 + ]), + fe([ + 0xfe, 0x7a, 0x74, 0xcf, 0xd4, 0xd2, 0xe5, 0x19, 0xde, 0xd0, 0xdb, 0x70, 0x23, 0x69, 0xe6, 0x6d, + 0xec, 0xec, 0xcc, 0x09, 0x33, 0x6a, 0x77, 0xdc, 0x6b, 0x22, 0x76, 0x5d, 0x92, 0x09, 0xac, 0x2d + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x23, 0x15, 0x17, 0xeb, 0xd3, 0xdb, 0x12, 0x5e, 0x01, 0xf0, 0x91, 0xab, 0x2c, 0x41, 0xce, 0xac, - 0xed, 0x1b, 0x4b, 0x2d, 0xbc, 0xdb, 0x17, 0x66, 0x89, 0x46, 0xad, 0x4b, 0x1e, 0x6f, 0x0b, 0x14]) , - fe([0x11, 0xce, 0xbf, 0xb6, 0x77, 0x2d, 0x48, 0x22, 0x18, 0x4f, 0xa3, 0x5d, 0x4a, 0xb0, 0x70, 0x12, - 0x3e, 0x54, 0xd7, 0xd8, 0x0e, 0x2b, 0x27, 0xdc, 0x53, 0xff, 0xca, 0x8c, 0x59, 0xb3, 0x4e, 0x44])) + aff( + fe([ + 0x23, 0x15, 0x17, 0xeb, 0xd3, 0xdb, 0x12, 0x5e, 0x01, 0xf0, 0x91, 0xab, 0x2c, 0x41, 0xce, 0xac, + 0xed, 0x1b, 0x4b, 0x2d, 0xbc, 0xdb, 0x17, 0x66, 0x89, 0x46, 0xad, 0x4b, 0x1e, 0x6f, 0x0b, 0x14 + ]), + fe([ + 0x11, 0xce, 0xbf, 0xb6, 0x77, 0x2d, 0x48, 0x22, 0x18, 0x4f, 0xa3, 0x5d, 0x4a, 0xb0, 0x70, 0x12, + 0x3e, 0x54, 0xd7, 0xd8, 0x0e, 0x2b, 0x27, 0xdc, 0x53, 0xff, 0xca, 0x8c, 0x59, 0xb3, 0x4e, 0x44 + ]) + ) ) v.append( - aff(fe([0x07, 0x76, 0x61, 0x0f, 0x66, 0xb2, 0x21, 0x39, 0x7e, 0xc0, 0xec, 0x45, 0x28, 0x82, 0xa1, 0x29, - 0x32, 0x44, 0x35, 0x13, 0x5e, 0x61, 0x5e, 0x54, 0xcb, 0x7c, 0xef, 0xf6, 0x41, 0xcf, 0x9f, 0x0a]) , - fe([0xdd, 0xf9, 0xda, 0x84, 0xc3, 0xe6, 0x8a, 0x9f, 0x24, 0xd2, 0x96, 0x5d, 0x39, 0x6f, 0x58, 0x8c, - 0xc1, 0x56, 0x93, 0xab, 0xb5, 0x79, 0x3b, 0xd2, 0xa8, 0x73, 0x16, 0xed, 0xfa, 0xb4, 0x2f, 0x73])) + aff( + fe([ + 0x07, 0x76, 0x61, 0x0f, 0x66, 0xb2, 0x21, 0x39, 0x7e, 0xc0, 0xec, 0x45, 0x28, 0x82, 0xa1, 0x29, + 0x32, 0x44, 0x35, 0x13, 0x5e, 0x61, 0x5e, 0x54, 0xcb, 0x7c, 0xef, 0xf6, 0x41, 0xcf, 0x9f, 0x0a + ]), + fe([ + 0xdd, 0xf9, 0xda, 0x84, 0xc3, 0xe6, 0x8a, 0x9f, 0x24, 0xd2, 0x96, 0x5d, 0x39, 0x6f, 0x58, 0x8c, + 0xc1, 0x56, 0x93, 0xab, 0xb5, 0x79, 0x3b, 0xd2, 0xa8, 0x73, 0x16, 0xed, 0xfa, 0xb4, 0x2f, 0x73 + ]) + ) ) v.append( - aff(fe([0x8b, 0xb1, 0x95, 0xe5, 0x92, 0x50, 0x35, 0x11, 0x76, 0xac, 0xf4, 0x4d, 0x24, 0xc3, 0x32, 0xe6, - 0xeb, 0xfe, 0x2c, 0x87, 0xc4, 0xf1, 0x56, 0xc4, 0x75, 0x24, 0x7a, 0x56, 0x85, 0x5a, 0x3a, 0x13]) , - fe([0x0d, 0x16, 0xac, 0x3c, 0x4a, 0x58, 0x86, 0x3a, 0x46, 0x7f, 0x6c, 0xa3, 0x52, 0x6e, 0x37, 0xe4, - 0x96, 0x9c, 0xe9, 0x5c, 0x66, 0x41, 0x67, 0xe4, 0xfb, 0x79, 0x0c, 0x05, 0xf6, 0x64, 0xd5, 0x7c])) + aff( + fe([ + 0x8b, 0xb1, 0x95, 0xe5, 0x92, 0x50, 0x35, 0x11, 0x76, 0xac, 0xf4, 0x4d, 0x24, 0xc3, 0x32, 0xe6, + 0xeb, 0xfe, 0x2c, 0x87, 0xc4, 0xf1, 0x56, 0xc4, 0x75, 0x24, 0x7a, 0x56, 0x85, 0x5a, 0x3a, 0x13 + ]), + fe([ + 0x0d, 0x16, 0xac, 0x3c, 0x4a, 0x58, 0x86, 0x3a, 0x46, 0x7f, 0x6c, 0xa3, 0x52, 0x6e, 0x37, 0xe4, + 0x96, 0x9c, 0xe9, 0x5c, 0x66, 0x41, 0x67, 0xe4, 0xfb, 0x79, 0x0c, 0x05, 0xf6, 0x64, 0xd5, 0x7c + ]) + ) ) v.append( - aff(fe([0x28, 0xc1, 0xe1, 0x54, 0x73, 0xf2, 0xbf, 0x76, 0x74, 0x19, 0x19, 0x1b, 0xe4, 0xb9, 0xa8, 0x46, - 0x65, 0x73, 0xf3, 0x77, 0x9b, 0x29, 0x74, 0x5b, 0xc6, 0x89, 0x6c, 0x2c, 0x7c, 0xf8, 0xb3, 0x0f]) , - fe([0xf7, 0xd5, 0xe9, 0x74, 0x5d, 0xb8, 0x25, 0x16, 0xb5, 0x30, 0xbc, 0x84, 0xc5, 0xf0, 0xad, 0xca, - 0x12, 0x28, 0xbc, 0x9d, 0xd4, 0xfa, 0x82, 0xe6, 0xe3, 0xbf, 0xa2, 0x15, 0x2c, 0xd4, 0x34, 0x10])) + aff( + fe([ + 0x28, 0xc1, 0xe1, 0x54, 0x73, 0xf2, 0xbf, 0x76, 0x74, 0x19, 0x19, 0x1b, 0xe4, 0xb9, 0xa8, 0x46, + 0x65, 0x73, 0xf3, 0x77, 0x9b, 0x29, 0x74, 0x5b, 0xc6, 0x89, 0x6c, 0x2c, 0x7c, 0xf8, 0xb3, 0x0f + ]), + fe([ + 0xf7, 0xd5, 0xe9, 0x74, 0x5d, 0xb8, 0x25, 0x16, 0xb5, 0x30, 0xbc, 0x84, 0xc5, 0xf0, 0xad, 0xca, + 0x12, 0x28, 0xbc, 0x9d, 0xd4, 0xfa, 0x82, 0xe6, 0xe3, 0xbf, 0xa2, 0x15, 0x2c, 0xd4, 0x34, 0x10 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x61, 0xb1, 0x46, 0xba, 0x0e, 0x31, 0xa5, 0x67, 0x6c, 0x7f, 0xd6, 0xd9, 0x27, 0x85, 0x0f, 0x79, - 0x14, 0xc8, 0x6c, 0x2f, 0x5f, 0x5b, 0x9c, 0x35, 0x3d, 0x38, 0x86, 0x77, 0x65, 0x55, 0x6a, 0x7b]) , - fe([0xd3, 0xb0, 0x3a, 0x66, 0x60, 0x1b, 0x43, 0xf1, 0x26, 0x58, 0x99, 0x09, 0x8f, 0x2d, 0xa3, 0x14, - 0x71, 0x85, 0xdb, 0xed, 0xf6, 0x26, 0xd5, 0x61, 0x9a, 0x73, 0xac, 0x0e, 0xea, 0xac, 0xb7, 0x0c])) + aff( + fe([ + 0x61, 0xb1, 0x46, 0xba, 0x0e, 0x31, 0xa5, 0x67, 0x6c, 0x7f, 0xd6, 0xd9, 0x27, 0x85, 0x0f, 0x79, + 0x14, 0xc8, 0x6c, 0x2f, 0x5f, 0x5b, 0x9c, 0x35, 0x3d, 0x38, 0x86, 0x77, 0x65, 0x55, 0x6a, 0x7b + ]), + fe([ + 0xd3, 0xb0, 0x3a, 0x66, 0x60, 0x1b, 0x43, 0xf1, 0x26, 0x58, 0x99, 0x09, 0x8f, 0x2d, 0xa3, 0x14, + 0x71, 0x85, 0xdb, 0xed, 0xf6, 0x26, 0xd5, 0x61, 0x9a, 0x73, 0xac, 0x0e, 0xea, 0xac, 0xb7, 0x0c + ]) + ) ) v.append( - aff(fe([0x5e, 0xf4, 0xe5, 0x17, 0x0e, 0x10, 0x9f, 0xe7, 0x43, 0x5f, 0x67, 0x5c, 0xac, 0x4b, 0xe5, 0x14, - 0x41, 0xd2, 0xbf, 0x48, 0xf5, 0x14, 0xb0, 0x71, 0xc6, 0x61, 0xc1, 0xb2, 0x70, 0x58, 0xd2, 0x5a]) , - fe([0x2d, 0xba, 0x16, 0x07, 0x92, 0x94, 0xdc, 0xbd, 0x50, 0x2b, 0xc9, 0x7f, 0x42, 0x00, 0xba, 0x61, - 0xed, 0xf8, 0x43, 0xed, 0xf5, 0xf9, 0x40, 0x60, 0xb2, 0xb0, 0x82, 0xcb, 0xed, 0x75, 0xc7, 0x65])) + aff( + fe([ + 0x5e, 0xf4, 0xe5, 0x17, 0x0e, 0x10, 0x9f, 0xe7, 0x43, 0x5f, 0x67, 0x5c, 0xac, 0x4b, 0xe5, 0x14, + 0x41, 0xd2, 0xbf, 0x48, 0xf5, 0x14, 0xb0, 0x71, 0xc6, 0x61, 0xc1, 0xb2, 0x70, 0x58, 0xd2, 0x5a + ]), + fe([ + 0x2d, 0xba, 0x16, 0x07, 0x92, 0x94, 0xdc, 0xbd, 0x50, 0x2b, 0xc9, 0x7f, 0x42, 0x00, 0xba, 0x61, + 0xed, 0xf8, 0x43, 0xed, 0xf5, 0xf9, 0x40, 0x60, 0xb2, 0xb0, 0x82, 0xcb, 0xed, 0x75, 0xc7, 0x65 + ]) + ) ) v.append( - aff(fe([0x80, 0xba, 0x0d, 0x09, 0x40, 0xa7, 0x39, 0xa6, 0x67, 0x34, 0x7e, 0x66, 0xbe, 0x56, 0xfb, 0x53, - 0x78, 0xc4, 0x46, 0xe8, 0xed, 0x68, 0x6c, 0x7f, 0xce, 0xe8, 0x9f, 0xce, 0xa2, 0x64, 0x58, 0x53]) , - fe([0xe8, 0xc1, 0xa9, 0xc2, 0x7b, 0x59, 0x21, 0x33, 0xe2, 0x43, 0x73, 0x2b, 0xac, 0x2d, 0xc1, 0x89, - 0x3b, 0x15, 0xe2, 0xd5, 0xc0, 0x97, 0x8a, 0xfd, 0x6f, 0x36, 0x33, 0xb7, 0xb9, 0xc3, 0x88, 0x09])) + aff( + fe([ + 0x80, 0xba, 0x0d, 0x09, 0x40, 0xa7, 0x39, 0xa6, 0x67, 0x34, 0x7e, 0x66, 0xbe, 0x56, 0xfb, 0x53, + 0x78, 0xc4, 0x46, 0xe8, 0xed, 0x68, 0x6c, 0x7f, 0xce, 0xe8, 0x9f, 0xce, 0xa2, 0x64, 0x58, 0x53 + ]), + fe([ + 0xe8, 0xc1, 0xa9, 0xc2, 0x7b, 0x59, 0x21, 0x33, 0xe2, 0x43, 0x73, 0x2b, 0xac, 0x2d, 0xc1, 0x89, + 0x3b, 0x15, 0xe2, 0xd5, 0xc0, 0x97, 0x8a, 0xfd, 0x6f, 0x36, 0x33, 0xb7, 0xb9, 0xc3, 0x88, 0x09 + ]) + ) ) v.append( - aff(fe([0xd0, 0xb6, 0x56, 0x30, 0x5c, 0xae, 0xb3, 0x75, 0x44, 0xa4, 0x83, 0x51, 0x6e, 0x01, 0x65, 0xef, - 0x45, 0x76, 0xe6, 0xf5, 0xa2, 0x0d, 0xd4, 0x16, 0x3b, 0x58, 0x2f, 0xf2, 0x2f, 0x36, 0x18, 0x3f]) , - fe([0xfd, 0x2f, 0xe0, 0x9b, 0x1e, 0x8c, 0xc5, 0x18, 0xa9, 0xca, 0xd4, 0x2b, 0x35, 0xb6, 0x95, 0x0a, - 0x9f, 0x7e, 0xfb, 0xc4, 0xef, 0x88, 0x7b, 0x23, 0x43, 0xec, 0x2f, 0x0d, 0x0f, 0x7a, 0xfc, 0x5c])) + aff( + fe([ + 0xd0, 0xb6, 0x56, 0x30, 0x5c, 0xae, 0xb3, 0x75, 0x44, 0xa4, 0x83, 0x51, 0x6e, 0x01, 0x65, 0xef, + 0x45, 0x76, 0xe6, 0xf5, 0xa2, 0x0d, 0xd4, 0x16, 0x3b, 0x58, 0x2f, 0xf2, 0x2f, 0x36, 0x18, 0x3f + ]), + fe([ + 0xfd, 0x2f, 0xe0, 0x9b, 0x1e, 0x8c, 0xc5, 0x18, 0xa9, 0xca, 0xd4, 0x2b, 0x35, 0xb6, 0x95, 0x0a, + 0x9f, 0x7e, 0xfb, 0xc4, 0xef, 0x88, 0x7b, 0x23, 0x43, 0xec, 0x2f, 0x0d, 0x0f, 0x7a, 0xfc, 0x5c + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x8d, 0xd2, 0xda, 0xc7, 0x44, 0xd6, 0x7a, 0xdb, 0x26, 0x7d, 0x1d, 0xb8, 0xe1, 0xde, 0x9d, 0x7a, - 0x7d, 0x17, 0x7e, 0x1c, 0x37, 0x04, 0x8d, 0x2d, 0x7c, 0x5e, 0x18, 0x38, 0x1e, 0xaf, 0xc7, 0x1b]) , - fe([0x33, 0x48, 0x31, 0x00, 0x59, 0xf6, 0xf2, 0xca, 0x0f, 0x27, 0x1b, 0x63, 0x12, 0x7e, 0x02, 0x1d, - 0x49, 0xc0, 0x5d, 0x79, 0x87, 0xef, 0x5e, 0x7a, 0x2f, 0x1f, 0x66, 0x55, 0xd8, 0x09, 0xd9, 0x61])) + aff( + fe([ + 0x8d, 0xd2, 0xda, 0xc7, 0x44, 0xd6, 0x7a, 0xdb, 0x26, 0x7d, 0x1d, 0xb8, 0xe1, 0xde, 0x9d, 0x7a, + 0x7d, 0x17, 0x7e, 0x1c, 0x37, 0x04, 0x8d, 0x2d, 0x7c, 0x5e, 0x18, 0x38, 0x1e, 0xaf, 0xc7, 0x1b + ]), + fe([ + 0x33, 0x48, 0x31, 0x00, 0x59, 0xf6, 0xf2, 0xca, 0x0f, 0x27, 0x1b, 0x63, 0x12, 0x7e, 0x02, 0x1d, + 0x49, 0xc0, 0x5d, 0x79, 0x87, 0xef, 0x5e, 0x7a, 0x2f, 0x1f, 0x66, 0x55, 0xd8, 0x09, 0xd9, 0x61 + ]) + ) ) v.append( - aff(fe([0x54, 0x83, 0x02, 0x18, 0x82, 0x93, 0x99, 0x07, 0xd0, 0xa7, 0xda, 0xd8, 0x75, 0x89, 0xfa, 0xf2, - 0xd9, 0xa3, 0xb8, 0x6b, 0x5a, 0x35, 0x28, 0xd2, 0x6b, 0x59, 0xc2, 0xf8, 0x45, 0xe2, 0xbc, 0x06]) , - fe([0x65, 0xc0, 0xa3, 0x88, 0x51, 0x95, 0xfc, 0x96, 0x94, 0x78, 0xe8, 0x0d, 0x8b, 0x41, 0xc9, 0xc2, - 0x58, 0x48, 0x75, 0x10, 0x2f, 0xcd, 0x2a, 0xc9, 0xa0, 0x6d, 0x0f, 0xdd, 0x9c, 0x98, 0x26, 0x3d])) + aff( + fe([ + 0x54, 0x83, 0x02, 0x18, 0x82, 0x93, 0x99, 0x07, 0xd0, 0xa7, 0xda, 0xd8, 0x75, 0x89, 0xfa, 0xf2, + 0xd9, 0xa3, 0xb8, 0x6b, 0x5a, 0x35, 0x28, 0xd2, 0x6b, 0x59, 0xc2, 0xf8, 0x45, 0xe2, 0xbc, 0x06 + ]), + fe([ + 0x65, 0xc0, 0xa3, 0x88, 0x51, 0x95, 0xfc, 0x96, 0x94, 0x78, 0xe8, 0x0d, 0x8b, 0x41, 0xc9, 0xc2, + 0x58, 0x48, 0x75, 0x10, 0x2f, 0xcd, 0x2a, 0xc9, 0xa0, 0x6d, 0x0f, 0xdd, 0x9c, 0x98, 0x26, 0x3d + ]) + ) ) v.append( - aff(fe([0x2f, 0x66, 0x29, 0x1b, 0x04, 0x89, 0xbd, 0x7e, 0xee, 0x6e, 0xdd, 0xb7, 0x0e, 0xef, 0xb0, 0x0c, - 0xb4, 0xfc, 0x7f, 0xc2, 0xc9, 0x3a, 0x3c, 0x64, 0xef, 0x45, 0x44, 0xaf, 0x8a, 0x90, 0x65, 0x76]) , - fe([0xa1, 0x4c, 0x70, 0x4b, 0x0e, 0xa0, 0x83, 0x70, 0x13, 0xa4, 0xaf, 0xb8, 0x38, 0x19, 0x22, 0x65, - 0x09, 0xb4, 0x02, 0x4f, 0x06, 0xf8, 0x17, 0xce, 0x46, 0x45, 0xda, 0x50, 0x7c, 0x8a, 0xd1, 0x4e])) + aff( + fe([ + 0x2f, 0x66, 0x29, 0x1b, 0x04, 0x89, 0xbd, 0x7e, 0xee, 0x6e, 0xdd, 0xb7, 0x0e, 0xef, 0xb0, 0x0c, + 0xb4, 0xfc, 0x7f, 0xc2, 0xc9, 0x3a, 0x3c, 0x64, 0xef, 0x45, 0x44, 0xaf, 0x8a, 0x90, 0x65, 0x76 + ]), + fe([ + 0xa1, 0x4c, 0x70, 0x4b, 0x0e, 0xa0, 0x83, 0x70, 0x13, 0xa4, 0xaf, 0xb8, 0x38, 0x19, 0x22, 0x65, + 0x09, 0xb4, 0x02, 0x4f, 0x06, 0xf8, 0x17, 0xce, 0x46, 0x45, 0xda, 0x50, 0x7c, 0x8a, 0xd1, 0x4e + ]) + ) ) v.append( - aff(fe([0xf7, 0xd4, 0x16, 0x6c, 0x4e, 0x95, 0x9d, 0x5d, 0x0f, 0x91, 0x2b, 0x52, 0xfe, 0x5c, 0x34, 0xe5, - 0x30, 0xe6, 0xa4, 0x3b, 0xf3, 0xf3, 0x34, 0x08, 0xa9, 0x4a, 0xa0, 0xb5, 0x6e, 0xb3, 0x09, 0x0a]) , - fe([0x26, 0xd9, 0x5e, 0xa3, 0x0f, 0xeb, 0xa2, 0xf3, 0x20, 0x3b, 0x37, 0xd4, 0xe4, 0x9e, 0xce, 0x06, - 0x3d, 0x53, 0xed, 0xae, 0x2b, 0xeb, 0xb6, 0x24, 0x0a, 0x11, 0xa3, 0x0f, 0xd6, 0x7f, 0xa4, 0x3a])) + aff( + fe([ + 0xf7, 0xd4, 0x16, 0x6c, 0x4e, 0x95, 0x9d, 0x5d, 0x0f, 0x91, 0x2b, 0x52, 0xfe, 0x5c, 0x34, 0xe5, + 0x30, 0xe6, 0xa4, 0x3b, 0xf3, 0xf3, 0x34, 0x08, 0xa9, 0x4a, 0xa0, 0xb5, 0x6e, 0xb3, 0x09, 0x0a + ]), + fe([ + 0x26, 0xd9, 0x5e, 0xa3, 0x0f, 0xeb, 0xa2, 0xf3, 0x20, 0x3b, 0x37, 0xd4, 0xe4, 0x9e, 0xce, 0x06, + 0x3d, 0x53, 0xed, 0xae, 0x2b, 0xeb, 0xb6, 0x24, 0x0a, 0x11, 0xa3, 0x0f, 0xd6, 0x7f, 0xa4, 0x3a + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xdb, 0x9f, 0x2c, 0xfc, 0xd6, 0xb2, 0x1e, 0x2e, 0x52, 0x7a, 0x06, 0x87, 0x2d, 0x86, 0x72, 0x2b, - 0x6d, 0x90, 0x77, 0x46, 0x43, 0xb5, 0x7a, 0xf8, 0x60, 0x7d, 0x91, 0x60, 0x5b, 0x9d, 0x9e, 0x07]) , - fe([0x97, 0x87, 0xc7, 0x04, 0x1c, 0x38, 0x01, 0x39, 0x58, 0xc7, 0x85, 0xa3, 0xfc, 0x64, 0x00, 0x64, - 0x25, 0xa2, 0xbf, 0x50, 0x94, 0xca, 0x26, 0x31, 0x45, 0x0a, 0x24, 0xd2, 0x51, 0x29, 0x51, 0x16])) + aff( + fe([ + 0xdb, 0x9f, 0x2c, 0xfc, 0xd6, 0xb2, 0x1e, 0x2e, 0x52, 0x7a, 0x06, 0x87, 0x2d, 0x86, 0x72, 0x2b, + 0x6d, 0x90, 0x77, 0x46, 0x43, 0xb5, 0x7a, 0xf8, 0x60, 0x7d, 0x91, 0x60, 0x5b, 0x9d, 0x9e, 0x07 + ]), + fe([ + 0x97, 0x87, 0xc7, 0x04, 0x1c, 0x38, 0x01, 0x39, 0x58, 0xc7, 0x85, 0xa3, 0xfc, 0x64, 0x00, 0x64, + 0x25, 0xa2, 0xbf, 0x50, 0x94, 0xca, 0x26, 0x31, 0x45, 0x0a, 0x24, 0xd2, 0x51, 0x29, 0x51, 0x16 + ]) + ) ) v.append( - aff(fe([0x4d, 0x4a, 0xd7, 0x98, 0x71, 0x57, 0xac, 0x7d, 0x8b, 0x37, 0xbd, 0x63, 0xff, 0x87, 0xb1, 0x49, - 0x95, 0x20, 0x7c, 0xcf, 0x7c, 0x59, 0xc4, 0x91, 0x9c, 0xef, 0xd0, 0xdb, 0x60, 0x09, 0x9d, 0x46]) , - fe([0xcb, 0x78, 0x94, 0x90, 0xe4, 0x45, 0xb3, 0xf6, 0xd9, 0xf6, 0x57, 0x74, 0xd5, 0xf8, 0x83, 0x4f, - 0x39, 0xc9, 0xbd, 0x88, 0xc2, 0x57, 0x21, 0x1f, 0x24, 0x32, 0x68, 0xf8, 0xc7, 0x21, 0x5f, 0x0b])) + aff( + fe([ + 0x4d, 0x4a, 0xd7, 0x98, 0x71, 0x57, 0xac, 0x7d, 0x8b, 0x37, 0xbd, 0x63, 0xff, 0x87, 0xb1, 0x49, + 0x95, 0x20, 0x7c, 0xcf, 0x7c, 0x59, 0xc4, 0x91, 0x9c, 0xef, 0xd0, 0xdb, 0x60, 0x09, 0x9d, 0x46 + ]), + fe([ + 0xcb, 0x78, 0x94, 0x90, 0xe4, 0x45, 0xb3, 0xf6, 0xd9, 0xf6, 0x57, 0x74, 0xd5, 0xf8, 0x83, 0x4f, + 0x39, 0xc9, 0xbd, 0x88, 0xc2, 0x57, 0x21, 0x1f, 0x24, 0x32, 0x68, 0xf8, 0xc7, 0x21, 0x5f, 0x0b + ]) + ) ) v.append( - aff(fe([0x2a, 0x36, 0x68, 0xfc, 0x5f, 0xb6, 0x4f, 0xa5, 0xe3, 0x9d, 0x24, 0x2f, 0xc0, 0x93, 0x61, 0xcf, - 0xf8, 0x0a, 0xed, 0xe1, 0xdb, 0x27, 0xec, 0x0e, 0x14, 0x32, 0x5f, 0x8e, 0xa1, 0x62, 0x41, 0x16]) , - fe([0x95, 0x21, 0x01, 0xce, 0x95, 0x5b, 0x0e, 0x57, 0xc7, 0xb9, 0x62, 0xb5, 0x28, 0xca, 0x11, 0xec, - 0xb4, 0x46, 0x06, 0x73, 0x26, 0xff, 0xfb, 0x66, 0x7d, 0xee, 0x5f, 0xb2, 0x56, 0xfd, 0x2a, 0x08])) + aff( + fe([ + 0x2a, 0x36, 0x68, 0xfc, 0x5f, 0xb6, 0x4f, 0xa5, 0xe3, 0x9d, 0x24, 0x2f, 0xc0, 0x93, 0x61, 0xcf, + 0xf8, 0x0a, 0xed, 0xe1, 0xdb, 0x27, 0xec, 0x0e, 0x14, 0x32, 0x5f, 0x8e, 0xa1, 0x62, 0x41, 0x16 + ]), + fe([ + 0x95, 0x21, 0x01, 0xce, 0x95, 0x5b, 0x0e, 0x57, 0xc7, 0xb9, 0x62, 0xb5, 0x28, 0xca, 0x11, 0xec, + 0xb4, 0x46, 0x06, 0x73, 0x26, 0xff, 0xfb, 0x66, 0x7d, 0xee, 0x5f, 0xb2, 0x56, 0xfd, 0x2a, 0x08 + ]) + ) ) v.append( - aff(fe([0x92, 0x67, 0x77, 0x56, 0xa1, 0xff, 0xc4, 0xc5, 0x95, 0xf0, 0xe3, 0x3a, 0x0a, 0xca, 0x94, 0x4d, - 0x9e, 0x7e, 0x3d, 0xb9, 0x6e, 0xb6, 0xb0, 0xce, 0xa4, 0x30, 0x89, 0x99, 0xe9, 0xad, 0x11, 0x59]) , - fe([0xf6, 0x48, 0x95, 0xa1, 0x6f, 0x5f, 0xb7, 0xa5, 0xbb, 0x30, 0x00, 0x1c, 0xd2, 0x8a, 0xd6, 0x25, - 0x26, 0x1b, 0xb2, 0x0d, 0x37, 0x6a, 0x05, 0xf4, 0x9d, 0x3e, 0x17, 0x2a, 0x43, 0xd2, 0x3a, 0x06])) + aff( + fe([ + 0x92, 0x67, 0x77, 0x56, 0xa1, 0xff, 0xc4, 0xc5, 0x95, 0xf0, 0xe3, 0x3a, 0x0a, 0xca, 0x94, 0x4d, + 0x9e, 0x7e, 0x3d, 0xb9, 0x6e, 0xb6, 0xb0, 0xce, 0xa4, 0x30, 0x89, 0x99, 0xe9, 0xad, 0x11, 0x59 + ]), + fe([ + 0xf6, 0x48, 0x95, 0xa1, 0x6f, 0x5f, 0xb7, 0xa5, 0xbb, 0x30, 0x00, 0x1c, 0xd2, 0x8a, 0xd6, 0x25, + 0x26, 0x1b, 0xb2, 0x0d, 0x37, 0x6a, 0x05, 0xf4, 0x9d, 0x3e, 0x17, 0x2a, 0x43, 0xd2, 0x3a, 0x06 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x32, 0x99, 0x93, 0xd1, 0x9a, 0x72, 0xf3, 0xa9, 0x16, 0xbd, 0xb4, 0x4c, 0xdd, 0xf9, 0xd4, 0xb2, - 0x64, 0x9a, 0xd3, 0x05, 0xe4, 0xa3, 0x73, 0x1c, 0xcb, 0x7e, 0x57, 0x67, 0xff, 0x04, 0xb3, 0x10]) , - fe([0xb9, 0x4b, 0xa4, 0xad, 0xd0, 0x6d, 0x61, 0x23, 0xb4, 0xaf, 0x34, 0xa9, 0xaa, 0x65, 0xec, 0xd9, - 0x69, 0xe3, 0x85, 0xcd, 0xcc, 0xe7, 0xb0, 0x9b, 0x41, 0xc1, 0x1c, 0xf9, 0xa0, 0xfa, 0xb7, 0x13])) + aff( + fe([ + 0x32, 0x99, 0x93, 0xd1, 0x9a, 0x72, 0xf3, 0xa9, 0x16, 0xbd, 0xb4, 0x4c, 0xdd, 0xf9, 0xd4, 0xb2, + 0x64, 0x9a, 0xd3, 0x05, 0xe4, 0xa3, 0x73, 0x1c, 0xcb, 0x7e, 0x57, 0x67, 0xff, 0x04, 0xb3, 0x10 + ]), + fe([ + 0xb9, 0x4b, 0xa4, 0xad, 0xd0, 0x6d, 0x61, 0x23, 0xb4, 0xaf, 0x34, 0xa9, 0xaa, 0x65, 0xec, 0xd9, + 0x69, 0xe3, 0x85, 0xcd, 0xcc, 0xe7, 0xb0, 0x9b, 0x41, 0xc1, 0x1c, 0xf9, 0xa0, 0xfa, 0xb7, 0x13 + ]) + ) ) v.append( - aff(fe([0x04, 0xfd, 0x88, 0x3c, 0x0c, 0xd0, 0x09, 0x52, 0x51, 0x4f, 0x06, 0x19, 0xcc, 0xc3, 0xbb, 0xde, - 0x80, 0xc5, 0x33, 0xbc, 0xf9, 0xf3, 0x17, 0x36, 0xdd, 0xc6, 0xde, 0xe8, 0x9b, 0x5d, 0x79, 0x1b]) , - fe([0x65, 0x0a, 0xbe, 0x51, 0x57, 0xad, 0x50, 0x79, 0x08, 0x71, 0x9b, 0x07, 0x95, 0x8f, 0xfb, 0xae, - 0x4b, 0x38, 0xba, 0xcf, 0x53, 0x2a, 0x86, 0x1e, 0xc0, 0x50, 0x5c, 0x67, 0x1b, 0xf6, 0x87, 0x6c])) + aff( + fe([ + 0x04, 0xfd, 0x88, 0x3c, 0x0c, 0xd0, 0x09, 0x52, 0x51, 0x4f, 0x06, 0x19, 0xcc, 0xc3, 0xbb, 0xde, + 0x80, 0xc5, 0x33, 0xbc, 0xf9, 0xf3, 0x17, 0x36, 0xdd, 0xc6, 0xde, 0xe8, 0x9b, 0x5d, 0x79, 0x1b + ]), + fe([ + 0x65, 0x0a, 0xbe, 0x51, 0x57, 0xad, 0x50, 0x79, 0x08, 0x71, 0x9b, 0x07, 0x95, 0x8f, 0xfb, 0xae, + 0x4b, 0x38, 0xba, 0xcf, 0x53, 0x2a, 0x86, 0x1e, 0xc0, 0x50, 0x5c, 0x67, 0x1b, 0xf6, 0x87, 0x6c + ]) + ) ) v.append( - aff(fe([0x4f, 0x00, 0xb2, 0x66, 0x55, 0xed, 0x4a, 0xed, 0x8d, 0xe1, 0x66, 0x18, 0xb2, 0x14, 0x74, 0x8d, - 0xfd, 0x1a, 0x36, 0x0f, 0x26, 0x5c, 0x8b, 0x89, 0xf3, 0xab, 0xf2, 0xf3, 0x24, 0x67, 0xfd, 0x70]) , - fe([0xfd, 0x4e, 0x2a, 0xc1, 0x3a, 0xca, 0x8f, 0x00, 0xd8, 0xec, 0x74, 0x67, 0xef, 0x61, 0xe0, 0x28, - 0xd0, 0x96, 0xf4, 0x48, 0xde, 0x81, 0xe3, 0xef, 0xdc, 0xaa, 0x7d, 0xf3, 0xb6, 0x55, 0xa6, 0x65])) + aff( + fe([ + 0x4f, 0x00, 0xb2, 0x66, 0x55, 0xed, 0x4a, 0xed, 0x8d, 0xe1, 0x66, 0x18, 0xb2, 0x14, 0x74, 0x8d, + 0xfd, 0x1a, 0x36, 0x0f, 0x26, 0x5c, 0x8b, 0x89, 0xf3, 0xab, 0xf2, 0xf3, 0x24, 0x67, 0xfd, 0x70 + ]), + fe([ + 0xfd, 0x4e, 0x2a, 0xc1, 0x3a, 0xca, 0x8f, 0x00, 0xd8, 0xec, 0x74, 0x67, 0xef, 0x61, 0xe0, 0x28, + 0xd0, 0x96, 0xf4, 0x48, 0xde, 0x81, 0xe3, 0xef, 0xdc, 0xaa, 0x7d, 0xf3, 0xb6, 0x55, 0xa6, 0x65 + ]) + ) ) v.append( - aff(fe([0xeb, 0xcb, 0xc5, 0x70, 0x91, 0x31, 0x10, 0x93, 0x0d, 0xc8, 0xd0, 0xef, 0x62, 0xe8, 0x6f, 0x82, - 0xe3, 0x69, 0x3d, 0x91, 0x7f, 0x31, 0xe1, 0x26, 0x35, 0x3c, 0x4a, 0x2f, 0xab, 0xc4, 0x9a, 0x5e]) , - fe([0xab, 0x1b, 0xb5, 0xe5, 0x2b, 0xc3, 0x0e, 0x29, 0xb0, 0xd0, 0x73, 0xe6, 0x4f, 0x64, 0xf2, 0xbc, - 0xe4, 0xe4, 0xe1, 0x9a, 0x52, 0x33, 0x2f, 0xbd, 0xcc, 0x03, 0xee, 0x8a, 0xfa, 0x00, 0x5f, 0x50])) + aff( + fe([ + 0xeb, 0xcb, 0xc5, 0x70, 0x91, 0x31, 0x10, 0x93, 0x0d, 0xc8, 0xd0, 0xef, 0x62, 0xe8, 0x6f, 0x82, + 0xe3, 0x69, 0x3d, 0x91, 0x7f, 0x31, 0xe1, 0x26, 0x35, 0x3c, 0x4a, 0x2f, 0xab, 0xc4, 0x9a, 0x5e + ]), + fe([ + 0xab, 0x1b, 0xb5, 0xe5, 0x2b, 0xc3, 0x0e, 0x29, 0xb0, 0xd0, 0x73, 0xe6, 0x4f, 0x64, 0xf2, 0xbc, + 0xe4, 0xe4, 0xe1, 0x9a, 0x52, 0x33, 0x2f, 0xbd, 0xcc, 0x03, 0xee, 0x8a, 0xfa, 0x00, 0x5f, 0x50 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xf6, 0xdb, 0x0d, 0x22, 0x3d, 0xb5, 0x14, 0x75, 0x31, 0xf0, 0x81, 0xe2, 0xb9, 0x37, 0xa2, 0xa9, - 0x84, 0x11, 0x9a, 0x07, 0xb5, 0x53, 0x89, 0x78, 0xa9, 0x30, 0x27, 0xa1, 0xf1, 0x4e, 0x5c, 0x2e]) , - fe([0x8b, 0x00, 0x54, 0xfb, 0x4d, 0xdc, 0xcb, 0x17, 0x35, 0x40, 0xff, 0xb7, 0x8c, 0xfe, 0x4a, 0xe4, - 0x4e, 0x99, 0x4e, 0xa8, 0x74, 0x54, 0x5d, 0x5c, 0x96, 0xa3, 0x12, 0x55, 0x36, 0x31, 0x17, 0x5c])) + aff( + fe([ + 0xf6, 0xdb, 0x0d, 0x22, 0x3d, 0xb5, 0x14, 0x75, 0x31, 0xf0, 0x81, 0xe2, 0xb9, 0x37, 0xa2, 0xa9, + 0x84, 0x11, 0x9a, 0x07, 0xb5, 0x53, 0x89, 0x78, 0xa9, 0x30, 0x27, 0xa1, 0xf1, 0x4e, 0x5c, 0x2e + ]), + fe([ + 0x8b, 0x00, 0x54, 0xfb, 0x4d, 0xdc, 0xcb, 0x17, 0x35, 0x40, 0xff, 0xb7, 0x8c, 0xfe, 0x4a, 0xe4, + 0x4e, 0x99, 0x4e, 0xa8, 0x74, 0x54, 0x5d, 0x5c, 0x96, 0xa3, 0x12, 0x55, 0x36, 0x31, 0x17, 0x5c + ]) + ) ) v.append( - aff(fe([0xce, 0x24, 0xef, 0x7b, 0x86, 0xf2, 0x0f, 0x77, 0xe8, 0x5c, 0x7d, 0x87, 0x38, 0x2d, 0xef, 0xaf, - 0xf2, 0x8c, 0x72, 0x2e, 0xeb, 0xb6, 0x55, 0x4b, 0x6e, 0xf1, 0x4e, 0x8a, 0x0e, 0x9a, 0x6c, 0x4c]) , - fe([0x25, 0xea, 0x86, 0xc2, 0xd1, 0x4f, 0xb7, 0x3e, 0xa8, 0x5c, 0x8d, 0x66, 0x81, 0x25, 0xed, 0xc5, - 0x4c, 0x05, 0xb9, 0xd8, 0xd6, 0x70, 0xbe, 0x73, 0x82, 0xe8, 0xa1, 0xe5, 0x1e, 0x71, 0xd5, 0x26])) + aff( + fe([ + 0xce, 0x24, 0xef, 0x7b, 0x86, 0xf2, 0x0f, 0x77, 0xe8, 0x5c, 0x7d, 0x87, 0x38, 0x2d, 0xef, 0xaf, + 0xf2, 0x8c, 0x72, 0x2e, 0xeb, 0xb6, 0x55, 0x4b, 0x6e, 0xf1, 0x4e, 0x8a, 0x0e, 0x9a, 0x6c, 0x4c + ]), + fe([ + 0x25, 0xea, 0x86, 0xc2, 0xd1, 0x4f, 0xb7, 0x3e, 0xa8, 0x5c, 0x8d, 0x66, 0x81, 0x25, 0xed, 0xc5, + 0x4c, 0x05, 0xb9, 0xd8, 0xd6, 0x70, 0xbe, 0x73, 0x82, 0xe8, 0xa1, 0xe5, 0x1e, 0x71, 0xd5, 0x26 + ]) + ) ) v.append( - aff(fe([0x4e, 0x6d, 0xc3, 0xa7, 0x4f, 0x22, 0x45, 0x26, 0xa2, 0x7e, 0x16, 0xf7, 0xf7, 0x63, 0xdc, 0x86, - 0x01, 0x2a, 0x71, 0x38, 0x5c, 0x33, 0xc3, 0xce, 0x30, 0xff, 0xf9, 0x2c, 0x91, 0x71, 0x8a, 0x72]) , - fe([0x8c, 0x44, 0x09, 0x28, 0xd5, 0x23, 0xc9, 0x8f, 0xf3, 0x84, 0x45, 0xc6, 0x9a, 0x5e, 0xff, 0xd2, - 0xc7, 0x57, 0x93, 0xa3, 0xc1, 0x69, 0xdd, 0x62, 0x0f, 0xda, 0x5c, 0x30, 0x59, 0x5d, 0xe9, 0x4c])) + aff( + fe([ + 0x4e, 0x6d, 0xc3, 0xa7, 0x4f, 0x22, 0x45, 0x26, 0xa2, 0x7e, 0x16, 0xf7, 0xf7, 0x63, 0xdc, 0x86, + 0x01, 0x2a, 0x71, 0x38, 0x5c, 0x33, 0xc3, 0xce, 0x30, 0xff, 0xf9, 0x2c, 0x91, 0x71, 0x8a, 0x72 + ]), + fe([ + 0x8c, 0x44, 0x09, 0x28, 0xd5, 0x23, 0xc9, 0x8f, 0xf3, 0x84, 0x45, 0xc6, 0x9a, 0x5e, 0xff, 0xd2, + 0xc7, 0x57, 0x93, 0xa3, 0xc1, 0x69, 0xdd, 0x62, 0x0f, 0xda, 0x5c, 0x30, 0x59, 0x5d, 0xe9, 0x4c + ]) + ) ) v.append( - aff(fe([0x92, 0x7e, 0x50, 0x27, 0x72, 0xd7, 0x0c, 0xd6, 0x69, 0x96, 0x81, 0x35, 0x84, 0x94, 0x35, 0x8b, - 0x6c, 0xaa, 0x62, 0x86, 0x6e, 0x1c, 0x15, 0xf3, 0x6c, 0xb3, 0xff, 0x65, 0x1b, 0xa2, 0x9b, 0x59]) , - fe([0xe2, 0xa9, 0x65, 0x88, 0xc4, 0x50, 0xfa, 0xbb, 0x3b, 0x6e, 0x5f, 0x44, 0x01, 0xca, 0x97, 0xd4, - 0xdd, 0xf6, 0xcd, 0x3f, 0x3f, 0xe5, 0x97, 0x67, 0x2b, 0x8c, 0x66, 0x0f, 0x35, 0x9b, 0xf5, 0x07])) + aff( + fe([ + 0x92, 0x7e, 0x50, 0x27, 0x72, 0xd7, 0x0c, 0xd6, 0x69, 0x96, 0x81, 0x35, 0x84, 0x94, 0x35, 0x8b, + 0x6c, 0xaa, 0x62, 0x86, 0x6e, 0x1c, 0x15, 0xf3, 0x6c, 0xb3, 0xff, 0x65, 0x1b, 0xa2, 0x9b, 0x59 + ]), + fe([ + 0xe2, 0xa9, 0x65, 0x88, 0xc4, 0x50, 0xfa, 0xbb, 0x3b, 0x6e, 0x5f, 0x44, 0x01, 0xca, 0x97, 0xd4, + 0xdd, 0xf6, 0xcd, 0x3f, 0x3f, 0xe5, 0x97, 0x67, 0x2b, 0x8c, 0x66, 0x0f, 0x35, 0x9b, 0xf5, 0x07 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xf1, 0x59, 0x27, 0xd8, 0xdb, 0x5a, 0x11, 0x5e, 0x82, 0xf3, 0x38, 0xff, 0x1c, 0xed, 0xfe, 0x3f, - 0x64, 0x54, 0x3f, 0x7f, 0xd1, 0x81, 0xed, 0xef, 0x65, 0xc5, 0xcb, 0xfd, 0xe1, 0x80, 0xcd, 0x11]) , - fe([0xe0, 0xdb, 0x22, 0x28, 0xe6, 0xff, 0x61, 0x9d, 0x41, 0x14, 0x2d, 0x3b, 0x26, 0x22, 0xdf, 0xf1, - 0x34, 0x81, 0xe9, 0x45, 0xee, 0x0f, 0x98, 0x8b, 0xa6, 0x3f, 0xef, 0xf7, 0x43, 0x19, 0xf1, 0x43])) + aff( + fe([ + 0xf1, 0x59, 0x27, 0xd8, 0xdb, 0x5a, 0x11, 0x5e, 0x82, 0xf3, 0x38, 0xff, 0x1c, 0xed, 0xfe, 0x3f, + 0x64, 0x54, 0x3f, 0x7f, 0xd1, 0x81, 0xed, 0xef, 0x65, 0xc5, 0xcb, 0xfd, 0xe1, 0x80, 0xcd, 0x11 + ]), + fe([ + 0xe0, 0xdb, 0x22, 0x28, 0xe6, 0xff, 0x61, 0x9d, 0x41, 0x14, 0x2d, 0x3b, 0x26, 0x22, 0xdf, 0xf1, + 0x34, 0x81, 0xe9, 0x45, 0xee, 0x0f, 0x98, 0x8b, 0xa6, 0x3f, 0xef, 0xf7, 0x43, 0x19, 0xf1, 0x43 + ]) + ) ) v.append( - aff(fe([0xee, 0xf3, 0x00, 0xa1, 0x50, 0xde, 0xc0, 0xb6, 0x01, 0xe3, 0x8c, 0x3c, 0x4d, 0x31, 0xd2, 0xb0, - 0x58, 0xcd, 0xed, 0x10, 0x4a, 0x7a, 0xef, 0x80, 0xa9, 0x19, 0x32, 0xf3, 0xd8, 0x33, 0x8c, 0x06]) , - fe([0xcb, 0x7d, 0x4f, 0xff, 0x30, 0xd8, 0x12, 0x3b, 0x39, 0x1c, 0x06, 0xf9, 0x4c, 0x34, 0x35, 0x71, - 0xb5, 0x16, 0x94, 0x67, 0xdf, 0xee, 0x11, 0xde, 0xa4, 0x1d, 0x88, 0x93, 0x35, 0xa9, 0x32, 0x10])) + aff( + fe([ + 0xee, 0xf3, 0x00, 0xa1, 0x50, 0xde, 0xc0, 0xb6, 0x01, 0xe3, 0x8c, 0x3c, 0x4d, 0x31, 0xd2, 0xb0, + 0x58, 0xcd, 0xed, 0x10, 0x4a, 0x7a, 0xef, 0x80, 0xa9, 0x19, 0x32, 0xf3, 0xd8, 0x33, 0x8c, 0x06 + ]), + fe([ + 0xcb, 0x7d, 0x4f, 0xff, 0x30, 0xd8, 0x12, 0x3b, 0x39, 0x1c, 0x06, 0xf9, 0x4c, 0x34, 0x35, 0x71, + 0xb5, 0x16, 0x94, 0x67, 0xdf, 0xee, 0x11, 0xde, 0xa4, 0x1d, 0x88, 0x93, 0x35, 0xa9, 0x32, 0x10 + ]) + ) ) v.append( - aff(fe([0xe9, 0xc3, 0xbc, 0x7b, 0x5c, 0xfc, 0xb2, 0xf9, 0xc9, 0x2f, 0xe5, 0xba, 0x3a, 0x0b, 0xab, 0x64, - 0x38, 0x6f, 0x5b, 0x4b, 0x93, 0xda, 0x64, 0xec, 0x4d, 0x3d, 0xa0, 0xf5, 0xbb, 0xba, 0x47, 0x48]) , - fe([0x60, 0xbc, 0x45, 0x1f, 0x23, 0xa2, 0x3b, 0x70, 0x76, 0xe6, 0x97, 0x99, 0x4f, 0x77, 0x54, 0x67, - 0x30, 0x9a, 0xe7, 0x66, 0xd6, 0xcd, 0x2e, 0x51, 0x24, 0x2c, 0x42, 0x4a, 0x11, 0xfe, 0x6f, 0x7e])) + aff( + fe([ + 0xe9, 0xc3, 0xbc, 0x7b, 0x5c, 0xfc, 0xb2, 0xf9, 0xc9, 0x2f, 0xe5, 0xba, 0x3a, 0x0b, 0xab, 0x64, + 0x38, 0x6f, 0x5b, 0x4b, 0x93, 0xda, 0x64, 0xec, 0x4d, 0x3d, 0xa0, 0xf5, 0xbb, 0xba, 0x47, 0x48 + ]), + fe([ + 0x60, 0xbc, 0x45, 0x1f, 0x23, 0xa2, 0x3b, 0x70, 0x76, 0xe6, 0x97, 0x99, 0x4f, 0x77, 0x54, 0x67, + 0x30, 0x9a, 0xe7, 0x66, 0xd6, 0xcd, 0x2e, 0x51, 0x24, 0x2c, 0x42, 0x4a, 0x11, 0xfe, 0x6f, 0x7e + ]) + ) ) v.append( - aff(fe([0x87, 0xc0, 0xb1, 0xf0, 0xa3, 0x6f, 0x0c, 0x93, 0xa9, 0x0a, 0x72, 0xef, 0x5c, 0xbe, 0x65, 0x35, - 0xa7, 0x6a, 0x4e, 0x2c, 0xbf, 0x21, 0x23, 0xe8, 0x2f, 0x97, 0xc7, 0x3e, 0xc8, 0x17, 0xac, 0x1e]) , - fe([0x7b, 0xef, 0x21, 0xe5, 0x40, 0xcc, 0x1e, 0xdc, 0xd6, 0xbd, 0x97, 0x7a, 0x7c, 0x75, 0x86, 0x7a, - 0x25, 0x5a, 0x6e, 0x7c, 0xe5, 0x51, 0x3c, 0x1b, 0x5b, 0x82, 0x9a, 0x07, 0x60, 0xa1, 0x19, 0x04])) + aff( + fe([ + 0x87, 0xc0, 0xb1, 0xf0, 0xa3, 0x6f, 0x0c, 0x93, 0xa9, 0x0a, 0x72, 0xef, 0x5c, 0xbe, 0x65, 0x35, + 0xa7, 0x6a, 0x4e, 0x2c, 0xbf, 0x21, 0x23, 0xe8, 0x2f, 0x97, 0xc7, 0x3e, 0xc8, 0x17, 0xac, 0x1e + ]), + fe([ + 0x7b, 0xef, 0x21, 0xe5, 0x40, 0xcc, 0x1e, 0xdc, 0xd6, 0xbd, 0x97, 0x7a, 0x7c, 0x75, 0x86, 0x7a, + 0x25, 0x5a, 0x6e, 0x7c, 0xe5, 0x51, 0x3c, 0x1b, 0x5b, 0x82, 0x9a, 0x07, 0x60, 0xa1, 0x19, 0x04 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x96, 0x88, 0xa6, 0xab, 0x8f, 0xe3, 0x3a, 0x49, 0xf8, 0xfe, 0x34, 0xe7, 0x6a, 0xb2, 0xfe, 0x40, - 0x26, 0x74, 0x57, 0x4c, 0xf6, 0xd4, 0x99, 0xce, 0x5d, 0x7b, 0x2f, 0x67, 0xd6, 0x5a, 0xe4, 0x4e]) , - fe([0x5c, 0x82, 0xb3, 0xbd, 0x55, 0x25, 0xf6, 0x6a, 0x93, 0xa4, 0x02, 0xc6, 0x7d, 0x5c, 0xb1, 0x2b, - 0x5b, 0xff, 0xfb, 0x56, 0xf8, 0x01, 0x41, 0x90, 0xc6, 0xb6, 0xac, 0x4f, 0xfe, 0xa7, 0x41, 0x70])) + aff( + fe([ + 0x96, 0x88, 0xa6, 0xab, 0x8f, 0xe3, 0x3a, 0x49, 0xf8, 0xfe, 0x34, 0xe7, 0x6a, 0xb2, 0xfe, 0x40, + 0x26, 0x74, 0x57, 0x4c, 0xf6, 0xd4, 0x99, 0xce, 0x5d, 0x7b, 0x2f, 0x67, 0xd6, 0x5a, 0xe4, 0x4e + ]), + fe([ + 0x5c, 0x82, 0xb3, 0xbd, 0x55, 0x25, 0xf6, 0x6a, 0x93, 0xa4, 0x02, 0xc6, 0x7d, 0x5c, 0xb1, 0x2b, + 0x5b, 0xff, 0xfb, 0x56, 0xf8, 0x01, 0x41, 0x90, 0xc6, 0xb6, 0xac, 0x4f, 0xfe, 0xa7, 0x41, 0x70 + ]) + ) ) v.append( - aff(fe([0xdb, 0xfa, 0x9b, 0x2c, 0xd4, 0x23, 0x67, 0x2c, 0x8a, 0x63, 0x6c, 0x07, 0x26, 0x48, 0x4f, 0xc2, - 0x03, 0xd2, 0x53, 0x20, 0x28, 0xed, 0x65, 0x71, 0x47, 0xa9, 0x16, 0x16, 0x12, 0xbc, 0x28, 0x33]) , - fe([0x39, 0xc0, 0xfa, 0xfa, 0xcd, 0x33, 0x43, 0xc7, 0x97, 0x76, 0x9b, 0x93, 0x91, 0x72, 0xeb, 0xc5, - 0x18, 0x67, 0x4c, 0x11, 0xf0, 0xf4, 0xe5, 0x73, 0xb2, 0x5c, 0x1b, 0xc2, 0x26, 0x3f, 0xbf, 0x2b])) + aff( + fe([ + 0xdb, 0xfa, 0x9b, 0x2c, 0xd4, 0x23, 0x67, 0x2c, 0x8a, 0x63, 0x6c, 0x07, 0x26, 0x48, 0x4f, 0xc2, + 0x03, 0xd2, 0x53, 0x20, 0x28, 0xed, 0x65, 0x71, 0x47, 0xa9, 0x16, 0x16, 0x12, 0xbc, 0x28, 0x33 + ]), + fe([ + 0x39, 0xc0, 0xfa, 0xfa, 0xcd, 0x33, 0x43, 0xc7, 0x97, 0x76, 0x9b, 0x93, 0x91, 0x72, 0xeb, 0xc5, + 0x18, 0x67, 0x4c, 0x11, 0xf0, 0xf4, 0xe5, 0x73, 0xb2, 0x5c, 0x1b, 0xc2, 0x26, 0x3f, 0xbf, 0x2b + ]) + ) ) v.append( - aff(fe([0x86, 0xe6, 0x8c, 0x1d, 0xdf, 0xca, 0xfc, 0xd5, 0xf8, 0x3a, 0xc3, 0x44, 0x72, 0xe6, 0x78, 0x9d, - 0x2b, 0x97, 0xf8, 0x28, 0x45, 0xb4, 0x20, 0xc9, 0x2a, 0x8c, 0x67, 0xaa, 0x11, 0xc5, 0x5b, 0x2f]) , - fe([0x17, 0x0f, 0x86, 0x52, 0xd7, 0x9d, 0xc3, 0x44, 0x51, 0x76, 0x32, 0x65, 0xb4, 0x37, 0x81, 0x99, - 0x46, 0x37, 0x62, 0xed, 0xcf, 0x64, 0x9d, 0x72, 0x40, 0x7a, 0x4c, 0x0b, 0x76, 0x2a, 0xfb, 0x56])) + aff( + fe([ + 0x86, 0xe6, 0x8c, 0x1d, 0xdf, 0xca, 0xfc, 0xd5, 0xf8, 0x3a, 0xc3, 0x44, 0x72, 0xe6, 0x78, 0x9d, + 0x2b, 0x97, 0xf8, 0x28, 0x45, 0xb4, 0x20, 0xc9, 0x2a, 0x8c, 0x67, 0xaa, 0x11, 0xc5, 0x5b, 0x2f + ]), + fe([ + 0x17, 0x0f, 0x86, 0x52, 0xd7, 0x9d, 0xc3, 0x44, 0x51, 0x76, 0x32, 0x65, 0xb4, 0x37, 0x81, 0x99, + 0x46, 0x37, 0x62, 0xed, 0xcf, 0x64, 0x9d, 0x72, 0x40, 0x7a, 0x4c, 0x0b, 0x76, 0x2a, 0xfb, 0x56 + ]) + ) ) v.append( - aff(fe([0x33, 0xa7, 0x90, 0x7c, 0xc3, 0x6f, 0x17, 0xa5, 0xa0, 0x67, 0x72, 0x17, 0xea, 0x7e, 0x63, 0x14, - 0x83, 0xde, 0xc1, 0x71, 0x2d, 0x41, 0x32, 0x7a, 0xf3, 0xd1, 0x2b, 0xd8, 0x2a, 0xa6, 0x46, 0x36]) , - fe([0xac, 0xcc, 0x6b, 0x7c, 0xf9, 0xb8, 0x8b, 0x08, 0x5c, 0xd0, 0x7d, 0x8f, 0x73, 0xea, 0x20, 0xda, - 0x86, 0xca, 0x00, 0xc7, 0xad, 0x73, 0x4d, 0xe9, 0xe8, 0xa9, 0xda, 0x1f, 0x03, 0x06, 0xdd, 0x24])) + aff( + fe([ + 0x33, 0xa7, 0x90, 0x7c, 0xc3, 0x6f, 0x17, 0xa5, 0xa0, 0x67, 0x72, 0x17, 0xea, 0x7e, 0x63, 0x14, + 0x83, 0xde, 0xc1, 0x71, 0x2d, 0x41, 0x32, 0x7a, 0xf3, 0xd1, 0x2b, 0xd8, 0x2a, 0xa6, 0x46, 0x36 + ]), + fe([ + 0xac, 0xcc, 0x6b, 0x7c, 0xf9, 0xb8, 0x8b, 0x08, 0x5c, 0xd0, 0x7d, 0x8f, 0x73, 0xea, 0x20, 0xda, + 0x86, 0xca, 0x00, 0xc7, 0xad, 0x73, 0x4d, 0xe9, 0xe8, 0xa9, 0xda, 0x1f, 0x03, 0x06, 0xdd, 0x24 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x9c, 0xb2, 0x61, 0x0a, 0x98, 0x2a, 0xa5, 0xd7, 0xee, 0xa9, 0xac, 0x65, 0xcb, 0x0a, 0x1e, 0xe2, - 0xbe, 0xdc, 0x85, 0x59, 0x0f, 0x9c, 0xa6, 0x57, 0x34, 0xa5, 0x87, 0xeb, 0x7b, 0x1e, 0x0c, 0x3c]) , - fe([0x2f, 0xbd, 0x84, 0x63, 0x0d, 0xb5, 0xa0, 0xf0, 0x4b, 0x9e, 0x93, 0xc6, 0x34, 0x9a, 0x34, 0xff, - 0x73, 0x19, 0x2f, 0x6e, 0x54, 0x45, 0x2c, 0x92, 0x31, 0x76, 0x34, 0xf1, 0xb2, 0x26, 0xe8, 0x74])) + aff( + fe([ + 0x9c, 0xb2, 0x61, 0x0a, 0x98, 0x2a, 0xa5, 0xd7, 0xee, 0xa9, 0xac, 0x65, 0xcb, 0x0a, 0x1e, 0xe2, + 0xbe, 0xdc, 0x85, 0x59, 0x0f, 0x9c, 0xa6, 0x57, 0x34, 0xa5, 0x87, 0xeb, 0x7b, 0x1e, 0x0c, 0x3c + ]), + fe([ + 0x2f, 0xbd, 0x84, 0x63, 0x0d, 0xb5, 0xa0, 0xf0, 0x4b, 0x9e, 0x93, 0xc6, 0x34, 0x9a, 0x34, 0xff, + 0x73, 0x19, 0x2f, 0x6e, 0x54, 0x45, 0x2c, 0x92, 0x31, 0x76, 0x34, 0xf1, 0xb2, 0x26, 0xe8, 0x74 + ]) + ) ) v.append( - aff(fe([0x0a, 0x67, 0x90, 0x6d, 0x0c, 0x4c, 0xcc, 0xc0, 0xe6, 0xbd, 0xa7, 0x5e, 0x55, 0x8c, 0xcd, 0x58, - 0x9b, 0x11, 0xa2, 0xbb, 0x4b, 0xb1, 0x43, 0x04, 0x3c, 0x55, 0xed, 0x23, 0xfe, 0xcd, 0xb1, 0x53]) , - fe([0x05, 0xfb, 0x75, 0xf5, 0x01, 0xaf, 0x38, 0x72, 0x58, 0xfc, 0x04, 0x29, 0x34, 0x7a, 0x67, 0xa2, - 0x08, 0x50, 0x6e, 0xd0, 0x2b, 0x73, 0xd5, 0xb8, 0xe4, 0x30, 0x96, 0xad, 0x45, 0xdf, 0xa6, 0x5c])) + aff( + fe([ + 0x0a, 0x67, 0x90, 0x6d, 0x0c, 0x4c, 0xcc, 0xc0, 0xe6, 0xbd, 0xa7, 0x5e, 0x55, 0x8c, 0xcd, 0x58, + 0x9b, 0x11, 0xa2, 0xbb, 0x4b, 0xb1, 0x43, 0x04, 0x3c, 0x55, 0xed, 0x23, 0xfe, 0xcd, 0xb1, 0x53 + ]), + fe([ + 0x05, 0xfb, 0x75, 0xf5, 0x01, 0xaf, 0x38, 0x72, 0x58, 0xfc, 0x04, 0x29, 0x34, 0x7a, 0x67, 0xa2, + 0x08, 0x50, 0x6e, 0xd0, 0x2b, 0x73, 0xd5, 0xb8, 0xe4, 0x30, 0x96, 0xad, 0x45, 0xdf, 0xa6, 0x5c + ]) + ) ) v.append( - aff(fe([0x0d, 0x88, 0x1a, 0x90, 0x7e, 0xdc, 0xd8, 0xfe, 0xc1, 0x2f, 0x5d, 0x67, 0xee, 0x67, 0x2f, 0xed, - 0x6f, 0x55, 0x43, 0x5f, 0x87, 0x14, 0x35, 0x42, 0xd3, 0x75, 0xae, 0xd5, 0xd3, 0x85, 0x1a, 0x76]) , - fe([0x87, 0xc8, 0xa0, 0x6e, 0xe1, 0xb0, 0xad, 0x6a, 0x4a, 0x34, 0x71, 0xed, 0x7c, 0xd6, 0x44, 0x03, - 0x65, 0x4a, 0x5c, 0x5c, 0x04, 0xf5, 0x24, 0x3f, 0xb0, 0x16, 0x5e, 0x8c, 0xb2, 0xd2, 0xc5, 0x20])) + aff( + fe([ + 0x0d, 0x88, 0x1a, 0x90, 0x7e, 0xdc, 0xd8, 0xfe, 0xc1, 0x2f, 0x5d, 0x67, 0xee, 0x67, 0x2f, 0xed, + 0x6f, 0x55, 0x43, 0x5f, 0x87, 0x14, 0x35, 0x42, 0xd3, 0x75, 0xae, 0xd5, 0xd3, 0x85, 0x1a, 0x76 + ]), + fe([ + 0x87, 0xc8, 0xa0, 0x6e, 0xe1, 0xb0, 0xad, 0x6a, 0x4a, 0x34, 0x71, 0xed, 0x7c, 0xd6, 0x44, 0x03, + 0x65, 0x4a, 0x5c, 0x5c, 0x04, 0xf5, 0x24, 0x3f, 0xb0, 0x16, 0x5e, 0x8c, 0xb2, 0xd2, 0xc5, 0x20 + ]) + ) ) v.append( - aff(fe([0x98, 0x83, 0xc2, 0x37, 0xa0, 0x41, 0xa8, 0x48, 0x5c, 0x5f, 0xbf, 0xc8, 0xfa, 0x24, 0xe0, 0x59, - 0x2c, 0xbd, 0xf6, 0x81, 0x7e, 0x88, 0xe6, 0xca, 0x04, 0xd8, 0x5d, 0x60, 0xbb, 0x74, 0xa7, 0x0b]) , - fe([0x21, 0x13, 0x91, 0xbf, 0x77, 0x7a, 0x33, 0xbc, 0xe9, 0x07, 0x39, 0x0a, 0xdd, 0x7d, 0x06, 0x10, - 0x9a, 0xee, 0x47, 0x73, 0x1b, 0x15, 0x5a, 0xfb, 0xcd, 0x4d, 0xd0, 0xd2, 0x3a, 0x01, 0xba, 0x54])) + aff( + fe([ + 0x98, 0x83, 0xc2, 0x37, 0xa0, 0x41, 0xa8, 0x48, 0x5c, 0x5f, 0xbf, 0xc8, 0xfa, 0x24, 0xe0, 0x59, + 0x2c, 0xbd, 0xf6, 0x81, 0x7e, 0x88, 0xe6, 0xca, 0x04, 0xd8, 0x5d, 0x60, 0xbb, 0x74, 0xa7, 0x0b + ]), + fe([ + 0x21, 0x13, 0x91, 0xbf, 0x77, 0x7a, 0x33, 0xbc, 0xe9, 0x07, 0x39, 0x0a, 0xdd, 0x7d, 0x06, 0x10, + 0x9a, 0xee, 0x47, 0x73, 0x1b, 0x15, 0x5a, 0xfb, 0xcd, 0x4d, 0xd0, 0xd2, 0x3a, 0x01, 0xba, 0x54 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x48, 0xd5, 0x39, 0x4a, 0x0b, 0x20, 0x6a, 0x43, 0xa0, 0x07, 0x82, 0x5e, 0x49, 0x7c, 0xc9, 0x47, - 0xf1, 0x7c, 0x37, 0xb9, 0x23, 0xef, 0x6b, 0x46, 0x45, 0x8c, 0x45, 0x76, 0xdf, 0x14, 0x6b, 0x6e]) , - fe([0x42, 0xc9, 0xca, 0x29, 0x4c, 0x76, 0x37, 0xda, 0x8a, 0x2d, 0x7c, 0x3a, 0x58, 0xf2, 0x03, 0xb4, - 0xb5, 0xb9, 0x1a, 0x13, 0x2d, 0xde, 0x5f, 0x6b, 0x9d, 0xba, 0x52, 0xc9, 0x5d, 0xb3, 0xf3, 0x30])) + aff( + fe([ + 0x48, 0xd5, 0x39, 0x4a, 0x0b, 0x20, 0x6a, 0x43, 0xa0, 0x07, 0x82, 0x5e, 0x49, 0x7c, 0xc9, 0x47, + 0xf1, 0x7c, 0x37, 0xb9, 0x23, 0xef, 0x6b, 0x46, 0x45, 0x8c, 0x45, 0x76, 0xdf, 0x14, 0x6b, 0x6e + ]), + fe([ + 0x42, 0xc9, 0xca, 0x29, 0x4c, 0x76, 0x37, 0xda, 0x8a, 0x2d, 0x7c, 0x3a, 0x58, 0xf2, 0x03, 0xb4, + 0xb5, 0xb9, 0x1a, 0x13, 0x2d, 0xde, 0x5f, 0x6b, 0x9d, 0xba, 0x52, 0xc9, 0x5d, 0xb3, 0xf3, 0x30 + ]) + ) ) v.append( - aff(fe([0x4c, 0x6f, 0xfe, 0x6b, 0x0c, 0x62, 0xd7, 0x48, 0x71, 0xef, 0xb1, 0x85, 0x79, 0xc0, 0xed, 0x24, - 0xb1, 0x08, 0x93, 0x76, 0x8e, 0xf7, 0x38, 0x8e, 0xeb, 0xfe, 0x80, 0x40, 0xaf, 0x90, 0x64, 0x49]) , - fe([0x4a, 0x88, 0xda, 0xc1, 0x98, 0x44, 0x3c, 0x53, 0x4e, 0xdb, 0x4b, 0xb9, 0x12, 0x5f, 0xcd, 0x08, - 0x04, 0xef, 0x75, 0xe7, 0xb1, 0x3a, 0xe5, 0x07, 0xfa, 0xca, 0x65, 0x7b, 0x72, 0x10, 0x64, 0x7f])) + aff( + fe([ + 0x4c, 0x6f, 0xfe, 0x6b, 0x0c, 0x62, 0xd7, 0x48, 0x71, 0xef, 0xb1, 0x85, 0x79, 0xc0, 0xed, 0x24, + 0xb1, 0x08, 0x93, 0x76, 0x8e, 0xf7, 0x38, 0x8e, 0xeb, 0xfe, 0x80, 0x40, 0xaf, 0x90, 0x64, 0x49 + ]), + fe([ + 0x4a, 0x88, 0xda, 0xc1, 0x98, 0x44, 0x3c, 0x53, 0x4e, 0xdb, 0x4b, 0xb9, 0x12, 0x5f, 0xcd, 0x08, + 0x04, 0xef, 0x75, 0xe7, 0xb1, 0x3a, 0xe5, 0x07, 0xfa, 0xca, 0x65, 0x7b, 0x72, 0x10, 0x64, 0x7f + ]) + ) ) v.append( - aff(fe([0x3d, 0x81, 0xf0, 0xeb, 0x16, 0xfd, 0x58, 0x33, 0x8d, 0x7c, 0x1a, 0xfb, 0x20, 0x2c, 0x8a, 0xee, - 0x90, 0xbb, 0x33, 0x6d, 0x45, 0xe9, 0x8e, 0x99, 0x85, 0xe1, 0x08, 0x1f, 0xc5, 0xf1, 0xb5, 0x46]) , - fe([0xe4, 0xe7, 0x43, 0x4b, 0xa0, 0x3f, 0x2b, 0x06, 0xba, 0x17, 0xae, 0x3d, 0xe6, 0xce, 0xbd, 0xb8, - 0xed, 0x74, 0x11, 0x35, 0xec, 0x96, 0xfe, 0x31, 0xe3, 0x0e, 0x7a, 0x4e, 0xc9, 0x1d, 0xcb, 0x20])) + aff( + fe([ + 0x3d, 0x81, 0xf0, 0xeb, 0x16, 0xfd, 0x58, 0x33, 0x8d, 0x7c, 0x1a, 0xfb, 0x20, 0x2c, 0x8a, 0xee, + 0x90, 0xbb, 0x33, 0x6d, 0x45, 0xe9, 0x8e, 0x99, 0x85, 0xe1, 0x08, 0x1f, 0xc5, 0xf1, 0xb5, 0x46 + ]), + fe([ + 0xe4, 0xe7, 0x43, 0x4b, 0xa0, 0x3f, 0x2b, 0x06, 0xba, 0x17, 0xae, 0x3d, 0xe6, 0xce, 0xbd, 0xb8, + 0xed, 0x74, 0x11, 0x35, 0xec, 0x96, 0xfe, 0x31, 0xe3, 0x0e, 0x7a, 0x4e, 0xc9, 0x1d, 0xcb, 0x20 + ]) + ) ) v.append( - aff(fe([0xe0, 0x67, 0xe9, 0x7b, 0xdb, 0x96, 0x5c, 0xb0, 0x32, 0xd0, 0x59, 0x31, 0x90, 0xdc, 0x92, 0x97, - 0xac, 0x09, 0x38, 0x31, 0x0f, 0x7e, 0xd6, 0x5d, 0xd0, 0x06, 0xb6, 0x1f, 0xea, 0xf0, 0x5b, 0x07]) , - fe([0x81, 0x9f, 0xc7, 0xde, 0x6b, 0x41, 0x22, 0x35, 0x14, 0x67, 0x77, 0x3e, 0x90, 0x81, 0xb0, 0xd9, - 0x85, 0x4c, 0xca, 0x9b, 0x3f, 0x04, 0x59, 0xd6, 0xaa, 0x17, 0xc3, 0x88, 0x34, 0x37, 0xba, 0x43])) + aff( + fe([ + 0xe0, 0x67, 0xe9, 0x7b, 0xdb, 0x96, 0x5c, 0xb0, 0x32, 0xd0, 0x59, 0x31, 0x90, 0xdc, 0x92, 0x97, + 0xac, 0x09, 0x38, 0x31, 0x0f, 0x7e, 0xd6, 0x5d, 0xd0, 0x06, 0xb6, 0x1f, 0xea, 0xf0, 0x5b, 0x07 + ]), + fe([ + 0x81, 0x9f, 0xc7, 0xde, 0x6b, 0x41, 0x22, 0x35, 0x14, 0x67, 0x77, 0x3e, 0x90, 0x81, 0xb0, 0xd9, + 0x85, 0x4c, 0xca, 0x9b, 0x3f, 0x04, 0x59, 0xd6, 0xaa, 0x17, 0xc3, 0x88, 0x34, 0x37, 0xba, 0x43 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x4c, 0xb6, 0x69, 0xc8, 0x81, 0x95, 0x94, 0x33, 0x92, 0x34, 0xe9, 0x3c, 0x84, 0x0d, 0x3d, 0x5a, - 0x37, 0x9c, 0x22, 0xa0, 0xaa, 0x65, 0xce, 0xb4, 0xc2, 0x2d, 0x66, 0x67, 0x02, 0xff, 0x74, 0x10]) , - fe([0x22, 0xb0, 0xd5, 0xe6, 0xc7, 0xef, 0xb1, 0xa7, 0x13, 0xda, 0x60, 0xb4, 0x80, 0xc1, 0x42, 0x7d, - 0x10, 0x70, 0x97, 0x04, 0x4d, 0xda, 0x23, 0x89, 0xc2, 0x0e, 0x68, 0xcb, 0xde, 0xe0, 0x9b, 0x29])) + aff( + fe([ + 0x4c, 0xb6, 0x69, 0xc8, 0x81, 0x95, 0x94, 0x33, 0x92, 0x34, 0xe9, 0x3c, 0x84, 0x0d, 0x3d, 0x5a, + 0x37, 0x9c, 0x22, 0xa0, 0xaa, 0x65, 0xce, 0xb4, 0xc2, 0x2d, 0x66, 0x67, 0x02, 0xff, 0x74, 0x10 + ]), + fe([ + 0x22, 0xb0, 0xd5, 0xe6, 0xc7, 0xef, 0xb1, 0xa7, 0x13, 0xda, 0x60, 0xb4, 0x80, 0xc1, 0x42, 0x7d, + 0x10, 0x70, 0x97, 0x04, 0x4d, 0xda, 0x23, 0x89, 0xc2, 0x0e, 0x68, 0xcb, 0xde, 0xe0, 0x9b, 0x29 + ]) + ) ) v.append( - aff(fe([0x33, 0xfe, 0x42, 0x2a, 0x36, 0x2b, 0x2e, 0x36, 0x64, 0x5c, 0x8b, 0xcc, 0x81, 0x6a, 0x15, 0x08, - 0xa1, 0x27, 0xe8, 0x57, 0xe5, 0x78, 0x8e, 0xf2, 0x58, 0x19, 0x12, 0x42, 0xae, 0xc4, 0x63, 0x3e]) , - fe([0x78, 0x96, 0x9c, 0xa7, 0xca, 0x80, 0xae, 0x02, 0x85, 0xb1, 0x7c, 0x04, 0x5c, 0xc1, 0x5b, 0x26, - 0xc1, 0xba, 0xed, 0xa5, 0x59, 0x70, 0x85, 0x8c, 0x8c, 0xe8, 0x87, 0xac, 0x6a, 0x28, 0x99, 0x35])) + aff( + fe([ + 0x33, 0xfe, 0x42, 0x2a, 0x36, 0x2b, 0x2e, 0x36, 0x64, 0x5c, 0x8b, 0xcc, 0x81, 0x6a, 0x15, 0x08, + 0xa1, 0x27, 0xe8, 0x57, 0xe5, 0x78, 0x8e, 0xf2, 0x58, 0x19, 0x12, 0x42, 0xae, 0xc4, 0x63, 0x3e + ]), + fe([ + 0x78, 0x96, 0x9c, 0xa7, 0xca, 0x80, 0xae, 0x02, 0x85, 0xb1, 0x7c, 0x04, 0x5c, 0xc1, 0x5b, 0x26, + 0xc1, 0xba, 0xed, 0xa5, 0x59, 0x70, 0x85, 0x8c, 0x8c, 0xe8, 0x87, 0xac, 0x6a, 0x28, 0x99, 0x35 + ]) + ) ) v.append( - aff(fe([0x9f, 0x04, 0x08, 0x28, 0xbe, 0x87, 0xda, 0x80, 0x28, 0x38, 0xde, 0x9f, 0xcd, 0xe4, 0xe3, 0x62, - 0xfb, 0x2e, 0x46, 0x8d, 0x01, 0xb3, 0x06, 0x51, 0xd4, 0x19, 0x3b, 0x11, 0xfa, 0xe2, 0xad, 0x1e]) , - fe([0xa0, 0x20, 0x99, 0x69, 0x0a, 0xae, 0xa3, 0x70, 0x4e, 0x64, 0x80, 0xb7, 0x85, 0x9c, 0x87, 0x54, - 0x43, 0x43, 0x55, 0x80, 0x6d, 0x8d, 0x7c, 0xa9, 0x64, 0xca, 0x6c, 0x2e, 0x21, 0xd8, 0xc8, 0x6c])) + aff( + fe([ + 0x9f, 0x04, 0x08, 0x28, 0xbe, 0x87, 0xda, 0x80, 0x28, 0x38, 0xde, 0x9f, 0xcd, 0xe4, 0xe3, 0x62, + 0xfb, 0x2e, 0x46, 0x8d, 0x01, 0xb3, 0x06, 0x51, 0xd4, 0x19, 0x3b, 0x11, 0xfa, 0xe2, 0xad, 0x1e + ]), + fe([ + 0xa0, 0x20, 0x99, 0x69, 0x0a, 0xae, 0xa3, 0x70, 0x4e, 0x64, 0x80, 0xb7, 0x85, 0x9c, 0x87, 0x54, + 0x43, 0x43, 0x55, 0x80, 0x6d, 0x8d, 0x7c, 0xa9, 0x64, 0xca, 0x6c, 0x2e, 0x21, 0xd8, 0xc8, 0x6c + ]) + ) ) v.append( - aff(fe([0x91, 0x4a, 0x07, 0xad, 0x08, 0x75, 0xc1, 0x4f, 0xa4, 0xb2, 0xc3, 0x6f, 0x46, 0x3e, 0xb1, 0xce, - 0x52, 0xab, 0x67, 0x09, 0x54, 0x48, 0x6b, 0x6c, 0xd7, 0x1d, 0x71, 0x76, 0xcb, 0xff, 0xdd, 0x31]) , - fe([0x36, 0x88, 0xfa, 0xfd, 0xf0, 0x36, 0x6f, 0x07, 0x74, 0x88, 0x50, 0xd0, 0x95, 0x38, 0x4a, 0x48, - 0x2e, 0x07, 0x64, 0x97, 0x11, 0x76, 0x01, 0x1a, 0x27, 0x4d, 0x8e, 0x25, 0x9a, 0x9b, 0x1c, 0x22])) + aff( + fe([ + 0x91, 0x4a, 0x07, 0xad, 0x08, 0x75, 0xc1, 0x4f, 0xa4, 0xb2, 0xc3, 0x6f, 0x46, 0x3e, 0xb1, 0xce, + 0x52, 0xab, 0x67, 0x09, 0x54, 0x48, 0x6b, 0x6c, 0xd7, 0x1d, 0x71, 0x76, 0xcb, 0xff, 0xdd, 0x31 + ]), + fe([ + 0x36, 0x88, 0xfa, 0xfd, 0xf0, 0x36, 0x6f, 0x07, 0x74, 0x88, 0x50, 0xd0, 0x95, 0x38, 0x4a, 0x48, + 0x2e, 0x07, 0x64, 0x97, 0x11, 0x76, 0x01, 0x1a, 0x27, 0x4d, 0x8e, 0x25, 0x9a, 0x9b, 0x1c, 0x22 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xbe, 0x57, 0xbd, 0x0e, 0x0f, 0xac, 0x5e, 0x76, 0xa3, 0x71, 0xad, 0x2b, 0x10, 0x45, 0x02, 0xec, - 0x59, 0xd5, 0x5d, 0xa9, 0x44, 0xcc, 0x25, 0x4c, 0xb3, 0x3c, 0x5b, 0x69, 0x07, 0x55, 0x26, 0x6b]) , - fe([0x30, 0x6b, 0xd4, 0xa7, 0x51, 0x29, 0xe3, 0xf9, 0x7a, 0x75, 0x2a, 0x82, 0x2f, 0xd6, 0x1d, 0x99, - 0x2b, 0x80, 0xd5, 0x67, 0x1e, 0x15, 0x9d, 0xca, 0xfd, 0xeb, 0xac, 0x97, 0x35, 0x09, 0x7f, 0x3f])) + aff( + fe([ + 0xbe, 0x57, 0xbd, 0x0e, 0x0f, 0xac, 0x5e, 0x76, 0xa3, 0x71, 0xad, 0x2b, 0x10, 0x45, 0x02, 0xec, + 0x59, 0xd5, 0x5d, 0xa9, 0x44, 0xcc, 0x25, 0x4c, 0xb3, 0x3c, 0x5b, 0x69, 0x07, 0x55, 0x26, 0x6b + ]), + fe([ + 0x30, 0x6b, 0xd4, 0xa7, 0x51, 0x29, 0xe3, 0xf9, 0x7a, 0x75, 0x2a, 0x82, 0x2f, 0xd6, 0x1d, 0x99, + 0x2b, 0x80, 0xd5, 0x67, 0x1e, 0x15, 0x9d, 0xca, 0xfd, 0xeb, 0xac, 0x97, 0x35, 0x09, 0x7f, 0x3f + ]) + ) ) v.append( - aff(fe([0x35, 0x0d, 0x34, 0x0a, 0xb8, 0x67, 0x56, 0x29, 0x20, 0xf3, 0x19, 0x5f, 0xe2, 0x83, 0x42, 0x73, - 0x53, 0xa8, 0xc5, 0x02, 0x19, 0x33, 0xb4, 0x64, 0xbd, 0xc3, 0x87, 0x8c, 0xd7, 0x76, 0xed, 0x25]) , - fe([0x47, 0x39, 0x37, 0x76, 0x0d, 0x1d, 0x0c, 0xf5, 0x5a, 0x6d, 0x43, 0x88, 0x99, 0x15, 0xb4, 0x52, - 0x0f, 0x2a, 0xb3, 0xb0, 0x3f, 0xa6, 0xb3, 0x26, 0xb3, 0xc7, 0x45, 0xf5, 0x92, 0x5f, 0x9b, 0x17])) + aff( + fe([ + 0x35, 0x0d, 0x34, 0x0a, 0xb8, 0x67, 0x56, 0x29, 0x20, 0xf3, 0x19, 0x5f, 0xe2, 0x83, 0x42, 0x73, + 0x53, 0xa8, 0xc5, 0x02, 0x19, 0x33, 0xb4, 0x64, 0xbd, 0xc3, 0x87, 0x8c, 0xd7, 0x76, 0xed, 0x25 + ]), + fe([ + 0x47, 0x39, 0x37, 0x76, 0x0d, 0x1d, 0x0c, 0xf5, 0x5a, 0x6d, 0x43, 0x88, 0x99, 0x15, 0xb4, 0x52, + 0x0f, 0x2a, 0xb3, 0xb0, 0x3f, 0xa6, 0xb3, 0x26, 0xb3, 0xc7, 0x45, 0xf5, 0x92, 0x5f, 0x9b, 0x17 + ]) + ) ) v.append( - aff(fe([0x9d, 0x23, 0xbd, 0x15, 0xfe, 0x52, 0x52, 0x15, 0x26, 0x79, 0x86, 0xba, 0x06, 0x56, 0x66, 0xbb, - 0x8c, 0x2e, 0x10, 0x11, 0xd5, 0x4a, 0x18, 0x52, 0xda, 0x84, 0x44, 0xf0, 0x3e, 0xe9, 0x8c, 0x35]) , - fe([0xad, 0xa0, 0x41, 0xec, 0xc8, 0x4d, 0xb9, 0xd2, 0x6e, 0x96, 0x4e, 0x5b, 0xc5, 0xc2, 0xa0, 0x1b, - 0xcf, 0x0c, 0xbf, 0x17, 0x66, 0x57, 0xc1, 0x17, 0x90, 0x45, 0x71, 0xc2, 0xe1, 0x24, 0xeb, 0x27])) + aff( + fe([ + 0x9d, 0x23, 0xbd, 0x15, 0xfe, 0x52, 0x52, 0x15, 0x26, 0x79, 0x86, 0xba, 0x06, 0x56, 0x66, 0xbb, + 0x8c, 0x2e, 0x10, 0x11, 0xd5, 0x4a, 0x18, 0x52, 0xda, 0x84, 0x44, 0xf0, 0x3e, 0xe9, 0x8c, 0x35 + ]), + fe([ + 0xad, 0xa0, 0x41, 0xec, 0xc8, 0x4d, 0xb9, 0xd2, 0x6e, 0x96, 0x4e, 0x5b, 0xc5, 0xc2, 0xa0, 0x1b, + 0xcf, 0x0c, 0xbf, 0x17, 0x66, 0x57, 0xc1, 0x17, 0x90, 0x45, 0x71, 0xc2, 0xe1, 0x24, 0xeb, 0x27 + ]) + ) ) v.append( - aff(fe([0x2c, 0xb9, 0x42, 0xa4, 0xaf, 0x3b, 0x42, 0x0e, 0xc2, 0x0f, 0xf2, 0xea, 0x83, 0xaf, 0x9a, 0x13, - 0x17, 0xb0, 0xbd, 0x89, 0x17, 0xe3, 0x72, 0xcb, 0x0e, 0x76, 0x7e, 0x41, 0x63, 0x04, 0x88, 0x71]) , - fe([0x75, 0x78, 0x38, 0x86, 0x57, 0xdd, 0x9f, 0xee, 0x54, 0x70, 0x65, 0xbf, 0xf1, 0x2c, 0xe0, 0x39, - 0x0d, 0xe3, 0x89, 0xfd, 0x8e, 0x93, 0x4f, 0x43, 0xdc, 0xd5, 0x5b, 0xde, 0xf9, 0x98, 0xe5, 0x7b])) + aff( + fe([ + 0x2c, 0xb9, 0x42, 0xa4, 0xaf, 0x3b, 0x42, 0x0e, 0xc2, 0x0f, 0xf2, 0xea, 0x83, 0xaf, 0x9a, 0x13, + 0x17, 0xb0, 0xbd, 0x89, 0x17, 0xe3, 0x72, 0xcb, 0x0e, 0x76, 0x7e, 0x41, 0x63, 0x04, 0x88, 0x71 + ]), + fe([ + 0x75, 0x78, 0x38, 0x86, 0x57, 0xdd, 0x9f, 0xee, 0x54, 0x70, 0x65, 0xbf, 0xf1, 0x2c, 0xe0, 0x39, + 0x0d, 0xe3, 0x89, 0xfd, 0x8e, 0x93, 0x4f, 0x43, 0xdc, 0xd5, 0x5b, 0xde, 0xf9, 0x98, 0xe5, 0x7b + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xe7, 0x3b, 0x65, 0x11, 0xdf, 0xb2, 0xf2, 0x63, 0x94, 0x12, 0x6f, 0x5c, 0x9e, 0x77, 0xc1, 0xb6, - 0xd8, 0xab, 0x58, 0x7a, 0x1d, 0x95, 0x73, 0xdd, 0xe7, 0xe3, 0x6f, 0xf2, 0x03, 0x1d, 0xdb, 0x76]) , - fe([0xae, 0x06, 0x4e, 0x2c, 0x52, 0x1b, 0xbc, 0x5a, 0x5a, 0xa5, 0xbe, 0x27, 0xbd, 0xeb, 0xe1, 0x14, - 0x17, 0x68, 0x26, 0x07, 0x03, 0xd1, 0x18, 0x0b, 0xdf, 0xf1, 0x06, 0x5c, 0xa6, 0x1b, 0xb9, 0x24])) + aff( + fe([ + 0xe7, 0x3b, 0x65, 0x11, 0xdf, 0xb2, 0xf2, 0x63, 0x94, 0x12, 0x6f, 0x5c, 0x9e, 0x77, 0xc1, 0xb6, + 0xd8, 0xab, 0x58, 0x7a, 0x1d, 0x95, 0x73, 0xdd, 0xe7, 0xe3, 0x6f, 0xf2, 0x03, 0x1d, 0xdb, 0x76 + ]), + fe([ + 0xae, 0x06, 0x4e, 0x2c, 0x52, 0x1b, 0xbc, 0x5a, 0x5a, 0xa5, 0xbe, 0x27, 0xbd, 0xeb, 0xe1, 0x14, + 0x17, 0x68, 0x26, 0x07, 0x03, 0xd1, 0x18, 0x0b, 0xdf, 0xf1, 0x06, 0x5c, 0xa6, 0x1b, 0xb9, 0x24 + ]) + ) ) v.append( - aff(fe([0xc5, 0x66, 0x80, 0x13, 0x0e, 0x48, 0x8c, 0x87, 0x31, 0x84, 0xb4, 0x60, 0xed, 0xc5, 0xec, 0xb6, - 0xc5, 0x05, 0x33, 0x5f, 0x2f, 0x7d, 0x40, 0xb6, 0x32, 0x1d, 0x38, 0x74, 0x1b, 0xf1, 0x09, 0x3d]) , - fe([0xd4, 0x69, 0x82, 0xbc, 0x8d, 0xf8, 0x34, 0x36, 0x75, 0x55, 0x18, 0x55, 0x58, 0x3c, 0x79, 0xaf, - 0x26, 0x80, 0xab, 0x9b, 0x95, 0x00, 0xf1, 0xcb, 0xda, 0xc1, 0x9f, 0xf6, 0x2f, 0xa2, 0xf4, 0x45])) + aff( + fe([ + 0xc5, 0x66, 0x80, 0x13, 0x0e, 0x48, 0x8c, 0x87, 0x31, 0x84, 0xb4, 0x60, 0xed, 0xc5, 0xec, 0xb6, + 0xc5, 0x05, 0x33, 0x5f, 0x2f, 0x7d, 0x40, 0xb6, 0x32, 0x1d, 0x38, 0x74, 0x1b, 0xf1, 0x09, 0x3d + ]), + fe([ + 0xd4, 0x69, 0x82, 0xbc, 0x8d, 0xf8, 0x34, 0x36, 0x75, 0x55, 0x18, 0x55, 0x58, 0x3c, 0x79, 0xaf, + 0x26, 0x80, 0xab, 0x9b, 0x95, 0x00, 0xf1, 0xcb, 0xda, 0xc1, 0x9f, 0xf6, 0x2f, 0xa2, 0xf4, 0x45 + ]) + ) ) v.append( - aff(fe([0x17, 0xbe, 0xeb, 0x85, 0xed, 0x9e, 0xcd, 0x56, 0xf5, 0x17, 0x45, 0x42, 0xb4, 0x1f, 0x44, 0x4c, - 0x05, 0x74, 0x15, 0x47, 0x00, 0xc6, 0x6a, 0x3d, 0x24, 0x09, 0x0d, 0x58, 0xb1, 0x42, 0xd7, 0x04]) , - fe([0x8d, 0xbd, 0xa3, 0xc4, 0x06, 0x9b, 0x1f, 0x90, 0x58, 0x60, 0x74, 0xb2, 0x00, 0x3b, 0x3c, 0xd2, - 0xda, 0x82, 0xbb, 0x10, 0x90, 0x69, 0x92, 0xa9, 0xb4, 0x30, 0x81, 0xe3, 0x7c, 0xa8, 0x89, 0x45])) + aff( + fe([ + 0x17, 0xbe, 0xeb, 0x85, 0xed, 0x9e, 0xcd, 0x56, 0xf5, 0x17, 0x45, 0x42, 0xb4, 0x1f, 0x44, 0x4c, + 0x05, 0x74, 0x15, 0x47, 0x00, 0xc6, 0x6a, 0x3d, 0x24, 0x09, 0x0d, 0x58, 0xb1, 0x42, 0xd7, 0x04 + ]), + fe([ + 0x8d, 0xbd, 0xa3, 0xc4, 0x06, 0x9b, 0x1f, 0x90, 0x58, 0x60, 0x74, 0xb2, 0x00, 0x3b, 0x3c, 0xd2, + 0xda, 0x82, 0xbb, 0x10, 0x90, 0x69, 0x92, 0xa9, 0xb4, 0x30, 0x81, 0xe3, 0x7c, 0xa8, 0x89, 0x45 + ]) + ) ) v.append( - aff(fe([0x3f, 0xdc, 0x05, 0xcb, 0x41, 0x3c, 0xc8, 0x23, 0x04, 0x2c, 0x38, 0x99, 0xe3, 0x68, 0x55, 0xf9, - 0xd3, 0x32, 0xc7, 0xbf, 0xfa, 0xd4, 0x1b, 0x5d, 0xde, 0xdc, 0x10, 0x42, 0xc0, 0x42, 0xd9, 0x75]) , - fe([0x2d, 0xab, 0x35, 0x4e, 0x87, 0xc4, 0x65, 0x97, 0x67, 0x24, 0xa4, 0x47, 0xad, 0x3f, 0x8e, 0xf3, - 0xcb, 0x31, 0x17, 0x77, 0xc5, 0xe2, 0xd7, 0x8f, 0x3c, 0xc1, 0xcd, 0x56, 0x48, 0xc1, 0x6c, 0x69])) + aff( + fe([ + 0x3f, 0xdc, 0x05, 0xcb, 0x41, 0x3c, 0xc8, 0x23, 0x04, 0x2c, 0x38, 0x99, 0xe3, 0x68, 0x55, 0xf9, + 0xd3, 0x32, 0xc7, 0xbf, 0xfa, 0xd4, 0x1b, 0x5d, 0xde, 0xdc, 0x10, 0x42, 0xc0, 0x42, 0xd9, 0x75 + ]), + fe([ + 0x2d, 0xab, 0x35, 0x4e, 0x87, 0xc4, 0x65, 0x97, 0x67, 0x24, 0xa4, 0x47, 0xad, 0x3f, 0x8e, 0xf3, + 0xcb, 0x31, 0x17, 0x77, 0xc5, 0xe2, 0xd7, 0x8f, 0x3c, 0xc1, 0xcd, 0x56, 0x48, 0xc1, 0x6c, 0x69 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x14, 0xae, 0x5f, 0x88, 0x7b, 0xa5, 0x90, 0xdf, 0x10, 0xb2, 0x8b, 0x5e, 0x24, 0x17, 0xc3, 0xa3, - 0xd4, 0x0f, 0x92, 0x61, 0x1a, 0x19, 0x5a, 0xad, 0x76, 0xbd, 0xd8, 0x1c, 0xdd, 0xe0, 0x12, 0x6d]) , - fe([0x8e, 0xbd, 0x70, 0x8f, 0x02, 0xa3, 0x24, 0x4d, 0x5a, 0x67, 0xc4, 0xda, 0xf7, 0x20, 0x0f, 0x81, - 0x5b, 0x7a, 0x05, 0x24, 0x67, 0x83, 0x0b, 0x2a, 0x80, 0xe7, 0xfd, 0x74, 0x4b, 0x9e, 0x5c, 0x0d])) + aff( + fe([ + 0x14, 0xae, 0x5f, 0x88, 0x7b, 0xa5, 0x90, 0xdf, 0x10, 0xb2, 0x8b, 0x5e, 0x24, 0x17, 0xc3, 0xa3, + 0xd4, 0x0f, 0x92, 0x61, 0x1a, 0x19, 0x5a, 0xad, 0x76, 0xbd, 0xd8, 0x1c, 0xdd, 0xe0, 0x12, 0x6d + ]), + fe([ + 0x8e, 0xbd, 0x70, 0x8f, 0x02, 0xa3, 0x24, 0x4d, 0x5a, 0x67, 0xc4, 0xda, 0xf7, 0x20, 0x0f, 0x81, + 0x5b, 0x7a, 0x05, 0x24, 0x67, 0x83, 0x0b, 0x2a, 0x80, 0xe7, 0xfd, 0x74, 0x4b, 0x9e, 0x5c, 0x0d + ]) + ) ) v.append( - aff(fe([0x94, 0xd5, 0x5f, 0x1f, 0xa2, 0xfb, 0xeb, 0xe1, 0x07, 0x34, 0xf8, 0x20, 0xad, 0x81, 0x30, 0x06, - 0x2d, 0xa1, 0x81, 0x95, 0x36, 0xcf, 0x11, 0x0b, 0xaf, 0xc1, 0x2b, 0x9a, 0x6c, 0x55, 0xc1, 0x16]) , - fe([0x36, 0x4f, 0xf1, 0x5e, 0x74, 0x35, 0x13, 0x28, 0xd7, 0x11, 0xcf, 0xb8, 0xde, 0x93, 0xb3, 0x05, - 0xb8, 0xb5, 0x73, 0xe9, 0xeb, 0xad, 0x19, 0x1e, 0x89, 0x0f, 0x8b, 0x15, 0xd5, 0x8c, 0xe3, 0x23])) + aff( + fe([ + 0x94, 0xd5, 0x5f, 0x1f, 0xa2, 0xfb, 0xeb, 0xe1, 0x07, 0x34, 0xf8, 0x20, 0xad, 0x81, 0x30, 0x06, + 0x2d, 0xa1, 0x81, 0x95, 0x36, 0xcf, 0x11, 0x0b, 0xaf, 0xc1, 0x2b, 0x9a, 0x6c, 0x55, 0xc1, 0x16 + ]), + fe([ + 0x36, 0x4f, 0xf1, 0x5e, 0x74, 0x35, 0x13, 0x28, 0xd7, 0x11, 0xcf, 0xb8, 0xde, 0x93, 0xb3, 0x05, + 0xb8, 0xb5, 0x73, 0xe9, 0xeb, 0xad, 0x19, 0x1e, 0x89, 0x0f, 0x8b, 0x15, 0xd5, 0x8c, 0xe3, 0x23 + ]) + ) ) v.append( - aff(fe([0x33, 0x79, 0xe7, 0x18, 0xe6, 0x0f, 0x57, 0x93, 0x15, 0xa0, 0xa7, 0xaa, 0xc4, 0xbf, 0x4f, 0x30, - 0x74, 0x95, 0x5e, 0x69, 0x4a, 0x5b, 0x45, 0xe4, 0x00, 0xeb, 0x23, 0x74, 0x4c, 0xdf, 0x6b, 0x45]) , - fe([0x97, 0x29, 0x6c, 0xc4, 0x42, 0x0b, 0xdd, 0xc0, 0x29, 0x5c, 0x9b, 0x34, 0x97, 0xd0, 0xc7, 0x79, - 0x80, 0x63, 0x74, 0xe4, 0x8e, 0x37, 0xb0, 0x2b, 0x7c, 0xe8, 0x68, 0x6c, 0xc3, 0x82, 0x97, 0x57])) + aff( + fe([ + 0x33, 0x79, 0xe7, 0x18, 0xe6, 0x0f, 0x57, 0x93, 0x15, 0xa0, 0xa7, 0xaa, 0xc4, 0xbf, 0x4f, 0x30, + 0x74, 0x95, 0x5e, 0x69, 0x4a, 0x5b, 0x45, 0xe4, 0x00, 0xeb, 0x23, 0x74, 0x4c, 0xdf, 0x6b, 0x45 + ]), + fe([ + 0x97, 0x29, 0x6c, 0xc4, 0x42, 0x0b, 0xdd, 0xc0, 0x29, 0x5c, 0x9b, 0x34, 0x97, 0xd0, 0xc7, 0x79, + 0x80, 0x63, 0x74, 0xe4, 0x8e, 0x37, 0xb0, 0x2b, 0x7c, 0xe8, 0x68, 0x6c, 0xc3, 0x82, 0x97, 0x57 + ]) + ) ) v.append( - aff(fe([0x22, 0xbe, 0x83, 0xb6, 0x4b, 0x80, 0x6b, 0x43, 0x24, 0x5e, 0xef, 0x99, 0x9b, 0xa8, 0xfc, 0x25, - 0x8d, 0x3b, 0x03, 0x94, 0x2b, 0x3e, 0xe7, 0x95, 0x76, 0x9b, 0xcc, 0x15, 0xdb, 0x32, 0xe6, 0x66]) , - fe([0x84, 0xf0, 0x4a, 0x13, 0xa6, 0xd6, 0xfa, 0x93, 0x46, 0x07, 0xf6, 0x7e, 0x5c, 0x6d, 0x5e, 0xf6, - 0xa6, 0xe7, 0x48, 0xf0, 0x06, 0xea, 0xff, 0x90, 0xc1, 0xcc, 0x4c, 0x19, 0x9c, 0x3c, 0x4e, 0x53])) + aff( + fe([ + 0x22, 0xbe, 0x83, 0xb6, 0x4b, 0x80, 0x6b, 0x43, 0x24, 0x5e, 0xef, 0x99, 0x9b, 0xa8, 0xfc, 0x25, + 0x8d, 0x3b, 0x03, 0x94, 0x2b, 0x3e, 0xe7, 0x95, 0x76, 0x9b, 0xcc, 0x15, 0xdb, 0x32, 0xe6, 0x66 + ]), + fe([ + 0x84, 0xf0, 0x4a, 0x13, 0xa6, 0xd6, 0xfa, 0x93, 0x46, 0x07, 0xf6, 0x7e, 0x5c, 0x6d, 0x5e, 0xf6, + 0xa6, 0xe7, 0x48, 0xf0, 0x06, 0xea, 0xff, 0x90, 0xc1, 0xcc, 0x4c, 0x19, 0x9c, 0x3c, 0x4e, 0x53 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x2a, 0x50, 0xe3, 0x07, 0x15, 0x59, 0xf2, 0x8b, 0x81, 0xf2, 0xf3, 0xd3, 0x6c, 0x99, 0x8c, 0x70, - 0x67, 0xec, 0xcc, 0xee, 0x9e, 0x59, 0x45, 0x59, 0x7d, 0x47, 0x75, 0x69, 0xf5, 0x24, 0x93, 0x5d]) , - fe([0x6a, 0x4f, 0x1b, 0xbe, 0x6b, 0x30, 0xcf, 0x75, 0x46, 0xe3, 0x7b, 0x9d, 0xfc, 0xcd, 0xd8, 0x5c, - 0x1f, 0xb4, 0xc8, 0xe2, 0x24, 0xec, 0x1a, 0x28, 0x05, 0x32, 0x57, 0xfd, 0x3c, 0x5a, 0x98, 0x10])) + aff( + fe([ + 0x2a, 0x50, 0xe3, 0x07, 0x15, 0x59, 0xf2, 0x8b, 0x81, 0xf2, 0xf3, 0xd3, 0x6c, 0x99, 0x8c, 0x70, + 0x67, 0xec, 0xcc, 0xee, 0x9e, 0x59, 0x45, 0x59, 0x7d, 0x47, 0x75, 0x69, 0xf5, 0x24, 0x93, 0x5d + ]), + fe([ + 0x6a, 0x4f, 0x1b, 0xbe, 0x6b, 0x30, 0xcf, 0x75, 0x46, 0xe3, 0x7b, 0x9d, 0xfc, 0xcd, 0xd8, 0x5c, + 0x1f, 0xb4, 0xc8, 0xe2, 0x24, 0xec, 0x1a, 0x28, 0x05, 0x32, 0x57, 0xfd, 0x3c, 0x5a, 0x98, 0x10 + ]) + ) ) v.append( - aff(fe([0xa3, 0xdb, 0xf7, 0x30, 0xd8, 0xc2, 0x9a, 0xe1, 0xd3, 0xce, 0x22, 0xe5, 0x80, 0x1e, 0xd9, 0xe4, - 0x1f, 0xab, 0xc0, 0x71, 0x1a, 0x86, 0x0e, 0x27, 0x99, 0x5b, 0xfa, 0x76, 0x99, 0xb0, 0x08, 0x3c]) , - fe([0x2a, 0x93, 0xd2, 0x85, 0x1b, 0x6a, 0x5d, 0xa6, 0xee, 0xd1, 0xd1, 0x33, 0xbd, 0x6a, 0x36, 0x73, - 0x37, 0x3a, 0x44, 0xb4, 0xec, 0xa9, 0x7a, 0xde, 0x83, 0x40, 0xd7, 0xdf, 0x28, 0xba, 0xa2, 0x30])) + aff( + fe([ + 0xa3, 0xdb, 0xf7, 0x30, 0xd8, 0xc2, 0x9a, 0xe1, 0xd3, 0xce, 0x22, 0xe5, 0x80, 0x1e, 0xd9, 0xe4, + 0x1f, 0xab, 0xc0, 0x71, 0x1a, 0x86, 0x0e, 0x27, 0x99, 0x5b, 0xfa, 0x76, 0x99, 0xb0, 0x08, 0x3c + ]), + fe([ + 0x2a, 0x93, 0xd2, 0x85, 0x1b, 0x6a, 0x5d, 0xa6, 0xee, 0xd1, 0xd1, 0x33, 0xbd, 0x6a, 0x36, 0x73, + 0x37, 0x3a, 0x44, 0xb4, 0xec, 0xa9, 0x7a, 0xde, 0x83, 0x40, 0xd7, 0xdf, 0x28, 0xba, 0xa2, 0x30 + ]) + ) ) v.append( - aff(fe([0xd3, 0xb5, 0x6d, 0x05, 0x3f, 0x9f, 0xf3, 0x15, 0x8d, 0x7c, 0xca, 0xc9, 0xfc, 0x8a, 0x7c, 0x94, - 0xb0, 0x63, 0x36, 0x9b, 0x78, 0xd1, 0x91, 0x1f, 0x93, 0xd8, 0x57, 0x43, 0xde, 0x76, 0xa3, 0x43]) , - fe([0x9b, 0x35, 0xe2, 0xa9, 0x3d, 0x32, 0x1e, 0xbb, 0x16, 0x28, 0x70, 0xe9, 0x45, 0x2f, 0x8f, 0x70, - 0x7f, 0x08, 0x7e, 0x53, 0xc4, 0x7a, 0xbf, 0xf7, 0xe1, 0xa4, 0x6a, 0xd8, 0xac, 0x64, 0x1b, 0x11])) + aff( + fe([ + 0xd3, 0xb5, 0x6d, 0x05, 0x3f, 0x9f, 0xf3, 0x15, 0x8d, 0x7c, 0xca, 0xc9, 0xfc, 0x8a, 0x7c, 0x94, + 0xb0, 0x63, 0x36, 0x9b, 0x78, 0xd1, 0x91, 0x1f, 0x93, 0xd8, 0x57, 0x43, 0xde, 0x76, 0xa3, 0x43 + ]), + fe([ + 0x9b, 0x35, 0xe2, 0xa9, 0x3d, 0x32, 0x1e, 0xbb, 0x16, 0x28, 0x70, 0xe9, 0x45, 0x2f, 0x8f, 0x70, + 0x7f, 0x08, 0x7e, 0x53, 0xc4, 0x7a, 0xbf, 0xf7, 0xe1, 0xa4, 0x6a, 0xd8, 0xac, 0x64, 0x1b, 0x11 + ]) + ) ) v.append( - aff(fe([0xb2, 0xeb, 0x47, 0x46, 0x18, 0x3e, 0x1f, 0x99, 0x0c, 0xcc, 0xf1, 0x2c, 0xe0, 0xe7, 0x8f, 0xe0, - 0x01, 0x7e, 0x65, 0xb8, 0x0c, 0xd0, 0xfb, 0xc8, 0xb9, 0x90, 0x98, 0x33, 0x61, 0x3b, 0xd8, 0x27]) , - fe([0xa0, 0xbe, 0x72, 0x3a, 0x50, 0x4b, 0x74, 0xab, 0x01, 0xc8, 0x93, 0xc5, 0xe4, 0xc7, 0x08, 0x6c, - 0xb4, 0xca, 0xee, 0xeb, 0x8e, 0xd7, 0x4e, 0x26, 0xc6, 0x1d, 0xe2, 0x71, 0xaf, 0x89, 0xa0, 0x2a])) + aff( + fe([ + 0xb2, 0xeb, 0x47, 0x46, 0x18, 0x3e, 0x1f, 0x99, 0x0c, 0xcc, 0xf1, 0x2c, 0xe0, 0xe7, 0x8f, 0xe0, + 0x01, 0x7e, 0x65, 0xb8, 0x0c, 0xd0, 0xfb, 0xc8, 0xb9, 0x90, 0x98, 0x33, 0x61, 0x3b, 0xd8, 0x27 + ]), + fe([ + 0xa0, 0xbe, 0x72, 0x3a, 0x50, 0x4b, 0x74, 0xab, 0x01, 0xc8, 0x93, 0xc5, 0xe4, 0xc7, 0x08, 0x6c, + 0xb4, 0xca, 0xee, 0xeb, 0x8e, 0xd7, 0x4e, 0x26, 0xc6, 0x1d, 0xe2, 0x71, 0xaf, 0x89, 0xa0, 0x2a + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x98, 0x0b, 0xe4, 0xde, 0xdb, 0xa8, 0xfa, 0x82, 0x74, 0x06, 0x52, 0x6d, 0x08, 0x52, 0x8a, 0xff, - 0x62, 0xc5, 0x6a, 0x44, 0x0f, 0x51, 0x8c, 0x1f, 0x6e, 0xb6, 0xc6, 0x2c, 0x81, 0xd3, 0x76, 0x46]) , - fe([0xf4, 0x29, 0x74, 0x2e, 0x80, 0xa7, 0x1a, 0x8f, 0xf6, 0xbd, 0xd6, 0x8e, 0xbf, 0xc1, 0x95, 0x2a, - 0xeb, 0xa0, 0x7f, 0x45, 0xa0, 0x50, 0x14, 0x05, 0xb1, 0x57, 0x4c, 0x74, 0xb7, 0xe2, 0x89, 0x7d])) + aff( + fe([ + 0x98, 0x0b, 0xe4, 0xde, 0xdb, 0xa8, 0xfa, 0x82, 0x74, 0x06, 0x52, 0x6d, 0x08, 0x52, 0x8a, 0xff, + 0x62, 0xc5, 0x6a, 0x44, 0x0f, 0x51, 0x8c, 0x1f, 0x6e, 0xb6, 0xc6, 0x2c, 0x81, 0xd3, 0x76, 0x46 + ]), + fe([ + 0xf4, 0x29, 0x74, 0x2e, 0x80, 0xa7, 0x1a, 0x8f, 0xf6, 0xbd, 0xd6, 0x8e, 0xbf, 0xc1, 0x95, 0x2a, + 0xeb, 0xa0, 0x7f, 0x45, 0xa0, 0x50, 0x14, 0x05, 0xb1, 0x57, 0x4c, 0x74, 0xb7, 0xe2, 0x89, 0x7d + ]) + ) ) v.append( - aff(fe([0x07, 0xee, 0xa7, 0xad, 0xb7, 0x09, 0x0b, 0x49, 0x4e, 0xbf, 0xca, 0xe5, 0x21, 0xe6, 0xe6, 0xaf, - 0xd5, 0x67, 0xf3, 0xce, 0x7e, 0x7c, 0x93, 0x7b, 0x5a, 0x10, 0x12, 0x0e, 0x6c, 0x06, 0x11, 0x75]) , - fe([0xd5, 0xfc, 0x86, 0xa3, 0x3b, 0xa3, 0x3e, 0x0a, 0xfb, 0x0b, 0xf7, 0x36, 0xb1, 0x5b, 0xda, 0x70, - 0xb7, 0x00, 0xa7, 0xda, 0x88, 0x8f, 0x84, 0xa8, 0xbc, 0x1c, 0x39, 0xb8, 0x65, 0xf3, 0x4d, 0x60])) + aff( + fe([ + 0x07, 0xee, 0xa7, 0xad, 0xb7, 0x09, 0x0b, 0x49, 0x4e, 0xbf, 0xca, 0xe5, 0x21, 0xe6, 0xe6, 0xaf, + 0xd5, 0x67, 0xf3, 0xce, 0x7e, 0x7c, 0x93, 0x7b, 0x5a, 0x10, 0x12, 0x0e, 0x6c, 0x06, 0x11, 0x75 + ]), + fe([ + 0xd5, 0xfc, 0x86, 0xa3, 0x3b, 0xa3, 0x3e, 0x0a, 0xfb, 0x0b, 0xf7, 0x36, 0xb1, 0x5b, 0xda, 0x70, + 0xb7, 0x00, 0xa7, 0xda, 0x88, 0x8f, 0x84, 0xa8, 0xbc, 0x1c, 0x39, 0xb8, 0x65, 0xf3, 0x4d, 0x60 + ]) + ) ) v.append( - aff(fe([0x96, 0x9d, 0x31, 0xf4, 0xa2, 0xbe, 0x81, 0xb9, 0xa5, 0x59, 0x9e, 0xba, 0x07, 0xbe, 0x74, 0x58, - 0xd8, 0xeb, 0xc5, 0x9f, 0x3d, 0xd1, 0xf4, 0xae, 0xce, 0x53, 0xdf, 0x4f, 0xc7, 0x2a, 0x89, 0x4d]) , - fe([0x29, 0xd8, 0xf2, 0xaa, 0xe9, 0x0e, 0xf7, 0x2e, 0x5f, 0x9d, 0x8a, 0x5b, 0x09, 0xed, 0xc9, 0x24, - 0x22, 0xf4, 0x0f, 0x25, 0x8f, 0x1c, 0x84, 0x6e, 0x34, 0x14, 0x6c, 0xea, 0xb3, 0x86, 0x5d, 0x04])) + aff( + fe([ + 0x96, 0x9d, 0x31, 0xf4, 0xa2, 0xbe, 0x81, 0xb9, 0xa5, 0x59, 0x9e, 0xba, 0x07, 0xbe, 0x74, 0x58, + 0xd8, 0xeb, 0xc5, 0x9f, 0x3d, 0xd1, 0xf4, 0xae, 0xce, 0x53, 0xdf, 0x4f, 0xc7, 0x2a, 0x89, 0x4d + ]), + fe([ + 0x29, 0xd8, 0xf2, 0xaa, 0xe9, 0x0e, 0xf7, 0x2e, 0x5f, 0x9d, 0x8a, 0x5b, 0x09, 0xed, 0xc9, 0x24, + 0x22, 0xf4, 0x0f, 0x25, 0x8f, 0x1c, 0x84, 0x6e, 0x34, 0x14, 0x6c, 0xea, 0xb3, 0x86, 0x5d, 0x04 + ]) + ) ) v.append( - aff(fe([0x07, 0x98, 0x61, 0xe8, 0x6a, 0xd2, 0x81, 0x49, 0x25, 0xd5, 0x5b, 0x18, 0xc7, 0x35, 0x52, 0x51, - 0xa4, 0x46, 0xad, 0x18, 0x0d, 0xc9, 0x5f, 0x18, 0x91, 0x3b, 0xb4, 0xc0, 0x60, 0x59, 0x8d, 0x66]) , - fe([0x03, 0x1b, 0x79, 0x53, 0x6e, 0x24, 0xae, 0x57, 0xd9, 0x58, 0x09, 0x85, 0x48, 0xa2, 0xd3, 0xb5, - 0xe2, 0x4d, 0x11, 0x82, 0xe6, 0x86, 0x3c, 0xe9, 0xb1, 0x00, 0x19, 0xc2, 0x57, 0xf7, 0x66, 0x7a])) + aff( + fe([ + 0x07, 0x98, 0x61, 0xe8, 0x6a, 0xd2, 0x81, 0x49, 0x25, 0xd5, 0x5b, 0x18, 0xc7, 0x35, 0x52, 0x51, + 0xa4, 0x46, 0xad, 0x18, 0x0d, 0xc9, 0x5f, 0x18, 0x91, 0x3b, 0xb4, 0xc0, 0x60, 0x59, 0x8d, 0x66 + ]), + fe([ + 0x03, 0x1b, 0x79, 0x53, 0x6e, 0x24, 0xae, 0x57, 0xd9, 0x58, 0x09, 0x85, 0x48, 0xa2, 0xd3, 0xb5, + 0xe2, 0x4d, 0x11, 0x82, 0xe6, 0x86, 0x3c, 0xe9, 0xb1, 0x00, 0x19, 0xc2, 0x57, 0xf7, 0x66, 0x7a + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x0f, 0xe3, 0x89, 0x03, 0xd7, 0x22, 0x95, 0x9f, 0xca, 0xb4, 0x8d, 0x9e, 0x6d, 0x97, 0xff, 0x8d, - 0x21, 0x59, 0x07, 0xef, 0x03, 0x2d, 0x5e, 0xf8, 0x44, 0x46, 0xe7, 0x85, 0x80, 0xc5, 0x89, 0x50]) , - fe([0x8b, 0xd8, 0x53, 0x86, 0x24, 0x86, 0x29, 0x52, 0x01, 0xfa, 0x20, 0xc3, 0x4e, 0x95, 0xcb, 0xad, - 0x7b, 0x34, 0x94, 0x30, 0xb7, 0x7a, 0xfa, 0x96, 0x41, 0x60, 0x2b, 0xcb, 0x59, 0xb9, 0xca, 0x50])) + aff( + fe([ + 0x0f, 0xe3, 0x89, 0x03, 0xd7, 0x22, 0x95, 0x9f, 0xca, 0xb4, 0x8d, 0x9e, 0x6d, 0x97, 0xff, 0x8d, + 0x21, 0x59, 0x07, 0xef, 0x03, 0x2d, 0x5e, 0xf8, 0x44, 0x46, 0xe7, 0x85, 0x80, 0xc5, 0x89, 0x50 + ]), + fe([ + 0x8b, 0xd8, 0x53, 0x86, 0x24, 0x86, 0x29, 0x52, 0x01, 0xfa, 0x20, 0xc3, 0x4e, 0x95, 0xcb, 0xad, + 0x7b, 0x34, 0x94, 0x30, 0xb7, 0x7a, 0xfa, 0x96, 0x41, 0x60, 0x2b, 0xcb, 0x59, 0xb9, 0xca, 0x50 + ]) + ) ) v.append( - aff(fe([0xc2, 0x5b, 0x9b, 0x78, 0x23, 0x1b, 0x3a, 0x88, 0x94, 0x5f, 0x0a, 0x9b, 0x98, 0x2b, 0x6e, 0x53, - 0x11, 0xf6, 0xff, 0xc6, 0x7d, 0x42, 0xcc, 0x02, 0x80, 0x40, 0x0d, 0x1e, 0xfb, 0xaf, 0x61, 0x07]) , - fe([0xb0, 0xe6, 0x2f, 0x81, 0x70, 0xa1, 0x2e, 0x39, 0x04, 0x7c, 0xc4, 0x2c, 0x87, 0x45, 0x4a, 0x5b, - 0x69, 0x97, 0xac, 0x6d, 0x2c, 0x10, 0x42, 0x7c, 0x3b, 0x15, 0x70, 0x60, 0x0e, 0x11, 0x6d, 0x3a])) + aff( + fe([ + 0xc2, 0x5b, 0x9b, 0x78, 0x23, 0x1b, 0x3a, 0x88, 0x94, 0x5f, 0x0a, 0x9b, 0x98, 0x2b, 0x6e, 0x53, + 0x11, 0xf6, 0xff, 0xc6, 0x7d, 0x42, 0xcc, 0x02, 0x80, 0x40, 0x0d, 0x1e, 0xfb, 0xaf, 0x61, 0x07 + ]), + fe([ + 0xb0, 0xe6, 0x2f, 0x81, 0x70, 0xa1, 0x2e, 0x39, 0x04, 0x7c, 0xc4, 0x2c, 0x87, 0x45, 0x4a, 0x5b, + 0x69, 0x97, 0xac, 0x6d, 0x2c, 0x10, 0x42, 0x7c, 0x3b, 0x15, 0x70, 0x60, 0x0e, 0x11, 0x6d, 0x3a + ]) + ) ) v.append( - aff(fe([0x9b, 0x18, 0x80, 0x5e, 0xdb, 0x05, 0xbd, 0xc6, 0xb7, 0x3c, 0xc2, 0x40, 0x4d, 0x5d, 0xce, 0x97, - 0x8a, 0x34, 0x15, 0xab, 0x28, 0x5d, 0x10, 0xf0, 0x37, 0x0c, 0xcc, 0x16, 0xfa, 0x1f, 0x33, 0x0d]) , - fe([0x19, 0xf9, 0x35, 0xaa, 0x59, 0x1a, 0x0c, 0x5c, 0x06, 0xfc, 0x6a, 0x0b, 0x97, 0x53, 0x36, 0xfc, - 0x2a, 0xa5, 0x5a, 0x9b, 0x30, 0xef, 0x23, 0xaf, 0x39, 0x5d, 0x9a, 0x6b, 0x75, 0x57, 0x48, 0x0b])) + aff( + fe([ + 0x9b, 0x18, 0x80, 0x5e, 0xdb, 0x05, 0xbd, 0xc6, 0xb7, 0x3c, 0xc2, 0x40, 0x4d, 0x5d, 0xce, 0x97, + 0x8a, 0x34, 0x15, 0xab, 0x28, 0x5d, 0x10, 0xf0, 0x37, 0x0c, 0xcc, 0x16, 0xfa, 0x1f, 0x33, 0x0d + ]), + fe([ + 0x19, 0xf9, 0x35, 0xaa, 0x59, 0x1a, 0x0c, 0x5c, 0x06, 0xfc, 0x6a, 0x0b, 0x97, 0x53, 0x36, 0xfc, + 0x2a, 0xa5, 0x5a, 0x9b, 0x30, 0xef, 0x23, 0xaf, 0x39, 0x5d, 0x9a, 0x6b, 0x75, 0x57, 0x48, 0x0b + ]) + ) ) v.append( - aff(fe([0x26, 0xdc, 0x76, 0x3b, 0xfc, 0xf9, 0x9c, 0x3f, 0x89, 0x0b, 0x62, 0x53, 0xaf, 0x83, 0x01, 0x2e, - 0xbc, 0x6a, 0xc6, 0x03, 0x0d, 0x75, 0x2a, 0x0d, 0xe6, 0x94, 0x54, 0xcf, 0xb3, 0xe5, 0x96, 0x25]) , - fe([0xfe, 0x82, 0xb1, 0x74, 0x31, 0x8a, 0xa7, 0x6f, 0x56, 0xbd, 0x8d, 0xf4, 0xe0, 0x94, 0x51, 0x59, - 0xde, 0x2c, 0x5a, 0xf4, 0x84, 0x6b, 0x4a, 0x88, 0x93, 0xc0, 0x0c, 0x9a, 0xac, 0xa7, 0xa0, 0x68])) + aff( + fe([ + 0x26, 0xdc, 0x76, 0x3b, 0xfc, 0xf9, 0x9c, 0x3f, 0x89, 0x0b, 0x62, 0x53, 0xaf, 0x83, 0x01, 0x2e, + 0xbc, 0x6a, 0xc6, 0x03, 0x0d, 0x75, 0x2a, 0x0d, 0xe6, 0x94, 0x54, 0xcf, 0xb3, 0xe5, 0x96, 0x25 + ]), + fe([ + 0xfe, 0x82, 0xb1, 0x74, 0x31, 0x8a, 0xa7, 0x6f, 0x56, 0xbd, 0x8d, 0xf4, 0xe0, 0x94, 0x51, 0x59, + 0xde, 0x2c, 0x5a, 0xf4, 0x84, 0x6b, 0x4a, 0x88, 0x93, 0xc0, 0x0c, 0x9a, 0xac, 0xa7, 0xa0, 0x68 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x25, 0x0d, 0xd6, 0xc7, 0x23, 0x47, 0x10, 0xad, 0xc7, 0x08, 0x5c, 0x87, 0x87, 0x93, 0x98, 0x18, - 0xb8, 0xd3, 0x9c, 0xac, 0x5a, 0x3d, 0xc5, 0x75, 0xf8, 0x49, 0x32, 0x14, 0xcc, 0x51, 0x96, 0x24]) , - fe([0x65, 0x9c, 0x5d, 0xf0, 0x37, 0x04, 0xf0, 0x34, 0x69, 0x2a, 0xf0, 0xa5, 0x64, 0xca, 0xde, 0x2b, - 0x5b, 0x15, 0x10, 0xd2, 0xab, 0x06, 0xdd, 0xc4, 0xb0, 0xb6, 0x5b, 0xc1, 0x17, 0xdf, 0x8f, 0x02])) + aff( + fe([ + 0x25, 0x0d, 0xd6, 0xc7, 0x23, 0x47, 0x10, 0xad, 0xc7, 0x08, 0x5c, 0x87, 0x87, 0x93, 0x98, 0x18, + 0xb8, 0xd3, 0x9c, 0xac, 0x5a, 0x3d, 0xc5, 0x75, 0xf8, 0x49, 0x32, 0x14, 0xcc, 0x51, 0x96, 0x24 + ]), + fe([ + 0x65, 0x9c, 0x5d, 0xf0, 0x37, 0x04, 0xf0, 0x34, 0x69, 0x2a, 0xf0, 0xa5, 0x64, 0xca, 0xde, 0x2b, + 0x5b, 0x15, 0x10, 0xd2, 0xab, 0x06, 0xdd, 0xc4, 0xb0, 0xb6, 0x5b, 0xc1, 0x17, 0xdf, 0x8f, 0x02 + ]) + ) ) v.append( - aff(fe([0xbd, 0x59, 0x3d, 0xbf, 0x5c, 0x31, 0x44, 0x2c, 0x32, 0x94, 0x04, 0x60, 0x84, 0x0f, 0xad, 0x00, - 0xb6, 0x8f, 0xc9, 0x1d, 0xcc, 0x5c, 0xa2, 0x49, 0x0e, 0x50, 0x91, 0x08, 0x9a, 0x43, 0x55, 0x05]) , - fe([0x5d, 0x93, 0x55, 0xdf, 0x9b, 0x12, 0x19, 0xec, 0x93, 0x85, 0x42, 0x9e, 0x66, 0x0f, 0x9d, 0xaf, - 0x99, 0xaf, 0x26, 0x89, 0xbc, 0x61, 0xfd, 0xff, 0xce, 0x4b, 0xf4, 0x33, 0x95, 0xc9, 0x35, 0x58])) + aff( + fe([ + 0xbd, 0x59, 0x3d, 0xbf, 0x5c, 0x31, 0x44, 0x2c, 0x32, 0x94, 0x04, 0x60, 0x84, 0x0f, 0xad, 0x00, + 0xb6, 0x8f, 0xc9, 0x1d, 0xcc, 0x5c, 0xa2, 0x49, 0x0e, 0x50, 0x91, 0x08, 0x9a, 0x43, 0x55, 0x05 + ]), + fe([ + 0x5d, 0x93, 0x55, 0xdf, 0x9b, 0x12, 0x19, 0xec, 0x93, 0x85, 0x42, 0x9e, 0x66, 0x0f, 0x9d, 0xaf, + 0x99, 0xaf, 0x26, 0x89, 0xbc, 0x61, 0xfd, 0xff, 0xce, 0x4b, 0xf4, 0x33, 0x95, 0xc9, 0x35, 0x58 + ]) + ) ) v.append( - aff(fe([0x12, 0x55, 0xf9, 0xda, 0xcb, 0x44, 0xa7, 0xdc, 0x57, 0xe2, 0xf9, 0x9a, 0xe6, 0x07, 0x23, 0x60, - 0x54, 0xa7, 0x39, 0xa5, 0x9b, 0x84, 0x56, 0x6e, 0xaa, 0x8b, 0x8f, 0xb0, 0x2c, 0x87, 0xaf, 0x67]) , - fe([0x00, 0xa9, 0x4c, 0xb2, 0x12, 0xf8, 0x32, 0xa8, 0x7a, 0x00, 0x4b, 0x49, 0x32, 0xba, 0x1f, 0x5d, - 0x44, 0x8e, 0x44, 0x7a, 0xdc, 0x11, 0xfb, 0x39, 0x08, 0x57, 0x87, 0xa5, 0x12, 0x42, 0x93, 0x0e])) + aff( + fe([ + 0x12, 0x55, 0xf9, 0xda, 0xcb, 0x44, 0xa7, 0xdc, 0x57, 0xe2, 0xf9, 0x9a, 0xe6, 0x07, 0x23, 0x60, + 0x54, 0xa7, 0x39, 0xa5, 0x9b, 0x84, 0x56, 0x6e, 0xaa, 0x8b, 0x8f, 0xb0, 0x2c, 0x87, 0xaf, 0x67 + ]), + fe([ + 0x00, 0xa9, 0x4c, 0xb2, 0x12, 0xf8, 0x32, 0xa8, 0x7a, 0x00, 0x4b, 0x49, 0x32, 0xba, 0x1f, 0x5d, + 0x44, 0x8e, 0x44, 0x7a, 0xdc, 0x11, 0xfb, 0x39, 0x08, 0x57, 0x87, 0xa5, 0x12, 0x42, 0x93, 0x0e + ]) + ) ) v.append( - aff(fe([0x17, 0xb4, 0xae, 0x72, 0x59, 0xd0, 0xaa, 0xa8, 0x16, 0x8b, 0x63, 0x11, 0xb3, 0x43, 0x04, 0xda, - 0x0c, 0xa8, 0xb7, 0x68, 0xdd, 0x4e, 0x54, 0xe7, 0xaf, 0x5d, 0x5d, 0x05, 0x76, 0x36, 0xec, 0x0d]) , - fe([0x6d, 0x7c, 0x82, 0x32, 0x38, 0x55, 0x57, 0x74, 0x5b, 0x7d, 0xc3, 0xc4, 0xfb, 0x06, 0x29, 0xf0, - 0x13, 0x55, 0x54, 0xc6, 0xa7, 0xdc, 0x4c, 0x9f, 0x98, 0x49, 0x20, 0xa8, 0xc3, 0x8d, 0xfa, 0x48])) + aff( + fe([ + 0x17, 0xb4, 0xae, 0x72, 0x59, 0xd0, 0xaa, 0xa8, 0x16, 0x8b, 0x63, 0x11, 0xb3, 0x43, 0x04, 0xda, + 0x0c, 0xa8, 0xb7, 0x68, 0xdd, 0x4e, 0x54, 0xe7, 0xaf, 0x5d, 0x5d, 0x05, 0x76, 0x36, 0xec, 0x0d + ]), + fe([ + 0x6d, 0x7c, 0x82, 0x32, 0x38, 0x55, 0x57, 0x74, 0x5b, 0x7d, 0xc3, 0xc4, 0xfb, 0x06, 0x29, 0xf0, + 0x13, 0x55, 0x54, 0xc6, 0xa7, 0xdc, 0x4c, 0x9f, 0x98, 0x49, 0x20, 0xa8, 0xc3, 0x8d, 0xfa, 0x48 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x87, 0x47, 0x9d, 0xe9, 0x25, 0xd5, 0xe3, 0x47, 0x78, 0xdf, 0x85, 0xa7, 0x85, 0x5e, 0x7a, 0x4c, - 0x5f, 0x79, 0x1a, 0xf3, 0xa2, 0xb2, 0x28, 0xa0, 0x9c, 0xdd, 0x30, 0x40, 0xd4, 0x38, 0xbd, 0x28]) , - fe([0xfc, 0xbb, 0xd5, 0x78, 0x6d, 0x1d, 0xd4, 0x99, 0xb4, 0xaa, 0x44, 0x44, 0x7a, 0x1b, 0xd8, 0xfe, - 0xb4, 0x99, 0xb9, 0xcc, 0xe7, 0xc4, 0xd3, 0x3a, 0x73, 0x83, 0x41, 0x5c, 0x40, 0xd7, 0x2d, 0x55])) + aff( + fe([ + 0x87, 0x47, 0x9d, 0xe9, 0x25, 0xd5, 0xe3, 0x47, 0x78, 0xdf, 0x85, 0xa7, 0x85, 0x5e, 0x7a, 0x4c, + 0x5f, 0x79, 0x1a, 0xf3, 0xa2, 0xb2, 0x28, 0xa0, 0x9c, 0xdd, 0x30, 0x40, 0xd4, 0x38, 0xbd, 0x28 + ]), + fe([ + 0xfc, 0xbb, 0xd5, 0x78, 0x6d, 0x1d, 0xd4, 0x99, 0xb4, 0xaa, 0x44, 0x44, 0x7a, 0x1b, 0xd8, 0xfe, + 0xb4, 0x99, 0xb9, 0xcc, 0xe7, 0xc4, 0xd3, 0x3a, 0x73, 0x83, 0x41, 0x5c, 0x40, 0xd7, 0x2d, 0x55 + ]) + ) ) v.append( - aff(fe([0x26, 0xe1, 0x7b, 0x5f, 0xe5, 0xdc, 0x3f, 0x7d, 0xa1, 0xa7, 0x26, 0x44, 0x22, 0x23, 0xc0, 0x8f, - 0x7d, 0xf1, 0xb5, 0x11, 0x47, 0x7b, 0x19, 0xd4, 0x75, 0x6f, 0x1e, 0xa5, 0x27, 0xfe, 0xc8, 0x0e]) , - fe([0xd3, 0x11, 0x3d, 0xab, 0xef, 0x2c, 0xed, 0xb1, 0x3d, 0x7c, 0x32, 0x81, 0x6b, 0xfe, 0xf8, 0x1c, - 0x3c, 0x7b, 0xc0, 0x61, 0xdf, 0xb8, 0x75, 0x76, 0x7f, 0xaa, 0xd8, 0x93, 0xaf, 0x3d, 0xe8, 0x3d])) + aff( + fe([ + 0x26, 0xe1, 0x7b, 0x5f, 0xe5, 0xdc, 0x3f, 0x7d, 0xa1, 0xa7, 0x26, 0x44, 0x22, 0x23, 0xc0, 0x8f, + 0x7d, 0xf1, 0xb5, 0x11, 0x47, 0x7b, 0x19, 0xd4, 0x75, 0x6f, 0x1e, 0xa5, 0x27, 0xfe, 0xc8, 0x0e + ]), + fe([ + 0xd3, 0x11, 0x3d, 0xab, 0xef, 0x2c, 0xed, 0xb1, 0x3d, 0x7c, 0x32, 0x81, 0x6b, 0xfe, 0xf8, 0x1c, + 0x3c, 0x7b, 0xc0, 0x61, 0xdf, 0xb8, 0x75, 0x76, 0x7f, 0xaa, 0xd8, 0x93, 0xaf, 0x3d, 0xe8, 0x3d + ]) + ) ) v.append( - aff(fe([0xfd, 0x5b, 0x4e, 0x8d, 0xb6, 0x7e, 0x82, 0x9b, 0xef, 0xce, 0x04, 0x69, 0x51, 0x52, 0xff, 0xef, - 0xa0, 0x52, 0xb5, 0x79, 0x17, 0x5e, 0x2f, 0xde, 0xd6, 0x3c, 0x2d, 0xa0, 0x43, 0xb4, 0x0b, 0x19]) , - fe([0xc0, 0x61, 0x48, 0x48, 0x17, 0xf4, 0x9e, 0x18, 0x51, 0x2d, 0xea, 0x2f, 0xf2, 0xf2, 0xe0, 0xa3, - 0x14, 0xb7, 0x8b, 0x3a, 0x30, 0xf5, 0x81, 0xc1, 0x5d, 0x71, 0x39, 0x62, 0x55, 0x1f, 0x60, 0x5a])) + aff( + fe([ + 0xfd, 0x5b, 0x4e, 0x8d, 0xb6, 0x7e, 0x82, 0x9b, 0xef, 0xce, 0x04, 0x69, 0x51, 0x52, 0xff, 0xef, + 0xa0, 0x52, 0xb5, 0x79, 0x17, 0x5e, 0x2f, 0xde, 0xd6, 0x3c, 0x2d, 0xa0, 0x43, 0xb4, 0x0b, 0x19 + ]), + fe([ + 0xc0, 0x61, 0x48, 0x48, 0x17, 0xf4, 0x9e, 0x18, 0x51, 0x2d, 0xea, 0x2f, 0xf2, 0xf2, 0xe0, 0xa3, + 0x14, 0xb7, 0x8b, 0x3a, 0x30, 0xf5, 0x81, 0xc1, 0x5d, 0x71, 0x39, 0x62, 0x55, 0x1f, 0x60, 0x5a + ]) + ) ) v.append( - aff(fe([0xe5, 0x89, 0x8a, 0x76, 0x6c, 0xdb, 0x4d, 0x0a, 0x5b, 0x72, 0x9d, 0x59, 0x6e, 0x63, 0x63, 0x18, - 0x7c, 0xe3, 0xfa, 0xe2, 0xdb, 0xa1, 0x8d, 0xf4, 0xa5, 0xd7, 0x16, 0xb2, 0xd0, 0xb3, 0x3f, 0x39]) , - fe([0xce, 0x60, 0x09, 0x6c, 0xf5, 0x76, 0x17, 0x24, 0x80, 0x3a, 0x96, 0xc7, 0x94, 0x2e, 0xf7, 0x6b, - 0xef, 0xb5, 0x05, 0x96, 0xef, 0xd3, 0x7b, 0x51, 0xda, 0x05, 0x44, 0x67, 0xbc, 0x07, 0x21, 0x4e])) + aff( + fe([ + 0xe5, 0x89, 0x8a, 0x76, 0x6c, 0xdb, 0x4d, 0x0a, 0x5b, 0x72, 0x9d, 0x59, 0x6e, 0x63, 0x63, 0x18, + 0x7c, 0xe3, 0xfa, 0xe2, 0xdb, 0xa1, 0x8d, 0xf4, 0xa5, 0xd7, 0x16, 0xb2, 0xd0, 0xb3, 0x3f, 0x39 + ]), + fe([ + 0xce, 0x60, 0x09, 0x6c, 0xf5, 0x76, 0x17, 0x24, 0x80, 0x3a, 0x96, 0xc7, 0x94, 0x2e, 0xf7, 0x6b, + 0xef, 0xb5, 0x05, 0x96, 0xef, 0xd3, 0x7b, 0x51, 0xda, 0x05, 0x44, 0x67, 0xbc, 0x07, 0x21, 0x4e + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xe9, 0x73, 0x6f, 0x21, 0xb9, 0xde, 0x22, 0x7d, 0xeb, 0x97, 0x31, 0x10, 0xa3, 0xea, 0xe1, 0xc6, - 0x37, 0xeb, 0x8f, 0x43, 0x58, 0xde, 0x41, 0x64, 0x0e, 0x3e, 0x07, 0x99, 0x3d, 0xf1, 0xdf, 0x1e]) , - fe([0xf8, 0xad, 0x43, 0xc2, 0x17, 0x06, 0xe2, 0xe4, 0xa9, 0x86, 0xcd, 0x18, 0xd7, 0x78, 0xc8, 0x74, - 0x66, 0xd2, 0x09, 0x18, 0xa5, 0xf1, 0xca, 0xa6, 0x62, 0x92, 0xc1, 0xcb, 0x00, 0xeb, 0x42, 0x2e])) + aff( + fe([ + 0xe9, 0x73, 0x6f, 0x21, 0xb9, 0xde, 0x22, 0x7d, 0xeb, 0x97, 0x31, 0x10, 0xa3, 0xea, 0xe1, 0xc6, + 0x37, 0xeb, 0x8f, 0x43, 0x58, 0xde, 0x41, 0x64, 0x0e, 0x3e, 0x07, 0x99, 0x3d, 0xf1, 0xdf, 0x1e + ]), + fe([ + 0xf8, 0xad, 0x43, 0xc2, 0x17, 0x06, 0xe2, 0xe4, 0xa9, 0x86, 0xcd, 0x18, 0xd7, 0x78, 0xc8, 0x74, + 0x66, 0xd2, 0x09, 0x18, 0xa5, 0xf1, 0xca, 0xa6, 0x62, 0x92, 0xc1, 0xcb, 0x00, 0xeb, 0x42, 0x2e + ]) + ) ) v.append( - aff(fe([0x7b, 0x34, 0x24, 0x4c, 0xcf, 0x38, 0xe5, 0x6c, 0x0a, 0x01, 0x2c, 0x22, 0x0b, 0x24, 0x38, 0xad, - 0x24, 0x7e, 0x19, 0xf0, 0x6c, 0xf9, 0x31, 0xf4, 0x35, 0x11, 0xf6, 0x46, 0x33, 0x3a, 0x23, 0x59]) , - fe([0x20, 0x0b, 0xa1, 0x08, 0x19, 0xad, 0x39, 0x54, 0xea, 0x3e, 0x23, 0x09, 0xb6, 0xe2, 0xd2, 0xbc, - 0x4d, 0xfc, 0x9c, 0xf0, 0x13, 0x16, 0x22, 0x3f, 0xb9, 0xd2, 0x11, 0x86, 0x90, 0x55, 0xce, 0x3c])) + aff( + fe([ + 0x7b, 0x34, 0x24, 0x4c, 0xcf, 0x38, 0xe5, 0x6c, 0x0a, 0x01, 0x2c, 0x22, 0x0b, 0x24, 0x38, 0xad, + 0x24, 0x7e, 0x19, 0xf0, 0x6c, 0xf9, 0x31, 0xf4, 0x35, 0x11, 0xf6, 0x46, 0x33, 0x3a, 0x23, 0x59 + ]), + fe([ + 0x20, 0x0b, 0xa1, 0x08, 0x19, 0xad, 0x39, 0x54, 0xea, 0x3e, 0x23, 0x09, 0xb6, 0xe2, 0xd2, 0xbc, + 0x4d, 0xfc, 0x9c, 0xf0, 0x13, 0x16, 0x22, 0x3f, 0xb9, 0xd2, 0x11, 0x86, 0x90, 0x55, 0xce, 0x3c + ]) + ) ) v.append( - aff(fe([0xc4, 0x0b, 0x4b, 0x62, 0x99, 0x37, 0x84, 0x3f, 0x74, 0xa2, 0xf9, 0xce, 0xe2, 0x0b, 0x0f, 0x2a, - 0x3d, 0xa3, 0xe3, 0xdb, 0x5a, 0x9d, 0x93, 0xcc, 0xa5, 0xef, 0x82, 0x91, 0x1d, 0xe6, 0x6c, 0x68]) , - fe([0xa3, 0x64, 0x17, 0x9b, 0x8b, 0xc8, 0x3a, 0x61, 0xe6, 0x9d, 0xc6, 0xed, 0x7b, 0x03, 0x52, 0x26, - 0x9d, 0x3a, 0xb3, 0x13, 0xcc, 0x8a, 0xfd, 0x2c, 0x1a, 0x1d, 0xed, 0x13, 0xd0, 0x55, 0x57, 0x0e])) + aff( + fe([ + 0xc4, 0x0b, 0x4b, 0x62, 0x99, 0x37, 0x84, 0x3f, 0x74, 0xa2, 0xf9, 0xce, 0xe2, 0x0b, 0x0f, 0x2a, + 0x3d, 0xa3, 0xe3, 0xdb, 0x5a, 0x9d, 0x93, 0xcc, 0xa5, 0xef, 0x82, 0x91, 0x1d, 0xe6, 0x6c, 0x68 + ]), + fe([ + 0xa3, 0x64, 0x17, 0x9b, 0x8b, 0xc8, 0x3a, 0x61, 0xe6, 0x9d, 0xc6, 0xed, 0x7b, 0x03, 0x52, 0x26, + 0x9d, 0x3a, 0xb3, 0x13, 0xcc, 0x8a, 0xfd, 0x2c, 0x1a, 0x1d, 0xed, 0x13, 0xd0, 0x55, 0x57, 0x0e + ]) + ) ) v.append( - aff(fe([0x1a, 0xea, 0xbf, 0xfd, 0x4a, 0x3c, 0x8e, 0xec, 0x29, 0x7e, 0x77, 0x77, 0x12, 0x99, 0xd7, 0x84, - 0xf9, 0x55, 0x7f, 0xf1, 0x8b, 0xb4, 0xd2, 0x95, 0xa3, 0x8d, 0xf0, 0x8a, 0xa7, 0xeb, 0x82, 0x4b]) , - fe([0x2c, 0x28, 0xf4, 0x3a, 0xf6, 0xde, 0x0a, 0xe0, 0x41, 0x44, 0x23, 0xf8, 0x3f, 0x03, 0x64, 0x9f, - 0xc3, 0x55, 0x4c, 0xc6, 0xc1, 0x94, 0x1c, 0x24, 0x5d, 0x5f, 0x92, 0x45, 0x96, 0x57, 0x37, 0x14])) + aff( + fe([ + 0x1a, 0xea, 0xbf, 0xfd, 0x4a, 0x3c, 0x8e, 0xec, 0x29, 0x7e, 0x77, 0x77, 0x12, 0x99, 0xd7, 0x84, + 0xf9, 0x55, 0x7f, 0xf1, 0x8b, 0xb4, 0xd2, 0x95, 0xa3, 0x8d, 0xf0, 0x8a, 0xa7, 0xeb, 0x82, 0x4b + ]), + fe([ + 0x2c, 0x28, 0xf4, 0x3a, 0xf6, 0xde, 0x0a, 0xe0, 0x41, 0x44, 0x23, 0xf8, 0x3f, 0x03, 0x64, 0x9f, + 0xc3, 0x55, 0x4c, 0xc6, 0xc1, 0x94, 0x1c, 0x24, 0x5d, 0x5f, 0x92, 0x45, 0x96, 0x57, 0x37, 0x14 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xc1, 0xcd, 0x90, 0x66, 0xb9, 0x76, 0xa0, 0x5b, 0xa5, 0x85, 0x75, 0x23, 0xf9, 0x89, 0xa5, 0x82, - 0xb2, 0x6f, 0xb1, 0xeb, 0xc4, 0x69, 0x6f, 0x18, 0x5a, 0xed, 0x94, 0x3d, 0x9d, 0xd9, 0x2c, 0x1a]) , - fe([0x35, 0xb0, 0xe6, 0x73, 0x06, 0xb7, 0x37, 0xe0, 0xf8, 0xb0, 0x22, 0xe8, 0xd2, 0xed, 0x0b, 0xef, - 0xe6, 0xc6, 0x5a, 0x99, 0x9e, 0x1a, 0x9f, 0x04, 0x97, 0xe4, 0x4d, 0x0b, 0xbe, 0xba, 0x44, 0x40])) + aff( + fe([ + 0xc1, 0xcd, 0x90, 0x66, 0xb9, 0x76, 0xa0, 0x5b, 0xa5, 0x85, 0x75, 0x23, 0xf9, 0x89, 0xa5, 0x82, + 0xb2, 0x6f, 0xb1, 0xeb, 0xc4, 0x69, 0x6f, 0x18, 0x5a, 0xed, 0x94, 0x3d, 0x9d, 0xd9, 0x2c, 0x1a + ]), + fe([ + 0x35, 0xb0, 0xe6, 0x73, 0x06, 0xb7, 0x37, 0xe0, 0xf8, 0xb0, 0x22, 0xe8, 0xd2, 0xed, 0x0b, 0xef, + 0xe6, 0xc6, 0x5a, 0x99, 0x9e, 0x1a, 0x9f, 0x04, 0x97, 0xe4, 0x4d, 0x0b, 0xbe, 0xba, 0x44, 0x40 + ]) + ) ) v.append( - aff(fe([0xc1, 0x56, 0x96, 0x91, 0x5f, 0x1f, 0xbb, 0x54, 0x6f, 0x88, 0x89, 0x0a, 0xb2, 0xd6, 0x41, 0x42, - 0x6a, 0x82, 0xee, 0x14, 0xaa, 0x76, 0x30, 0x65, 0x0f, 0x67, 0x39, 0xa6, 0x51, 0x7c, 0x49, 0x24]) , - fe([0x35, 0xa3, 0x78, 0xd1, 0x11, 0x0f, 0x75, 0xd3, 0x70, 0x46, 0xdb, 0x20, 0x51, 0xcb, 0x92, 0x80, - 0x54, 0x10, 0x74, 0x36, 0x86, 0xa9, 0xd7, 0xa3, 0x08, 0x78, 0xf1, 0x01, 0x29, 0xf8, 0x80, 0x3b])) + aff( + fe([ + 0xc1, 0x56, 0x96, 0x91, 0x5f, 0x1f, 0xbb, 0x54, 0x6f, 0x88, 0x89, 0x0a, 0xb2, 0xd6, 0x41, 0x42, + 0x6a, 0x82, 0xee, 0x14, 0xaa, 0x76, 0x30, 0x65, 0x0f, 0x67, 0x39, 0xa6, 0x51, 0x7c, 0x49, 0x24 + ]), + fe([ + 0x35, 0xa3, 0x78, 0xd1, 0x11, 0x0f, 0x75, 0xd3, 0x70, 0x46, 0xdb, 0x20, 0x51, 0xcb, 0x92, 0x80, + 0x54, 0x10, 0x74, 0x36, 0x86, 0xa9, 0xd7, 0xa3, 0x08, 0x78, 0xf1, 0x01, 0x29, 0xf8, 0x80, 0x3b + ]) + ) ) v.append( - aff(fe([0xdb, 0xa7, 0x9d, 0x9d, 0xbf, 0xa0, 0xcc, 0xed, 0x53, 0xa2, 0xa2, 0x19, 0x39, 0x48, 0x83, 0x19, - 0x37, 0x58, 0xd1, 0x04, 0x28, 0x40, 0xf7, 0x8a, 0xc2, 0x08, 0xb7, 0xa5, 0x42, 0xcf, 0x53, 0x4c]) , - fe([0xa7, 0xbb, 0xf6, 0x8e, 0xad, 0xdd, 0xf7, 0x90, 0xdd, 0x5f, 0x93, 0x89, 0xae, 0x04, 0x37, 0xe6, - 0x9a, 0xb7, 0xe8, 0xc0, 0xdf, 0x16, 0x2a, 0xbf, 0xc4, 0x3a, 0x3c, 0x41, 0xd5, 0x89, 0x72, 0x5a])) + aff( + fe([ + 0xdb, 0xa7, 0x9d, 0x9d, 0xbf, 0xa0, 0xcc, 0xed, 0x53, 0xa2, 0xa2, 0x19, 0x39, 0x48, 0x83, 0x19, + 0x37, 0x58, 0xd1, 0x04, 0x28, 0x40, 0xf7, 0x8a, 0xc2, 0x08, 0xb7, 0xa5, 0x42, 0xcf, 0x53, 0x4c + ]), + fe([ + 0xa7, 0xbb, 0xf6, 0x8e, 0xad, 0xdd, 0xf7, 0x90, 0xdd, 0x5f, 0x93, 0x89, 0xae, 0x04, 0x37, 0xe6, + 0x9a, 0xb7, 0xe8, 0xc0, 0xdf, 0x16, 0x2a, 0xbf, 0xc4, 0x3a, 0x3c, 0x41, 0xd5, 0x89, 0x72, 0x5a + ]) + ) ) v.append( - aff(fe([0x1f, 0x96, 0xff, 0x34, 0x2c, 0x13, 0x21, 0xcb, 0x0a, 0x89, 0x85, 0xbe, 0xb3, 0x70, 0x9e, 0x1e, - 0xde, 0x97, 0xaf, 0x96, 0x30, 0xf7, 0x48, 0x89, 0x40, 0x8d, 0x07, 0xf1, 0x25, 0xf0, 0x30, 0x58]) , - fe([0x1e, 0xd4, 0x93, 0x57, 0xe2, 0x17, 0xe7, 0x9d, 0xab, 0x3c, 0x55, 0x03, 0x82, 0x2f, 0x2b, 0xdb, - 0x56, 0x1e, 0x30, 0x2e, 0x24, 0x47, 0x6e, 0xe6, 0xff, 0x33, 0x24, 0x2c, 0x75, 0x51, 0xd4, 0x67])) + aff( + fe([ + 0x1f, 0x96, 0xff, 0x34, 0x2c, 0x13, 0x21, 0xcb, 0x0a, 0x89, 0x85, 0xbe, 0xb3, 0x70, 0x9e, 0x1e, + 0xde, 0x97, 0xaf, 0x96, 0x30, 0xf7, 0x48, 0x89, 0x40, 0x8d, 0x07, 0xf1, 0x25, 0xf0, 0x30, 0x58 + ]), + fe([ + 0x1e, 0xd4, 0x93, 0x57, 0xe2, 0x17, 0xe7, 0x9d, 0xab, 0x3c, 0x55, 0x03, 0x82, 0x2f, 0x2b, 0xdb, + 0x56, 0x1e, 0x30, 0x2e, 0x24, 0x47, 0x6e, 0xe6, 0xff, 0x33, 0x24, 0x2c, 0x75, 0x51, 0xd4, 0x67 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0x2b, 0x06, 0xd9, 0xa1, 0x5d, 0xe1, 0xf4, 0xd1, 0x1e, 0x3c, 0x9a, 0xc6, 0x29, 0x2b, 0x13, 0x13, - 0x78, 0xc0, 0xd8, 0x16, 0x17, 0x2d, 0x9e, 0xa9, 0xc9, 0x79, 0x57, 0xab, 0x24, 0x91, 0x92, 0x19]) , - fe([0x69, 0xfb, 0xa1, 0x9c, 0xa6, 0x75, 0x49, 0x7d, 0x60, 0x73, 0x40, 0x42, 0xc4, 0x13, 0x0a, 0x95, - 0x79, 0x1e, 0x04, 0x83, 0x94, 0x99, 0x9b, 0x1e, 0x0c, 0xe8, 0x1f, 0x54, 0xef, 0xcb, 0xc0, 0x52])) + aff( + fe([ + 0x2b, 0x06, 0xd9, 0xa1, 0x5d, 0xe1, 0xf4, 0xd1, 0x1e, 0x3c, 0x9a, 0xc6, 0x29, 0x2b, 0x13, 0x13, + 0x78, 0xc0, 0xd8, 0x16, 0x17, 0x2d, 0x9e, 0xa9, 0xc9, 0x79, 0x57, 0xab, 0x24, 0x91, 0x92, 0x19 + ]), + fe([ + 0x69, 0xfb, 0xa1, 0x9c, 0xa6, 0x75, 0x49, 0x7d, 0x60, 0x73, 0x40, 0x42, 0xc4, 0x13, 0x0a, 0x95, + 0x79, 0x1e, 0x04, 0x83, 0x94, 0x99, 0x9b, 0x1e, 0x0c, 0xe8, 0x1f, 0x54, 0xef, 0xcb, 0xc0, 0x52 + ]) + ) ) v.append( - aff(fe([0x14, 0x89, 0x73, 0xa1, 0x37, 0x87, 0x6a, 0x7a, 0xcf, 0x1d, 0xd9, 0x2e, 0x1a, 0x67, 0xed, 0x74, - 0xc0, 0xf0, 0x9c, 0x33, 0xdd, 0xdf, 0x08, 0xbf, 0x7b, 0xd1, 0x66, 0xda, 0xe6, 0xc9, 0x49, 0x08]) , - fe([0xe9, 0xdd, 0x5e, 0x55, 0xb0, 0x0a, 0xde, 0x21, 0x4c, 0x5a, 0x2e, 0xd4, 0x80, 0x3a, 0x57, 0x92, - 0x7a, 0xf1, 0xc4, 0x2c, 0x40, 0xaf, 0x2f, 0xc9, 0x92, 0x03, 0xe5, 0x5a, 0xbc, 0xdc, 0xf4, 0x09])) + aff( + fe([ + 0x14, 0x89, 0x73, 0xa1, 0x37, 0x87, 0x6a, 0x7a, 0xcf, 0x1d, 0xd9, 0x2e, 0x1a, 0x67, 0xed, 0x74, + 0xc0, 0xf0, 0x9c, 0x33, 0xdd, 0xdf, 0x08, 0xbf, 0x7b, 0xd1, 0x66, 0xda, 0xe6, 0xc9, 0x49, 0x08 + ]), + fe([ + 0xe9, 0xdd, 0x5e, 0x55, 0xb0, 0x0a, 0xde, 0x21, 0x4c, 0x5a, 0x2e, 0xd4, 0x80, 0x3a, 0x57, 0x92, + 0x7a, 0xf1, 0xc4, 0x2c, 0x40, 0xaf, 0x2f, 0xc9, 0x92, 0x03, 0xe5, 0x5a, 0xbc, 0xdc, 0xf4, 0x09 + ]) + ) ) v.append( - aff(fe([0xf3, 0xe1, 0x2b, 0x7c, 0x05, 0x86, 0x80, 0x93, 0x4a, 0xad, 0xb4, 0x8f, 0x7e, 0x99, 0x0c, 0xfd, - 0xcd, 0xef, 0xd1, 0xff, 0x2c, 0x69, 0x34, 0x13, 0x41, 0x64, 0xcf, 0x3b, 0xd0, 0x90, 0x09, 0x1e]) , - fe([0x9d, 0x45, 0xd6, 0x80, 0xe6, 0x45, 0xaa, 0xf4, 0x15, 0xaa, 0x5c, 0x34, 0x87, 0x99, 0xa2, 0x8c, - 0x26, 0x84, 0x62, 0x7d, 0xb6, 0x29, 0xc0, 0x52, 0xea, 0xf5, 0x81, 0x18, 0x0f, 0x35, 0xa9, 0x0e])) + aff( + fe([ + 0xf3, 0xe1, 0x2b, 0x7c, 0x05, 0x86, 0x80, 0x93, 0x4a, 0xad, 0xb4, 0x8f, 0x7e, 0x99, 0x0c, 0xfd, + 0xcd, 0xef, 0xd1, 0xff, 0x2c, 0x69, 0x34, 0x13, 0x41, 0x64, 0xcf, 0x3b, 0xd0, 0x90, 0x09, 0x1e + ]), + fe([ + 0x9d, 0x45, 0xd6, 0x80, 0xe6, 0x45, 0xaa, 0xf4, 0x15, 0xaa, 0x5c, 0x34, 0x87, 0x99, 0xa2, 0x8c, + 0x26, 0x84, 0x62, 0x7d, 0xb6, 0x29, 0xc0, 0x52, 0xea, 0xf5, 0x81, 0x18, 0x0f, 0x35, 0xa9, 0x0e + ]) + ) ) v.append( - aff(fe([0xe7, 0x20, 0x72, 0x7c, 0x6d, 0x94, 0x5f, 0x52, 0x44, 0x54, 0xe3, 0xf1, 0xb2, 0xb0, 0x36, 0x46, - 0x0f, 0xae, 0x92, 0xe8, 0x70, 0x9d, 0x6e, 0x79, 0xb1, 0xad, 0x37, 0xa9, 0x5f, 0xc0, 0xde, 0x03]) , - fe([0x15, 0x55, 0x37, 0xc6, 0x1c, 0x27, 0x1c, 0x6d, 0x14, 0x4f, 0xca, 0xa4, 0xc4, 0x88, 0x25, 0x46, - 0x39, 0xfc, 0x5a, 0xe5, 0xfe, 0x29, 0x11, 0x69, 0xf5, 0x72, 0x84, 0x4d, 0x78, 0x9f, 0x94, 0x15])) + aff( + fe([ + 0xe7, 0x20, 0x72, 0x7c, 0x6d, 0x94, 0x5f, 0x52, 0x44, 0x54, 0xe3, 0xf1, 0xb2, 0xb0, 0x36, 0x46, + 0x0f, 0xae, 0x92, 0xe8, 0x70, 0x9d, 0x6e, 0x79, 0xb1, 0xad, 0x37, 0xa9, 0x5f, 0xc0, 0xde, 0x03 + ]), + fe([ + 0x15, 0x55, 0x37, 0xc6, 0x1c, 0x27, 0x1c, 0x6d, 0x14, 0x4f, 0xca, 0xa4, 0xc4, 0x88, 0x25, 0x46, + 0x39, 0xfc, 0x5a, 0xe5, 0xfe, 0x29, 0x11, 0x69, 0xf5, 0x72, 0x84, 0x4d, 0x78, 0x9f, 0x94, 0x15 + ]) + ) ) v.append( - aff(fe([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), - fe([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])) + aff( + fe([ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]), + fe([ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + ]) + ) ) v.append( - aff(fe([0xec, 0xd3, 0xff, 0x57, 0x0b, 0xb0, 0xb2, 0xdc, 0xf8, 0x4f, 0xe2, 0x12, 0xd5, 0x36, 0xbe, 0x6b, - 0x09, 0x43, 0x6d, 0xa3, 0x4d, 0x90, 0x2d, 0xb8, 0x74, 0xe8, 0x71, 0x45, 0x19, 0x8b, 0x0c, 0x6a]) , - fe([0xb8, 0x42, 0x1c, 0x03, 0xad, 0x2c, 0x03, 0x8e, 0xac, 0xd7, 0x98, 0x29, 0x13, 0xc6, 0x02, 0x29, - 0xb5, 0xd4, 0xe7, 0xcf, 0xcc, 0x8b, 0x83, 0xec, 0x35, 0xc7, 0x9c, 0x74, 0xb7, 0xad, 0x85, 0x5f])) + aff( + fe([ + 0xec, 0xd3, 0xff, 0x57, 0x0b, 0xb0, 0xb2, 0xdc, 0xf8, 0x4f, 0xe2, 0x12, 0xd5, 0x36, 0xbe, 0x6b, + 0x09, 0x43, 0x6d, 0xa3, 0x4d, 0x90, 0x2d, 0xb8, 0x74, 0xe8, 0x71, 0x45, 0x19, 0x8b, 0x0c, 0x6a + ]), + fe([ + 0xb8, 0x42, 0x1c, 0x03, 0xad, 0x2c, 0x03, 0x8e, 0xac, 0xd7, 0x98, 0x29, 0x13, 0xc6, 0x02, 0x29, + 0xb5, 0xd4, 0xe7, 0xcf, 0xcc, 0x8b, 0x83, 0xec, 0x35, 0xc7, 0x9c, 0x74, 0xb7, 0xad, 0x85, 0x5f + ]) + ) ) v.append( - aff(fe([0x78, 0x84, 0xe1, 0x56, 0x45, 0x69, 0x68, 0x5a, 0x4f, 0xb8, 0xb1, 0x29, 0xff, 0x33, 0x03, 0x31, - 0xb7, 0xcb, 0x96, 0x25, 0xe6, 0xe6, 0x41, 0x98, 0x1a, 0xbb, 0x03, 0x56, 0xf2, 0xb2, 0x91, 0x34]) , - fe([0x2c, 0x6c, 0xf7, 0x66, 0xa4, 0x62, 0x6b, 0x39, 0xb3, 0xba, 0x65, 0xd3, 0x1c, 0xf8, 0x11, 0xaa, - 0xbe, 0xdc, 0x80, 0x59, 0x87, 0xf5, 0x7b, 0xe5, 0xe3, 0xb3, 0x3e, 0x39, 0xda, 0xbe, 0x88, 0x09])) + aff( + fe([ + 0x78, 0x84, 0xe1, 0x56, 0x45, 0x69, 0x68, 0x5a, 0x4f, 0xb8, 0xb1, 0x29, 0xff, 0x33, 0x03, 0x31, + 0xb7, 0xcb, 0x96, 0x25, 0xe6, 0xe6, 0x41, 0x98, 0x1a, 0xbb, 0x03, 0x56, 0xf2, 0xb2, 0x91, 0x34 + ]), + fe([ + 0x2c, 0x6c, 0xf7, 0x66, 0xa4, 0x62, 0x6b, 0x39, 0xb3, 0xba, 0x65, 0xd3, 0x1c, 0xf8, 0x11, 0xaa, + 0xbe, 0xdc, 0x80, 0x59, 0x87, 0xf5, 0x7b, 0xe5, 0xe3, 0xb3, 0x3e, 0x39, 0xda, 0xbe, 0x88, 0x09 + ]) + ) ) v.append( - aff(fe([0x8b, 0xf1, 0xa0, 0xf5, 0xdc, 0x29, 0xb4, 0xe2, 0x07, 0xc6, 0x7a, 0x00, 0xd0, 0x89, 0x17, 0x51, - 0xd4, 0xbb, 0xd4, 0x22, 0xea, 0x7e, 0x7d, 0x7c, 0x24, 0xea, 0xf2, 0xe8, 0x22, 0x12, 0x95, 0x06]) , - fe([0xda, 0x7c, 0xa4, 0x0c, 0xf4, 0xba, 0x6e, 0xe1, 0x89, 0xb5, 0x59, 0xca, 0xf1, 0xc0, 0x29, 0x36, - 0x09, 0x44, 0xe2, 0x7f, 0xd1, 0x63, 0x15, 0x99, 0xea, 0x25, 0xcf, 0x0c, 0x9d, 0xc0, 0x44, 0x6f])) + aff( + fe([ + 0x8b, 0xf1, 0xa0, 0xf5, 0xdc, 0x29, 0xb4, 0xe2, 0x07, 0xc6, 0x7a, 0x00, 0xd0, 0x89, 0x17, 0x51, + 0xd4, 0xbb, 0xd4, 0x22, 0xea, 0x7e, 0x7d, 0x7c, 0x24, 0xea, 0xf2, 0xe8, 0x22, 0x12, 0x95, 0x06 + ]), + fe([ + 0xda, 0x7c, 0xa4, 0x0c, 0xf4, 0xba, 0x6e, 0xe1, 0x89, 0xb5, 0x59, 0xca, 0xf1, 0xc0, 0x29, 0x36, + 0x09, 0x44, 0xe2, 0x7f, 0xd1, 0x63, 0x15, 0x99, 0xea, 0x25, 0xcf, 0x0c, 0x9d, 0xc0, 0x44, 0x6f + ]) + ) ) v.append( - aff(fe([0x1d, 0x86, 0x4e, 0xcf, 0xf7, 0x37, 0x10, 0x25, 0x8f, 0x12, 0xfb, 0x19, 0xfb, 0xe0, 0xed, 0x10, - 0xc8, 0xe2, 0xf5, 0x75, 0xb1, 0x33, 0xc0, 0x96, 0x0d, 0xfb, 0x15, 0x6c, 0x0d, 0x07, 0x5f, 0x05]) , - fe([0x69, 0x3e, 0x47, 0x97, 0x2c, 0xaf, 0x52, 0x7c, 0x78, 0x83, 0xad, 0x1b, 0x39, 0x82, 0x2f, 0x02, - 0x6f, 0x47, 0xdb, 0x2a, 0xb0, 0xe1, 0x91, 0x99, 0x55, 0xb8, 0x99, 0x3a, 0xa0, 0x44, 0x11, 0x51]) + aff( + fe([ + 0x1d, 0x86, 0x4e, 0xcf, 0xf7, 0x37, 0x10, 0x25, 0x8f, 0x12, 0xfb, 0x19, 0xfb, 0xe0, 0xed, 0x10, + 0xc8, 0xe2, 0xf5, 0x75, 0xb1, 0x33, 0xc0, 0x96, 0x0d, 0xfb, 0x15, 0x6c, 0x0d, 0x07, 0x5f, 0x05 + ]), + fe([ + 0x69, 0x3e, 0x47, 0x97, 0x2c, 0xaf, 0x52, 0x7c, 0x78, 0x83, 0xad, 0x1b, 0x39, 0x82, 0x2f, 0x02, + 0x6f, 0x47, 0xdb, 0x2a, 0xb0, 0xe1, 0x91, 0x99, 0x55, 0xb8, 0x99, 0x3a, 0xa0, 0x44, 0x11, 0x51 + ]) ) ) if v.count != 85 * 5 { diff --git a/LiskKit/Sources/Crypto/MnemonicPassphrase.swift b/LiskKit/Sources/Crypto/MnemonicPassphrase.swift index be7033ea1..91dec7472 100644 --- a/LiskKit/Sources/Crypto/MnemonicPassphrase.swift +++ b/LiskKit/Sources/Crypto/MnemonicPassphrase.swift @@ -68,5 +68,136 @@ extension FixedWidthInteger { extension MnemonicPassphrase { - private static let words: [String] = ["abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident", "account", "accuse", "achieve", "acid", "acoustic", "acquire", "across", "act", "action", "actor", "actress", "actual", "adapt", "add", "addict", "address", "adjust", "admit", "adult", "advance", "advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent", "agree", "ahead", "aim", "air", "airport", "aisle", "alarm", "album", "alcohol", "alert", "alien", "all", "alley", "allow", "almost", "alone", "alpha", "already", "also", "alter", "always", "amateur", "amazing", "among", "amount", "amused", "analyst", "anchor", "ancient", "anger", "angle", "angry", "animal", "ankle", "announce", "annual", "another", "answer", "antenna", "antique", "anxiety", "any", "apart", "apology", "appear", "apple", "approve", "april", "arch", "arctic", "area", "arena", "argue", "arm", "armed", "armor", "army", "around", "arrange", "arrest", "arrive", "arrow", "art", "artefact", "artist", "artwork", "ask", "aspect", "assault", "asset", "assist", "assume", "asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", "auction", "audit", "august", "aunt", "author", "auto", "autumn", "average", "avocado", "avoid", "awake", "aware", "away", "awesome", "awful", "awkward", "axis", "baby", "bachelor", "bacon", "badge", "bag", "balance", "balcony", "ball", "bamboo", "banana", "banner", "bar", "barely", "bargain", "barrel", "base", "basic", "basket", "battle", "beach", "bean", "beauty", "because", "become", "beef", "before", "begin", "behave", "behind", "believe", "below", "belt", "bench", "benefit", "best", "betray", "better", "between", "beyond", "bicycle", "bid", "bike", "bind", "biology", "bird", "birth", "bitter", "black", "blade", "blame", "blanket", "blast", "bleak", "bless", "blind", "blood", "blossom", "blouse", "blue", "blur", "blush", "board", "boat", "body", "boil", "bomb", "bone", "bonus", "book", "boost", "border", "boring", "borrow", "boss", "bottom", "bounce", "box", "boy", "bracket", "brain", "brand", "brass", "brave", "bread", "breeze", "brick", "bridge", "brief", "bright", "bring", "brisk", "broccoli", "broken", "bronze", "broom", "brother", "brown", "brush", "bubble", "buddy", "budget", "buffalo", "build", "bulb", "bulk", "bullet", "bundle", "bunker", "burden", "burger", "burst", "bus", "business", "busy", "butter", "buyer", "buzz", "cabbage", "cabin", "cable", "cactus", "cage", "cake", "call", "calm", "camera", "camp", "can", "canal", "cancel", "candy", "cannon", "canoe", "canvas", "canyon", "capable", "capital", "captain", "car", "carbon", "card", "cargo", "carpet", "carry", "cart", "case", "cash", "casino", "castle", "casual", "cat", "catalog", "catch", "category", "cattle", "caught", "cause", "caution", "cave", "ceiling", "celery", "cement", "census", "century", "cereal", "certain", "chair", "chalk", "champion", "change", "chaos", "chapter", "charge", "chase", "chat", "cheap", "check", "cheese", "chef", "cherry", "chest", "chicken", "chief", "child", "chimney", "choice", "choose", "chronic", "chuckle", "chunk", "churn", "cigar", "cinnamon", "circle", "citizen", "city", "civil", "claim", "clap", "clarify", "claw", "clay", "clean", "clerk", "clever", "click", "client", "cliff", "climb", "clinic", "clip", "clock", "clog", "close", "cloth", "cloud", "clown", "club", "clump", "cluster", "clutch", "coach", "coast", "coconut", "code", "coffee", "coil", "coin", "collect", "color", "column", "combine", "come", "comfort", "comic", "common", "company", "concert", "conduct", "confirm", "congress", "connect", "consider", "control", "convince", "cook", "cool", "copper", "copy", "coral", "core", "corn", "correct", "cost", "cotton", "couch", "country", "couple", "course", "cousin", "cover", "coyote", "crack", "cradle", "craft", "cram", "crane", "crash", "crater", "crawl", "crazy", "cream", "credit", "creek", "crew", "cricket", "crime", "crisp", "critic", "crop", "cross", "crouch", "crowd", "crucial", "cruel", "cruise", "crumble", "crunch", "crush", "cry", "crystal", "cube", "culture", "cup", "cupboard", "curious", "current", "curtain", "curve", "cushion", "custom", "cute", "cycle", "dad", "damage", "damp", "dance", "danger", "daring", "dash", "daughter", "dawn", "day", "deal", "debate", "debris", "decade", "december", "decide", "decline", "decorate", "decrease", "deer", "defense", "define", "defy", "degree", "delay", "deliver", "demand", "demise", "denial", "dentist", "deny", "depart", "depend", "deposit", "depth", "deputy", "derive", "describe", "desert", "design", "desk", "despair", "destroy", "detail", "detect", "develop", "device", "devote", "diagram", "dial", "diamond", "diary", "dice", "diesel", "diet", "differ", "digital", "dignity", "dilemma", "dinner", "dinosaur", "direct", "dirt", "disagree", "discover", "disease", "dish", "dismiss", "disorder", "display", "distance", "divert", "divide", "divorce", "dizzy", "doctor", "document", "dog", "doll", "dolphin", "domain", "donate", "donkey", "donor", "door", "dose", "double", "dove", "draft", "dragon", "drama", "drastic", "draw", "dream", "dress", "drift", "drill", "drink", "drip", "drive", "drop", "drum", "dry", "duck", "dumb", "dune", "during", "dust", "dutch", "duty", "dwarf", "dynamic", "eager", "eagle", "early", "earn", "earth", "easily", "east", "easy", "echo", "ecology", "economy", "edge", "edit", "educate", "effort", "egg", "eight", "either", "elbow", "elder", "electric", "elegant", "element", "elephant", "elevator", "elite", "else", "embark", "embody", "embrace", "emerge", "emotion", "employ", "empower", "empty", "enable", "enact", "end", "endless", "endorse", "enemy", "energy", "enforce", "engage", "engine", "enhance", "enjoy", "enlist", "enough", "enrich", "enroll", "ensure", "enter", "entire", "entry", "envelope", "episode", "equal", "equip", "era", "erase", "erode", "erosion", "error", "erupt", "escape", "essay", "essence", "estate", "eternal", "ethics", "evidence", "evil", "evoke", "evolve", "exact", "example", "excess", "exchange", "excite", "exclude", "excuse", "execute", "exercise", "exhaust", "exhibit", "exile", "exist", "exit", "exotic", "expand", "expect", "expire", "explain", "expose", "express", "extend", "extra", "eye", "eyebrow", "fabric", "face", "faculty", "fade", "faint", "faith", "fall", "false", "fame", "family", "famous", "fan", "fancy", "fantasy", "farm", "fashion", "fat", "fatal", "father", "fatigue", "fault", "favorite", "feature", "february", "federal", "fee", "feed", "feel", "female", "fence", "festival", "fetch", "fever", "few", "fiber", "fiction", "field", "figure", "file", "film", "filter", "final", "find", "fine", "finger", "finish", "fire", "firm", "first", "fiscal", "fish", "fit", "fitness", "fix", "flag", "flame", "flash", "flat", "flavor", "flee", "flight", "flip", "float", "flock", "floor", "flower", "fluid", "flush", "fly", "foam", "focus", "fog", "foil", "fold", "follow", "food", "foot", "force", "forest", "forget", "fork", "fortune", "forum", "forward", "fossil", "foster", "found", "fox", "fragile", "frame", "frequent", "fresh", "friend", "fringe", "frog", "front", "frost", "frown", "frozen", "fruit", "fuel", "fun", "funny", "furnace", "fury", "future", "gadget", "gain", "galaxy", "gallery", "game", "gap", "garage", "garbage", "garden", "garlic", "garment", "gas", "gasp", "gate", "gather", "gauge", "gaze", "general", "genius", "genre", "gentle", "genuine", "gesture", "ghost", "giant", "gift", "giggle", "ginger", "giraffe", "girl", "give", "glad", "glance", "glare", "glass", "glide", "glimpse", "globe", "gloom", "glory", "glove", "glow", "glue", "goat", "goddess", "gold", "good", "goose", "gorilla", "gospel", "gossip", "govern", "gown", "grab", "grace", "grain", "grant", "grape", "grass", "gravity", "great", "green", "grid", "grief", "grit", "grocery", "group", "grow", "grunt", "guard", "guess", "guide", "guilt", "guitar", "gun", "gym", "habit", "hair", "half", "hammer", "hamster", "hand", "happy", "harbor", "hard", "harsh", "harvest", "hat", "have", "hawk", "hazard", "head", "health", "heart", "heavy", "hedgehog", "height", "hello", "helmet", "help", "hen", "hero", "hidden", "high", "hill", "hint", "hip", "hire", "history", "hobby", "hockey", "hold", "hole", "holiday", "hollow", "home", "honey", "hood", "hope", "horn", "horror", "horse", "hospital", "host", "hotel", "hour", "hover", "hub", "huge", "human", "humble", "humor", "hundred", "hungry", "hunt", "hurdle", "hurry", "hurt", "husband", "hybrid", "ice", "icon", "idea", "identify", "idle", "ignore", "ill", "illegal", "illness", "image", "imitate", "immense", "immune", "impact", "impose", "improve", "impulse", "inch", "include", "income", "increase", "index", "indicate", "indoor", "industry", "infant", "inflict", "inform", "inhale", "inherit", "initial", "inject", "injury", "inmate", "inner", "innocent", "input", "inquiry", "insane", "insect", "inside", "inspire", "install", "intact", "interest", "into", "invest", "invite", "involve", "iron", "island", "isolate", "issue", "item", "ivory", "jacket", "jaguar", "jar", "jazz", "jealous", "jeans", "jelly", "jewel", "job", "join", "joke", "journey", "joy", "judge", "juice", "jump", "jungle", "junior", "junk", "just", "kangaroo", "keen", "keep", "ketchup", "key", "kick", "kid", "kidney", "kind", "kingdom", "kiss", "kit", "kitchen", "kite", "kitten", "kiwi", "knee", "knife", "knock", "know", "lab", "label", "labor", "ladder", "lady", "lake", "lamp", "language", "laptop", "large", "later", "latin", "laugh", "laundry", "lava", "law", "lawn", "lawsuit", "layer", "lazy", "leader", "leaf", "learn", "leave", "lecture", "left", "leg", "legal", "legend", "leisure", "lemon", "lend", "length", "lens", "leopard", "lesson", "letter", "level", "liar", "liberty", "library", "license", "life", "lift", "light", "like", "limb", "limit", "link", "lion", "liquid", "list", "little", "live", "lizard", "load", "loan", "lobster", "local", "lock", "logic", "lonely", "long", "loop", "lottery", "loud", "lounge", "love", "loyal", "lucky", "luggage", "lumber", "lunar", "lunch", "luxury", "lyrics", "machine", "mad", "magic", "magnet", "maid", "mail", "main", "major", "make", "mammal", "man", "manage", "mandate", "mango", "mansion", "manual", "maple", "marble", "march", "margin", "marine", "market", "marriage", "mask", "mass", "master", "match", "material", "math", "matrix", "matter", "maximum", "maze", "meadow", "mean", "measure", "meat", "mechanic", "medal", "media", "melody", "melt", "member", "memory", "mention", "menu", "mercy", "merge", "merit", "merry", "mesh", "message", "metal", "method", "middle", "midnight", "milk", "million", "mimic", "mind", "minimum", "minor", "minute", "miracle", "mirror", "misery", "miss", "mistake", "mix", "mixed", "mixture", "mobile", "model", "modify", "mom", "moment", "monitor", "monkey", "monster", "month", "moon", "moral", "more", "morning", "mosquito", "mother", "motion", "motor", "mountain", "mouse", "move", "movie", "much", "muffin", "mule", "multiply", "muscle", "museum", "mushroom", "music", "must", "mutual", "myself", "mystery", "myth", "naive", "name", "napkin", "narrow", "nasty", "nation", "nature", "near", "neck", "need", "negative", "neglect", "neither", "nephew", "nerve", "nest", "net", "network", "neutral", "never", "news", "next", "nice", "night", "noble", "noise", "nominee", "noodle", "normal", "north", "nose", "notable", "note", "nothing", "notice", "novel", "now", "nuclear", "number", "nurse", "nut", "oak", "obey", "object", "oblige", "obscure", "observe", "obtain", "obvious", "occur", "ocean", "october", "odor", "off", "offer", "office", "often", "oil", "okay", "old", "olive", "olympic", "omit", "once", "one", "onion", "online", "only", "open", "opera", "opinion", "oppose", "option", "orange", "orbit", "orchard", "order", "ordinary", "organ", "orient", "original", "orphan", "ostrich", "other", "outdoor", "outer", "output", "outside", "oval", "oven", "over", "own", "owner", "oxygen", "oyster", "ozone", "pact", "paddle", "page", "pair", "palace", "palm", "panda", "panel", "panic", "panther", "paper", "parade", "parent", "park", "parrot", "party", "pass", "patch", "path", "patient", "patrol", "pattern", "pause", "pave", "payment", "peace", "peanut", "pear", "peasant", "pelican", "pen", "penalty", "pencil", "people", "pepper", "perfect", "permit", "person", "pet", "phone", "photo", "phrase", "physical", "piano", "picnic", "picture", "piece", "pig", "pigeon", "pill", "pilot", "pink", "pioneer", "pipe", "pistol", "pitch", "pizza", "place", "planet", "plastic", "plate", "play", "please", "pledge", "pluck", "plug", "plunge", "poem", "poet", "point", "polar", "pole", "police", "pond", "pony", "pool", "popular", "portion", "position", "possible", "post", "potato", "pottery", "poverty", "powder", "power", "practice", "praise", "predict", "prefer", "prepare", "present", "pretty", "prevent", "price", "pride", "primary", "print", "priority", "prison", "private", "prize", "problem", "process", "produce", "profit", "program", "project", "promote", "proof", "property", "prosper", "protect", "proud", "provide", "public", "pudding", "pull", "pulp", "pulse", "pumpkin", "punch", "pupil", "puppy", "purchase", "purity", "purpose", "purse", "push", "put", "puzzle", "pyramid", "quality", "quantum", "quarter", "question", "quick", "quit", "quiz", "quote", "rabbit", "raccoon", "race", "rack", "radar", "radio", "rail", "rain", "raise", "rally", "ramp", "ranch", "random", "range", "rapid", "rare", "rate", "rather", "raven", "raw", "razor", "ready", "real", "reason", "rebel", "rebuild", "recall", "receive", "recipe", "record", "recycle", "reduce", "reflect", "reform", "refuse", "region", "regret", "regular", "reject", "relax", "release", "relief", "rely", "remain", "remember", "remind", "remove", "render", "renew", "rent", "reopen", "repair", "repeat", "replace", "report", "require", "rescue", "resemble", "resist", "resource", "response", "result", "retire", "retreat", "return", "reunion", "reveal", "review", "reward", "rhythm", "rib", "ribbon", "rice", "rich", "ride", "ridge", "rifle", "right", "rigid", "ring", "riot", "ripple", "risk", "ritual", "rival", "river", "road", "roast", "robot", "robust", "rocket", "romance", "roof", "rookie", "room", "rose", "rotate", "rough", "round", "route", "royal", "rubber", "rude", "rug", "rule", "run", "runway", "rural", "sad", "saddle", "sadness", "safe", "sail", "salad", "salmon", "salon", "salt", "salute", "same", "sample", "sand", "satisfy", "satoshi", "sauce", "sausage", "save", "say", "scale", "scan", "scare", "scatter", "scene", "scheme", "school", "science", "scissors", "scorpion", "scout", "scrap", "screen", "script", "scrub", "sea", "search", "season", "seat", "second", "secret", "section", "security", "seed", "seek", "segment", "select", "sell", "seminar", "senior", "sense", "sentence", "series", "service", "session", "settle", "setup", "seven", "shadow", "shaft", "shallow", "share", "shed", "shell", "sheriff", "shield", "shift", "shine", "ship", "shiver", "shock", "shoe", "shoot", "shop", "short", "shoulder", "shove", "shrimp", "shrug", "shuffle", "shy", "sibling", "sick", "side", "siege", "sight", "sign", "silent", "silk", "silly", "silver", "similar", "simple", "since", "sing", "siren", "sister", "situate", "six", "size", "skate", "sketch", "ski", "skill", "skin", "skirt", "skull", "slab", "slam", "sleep", "slender", "slice", "slide", "slight", "slim", "slogan", "slot", "slow", "slush", "small", "smart", "smile", "smoke", "smooth", "snack", "snake", "snap", "sniff", "snow", "soap", "soccer", "social", "sock", "soda", "soft", "solar", "soldier", "solid", "solution", "solve", "someone", "song", "soon", "sorry", "sort", "soul", "sound", "soup", "source", "south", "space", "spare", "spatial", "spawn", "speak", "special", "speed", "spell", "spend", "sphere", "spice", "spider", "spike", "spin", "spirit", "split", "spoil", "sponsor", "spoon", "sport", "spot", "spray", "spread", "spring", "spy", "square", "squeeze", "squirrel", "stable", "stadium", "staff", "stage", "stairs", "stamp", "stand", "start", "state", "stay", "steak", "steel", "stem", "step", "stereo", "stick", "still", "sting", "stock", "stomach", "stone", "stool", "story", "stove", "strategy", "street", "strike", "strong", "struggle", "student", "stuff", "stumble", "style", "subject", "submit", "subway", "success", "such", "sudden", "suffer", "sugar", "suggest", "suit", "summer", "sun", "sunny", "sunset", "super", "supply", "supreme", "sure", "surface", "surge", "surprise", "surround", "survey", "suspect", "sustain", "swallow", "swamp", "swap", "swarm", "swear", "sweet", "swift", "swim", "swing", "switch", "sword", "symbol", "symptom", "syrup", "system", "table", "tackle", "tag", "tail", "talent", "talk", "tank", "tape", "target", "task", "taste", "tattoo", "taxi", "teach", "team", "tell", "ten", "tenant", "tennis", "tent", "term", "test", "text", "thank", "that", "theme", "then", "theory", "there", "they", "thing", "this", "thought", "three", "thrive", "throw", "thumb", "thunder", "ticket", "tide", "tiger", "tilt", "timber", "time", "tiny", "tip", "tired", "tissue", "title", "toast", "tobacco", "today", "toddler", "toe", "together", "toilet", "token", "tomato", "tomorrow", "tone", "tongue", "tonight", "tool", "tooth", "top", "topic", "topple", "torch", "tornado", "tortoise", "toss", "total", "tourist", "toward", "tower", "town", "toy", "track", "trade", "traffic", "tragic", "train", "transfer", "trap", "trash", "travel", "tray", "treat", "tree", "trend", "trial", "tribe", "trick", "trigger", "trim", "trip", "trophy", "trouble", "truck", "true", "truly", "trumpet", "trust", "truth", "try", "tube", "tuition", "tumble", "tuna", "tunnel", "turkey", "turn", "turtle", "twelve", "twenty", "twice", "twin", "twist", "two", "type", "typical", "ugly", "umbrella", "unable", "unaware", "uncle", "uncover", "under", "undo", "unfair", "unfold", "unhappy", "uniform", "unique", "unit", "universe", "unknown", "unlock", "until", "unusual", "unveil", "update", "upgrade", "uphold", "upon", "upper", "upset", "urban", "urge", "usage", "use", "used", "useful", "useless", "usual", "utility", "vacant", "vacuum", "vague", "valid", "valley", "valve", "van", "vanish", "vapor", "various", "vast", "vault", "vehicle", "velvet", "vendor", "venture", "venue", "verb", "verify", "version", "very", "vessel", "veteran", "viable", "vibrant", "vicious", "victory", "video", "view", "village", "vintage", "violin", "virtual", "virus", "visa", "visit", "visual", "vital", "vivid", "vocal", "voice", "void", "volcano", "volume", "vote", "voyage", "wage", "wagon", "wait", "walk", "wall", "walnut", "want", "warfare", "warm", "warrior", "wash", "wasp", "waste", "water", "wave", "way", "wealth", "weapon", "wear", "weasel", "weather", "web", "wedding", "weekend", "weird", "welcome", "west", "wet", "whale", "what", "wheat", "wheel", "when", "where", "whip", "whisper", "wide", "width", "wife", "wild", "will", "win", "window", "wine", "wing", "wink", "winner", "winter", "wire", "wisdom", "wise", "wish", "witness", "wolf", "woman", "wonder", "wood", "wool", "word", "work", "world", "worry", "worth", "wrap", "wreck", "wrestle", "wrist", "write", "wrong", "yard", "year", "yellow", "you", "young", "youth", "zebra", "zero", "zone", "zoo"] + private static let words: [String] = [ + "abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident", "account", "accuse", "achieve", + "acid", "acoustic", "acquire", "across", "act", "action", "actor", "actress", "actual", "adapt", "add", "addict", "address", "adjust", "admit", "adult", + "advance", "advice", "aerobic", "affair", "afford", "afraid", "again", "age", "agent", "agree", "ahead", "aim", "air", "airport", "aisle", "alarm", + "album", "alcohol", "alert", "alien", "all", "alley", "allow", "almost", "alone", "alpha", "already", "also", "alter", "always", "amateur", "amazing", + "among", "amount", "amused", "analyst", "anchor", "ancient", "anger", "angle", "angry", "animal", "ankle", "announce", "annual", "another", "answer", + "antenna", "antique", "anxiety", "any", "apart", "apology", "appear", "apple", "approve", "april", "arch", "arctic", "area", "arena", "argue", "arm", + "armed", "armor", "army", "around", "arrange", "arrest", "arrive", "arrow", "art", "artefact", "artist", "artwork", "ask", "aspect", "assault", "asset", + "assist", "assume", "asthma", "athlete", "atom", "attack", "attend", "attitude", "attract", "auction", "audit", "august", "aunt", "author", "auto", + "autumn", "average", "avocado", "avoid", "awake", "aware", "away", "awesome", "awful", "awkward", "axis", "baby", "bachelor", "bacon", "badge", "bag", + "balance", "balcony", "ball", "bamboo", "banana", "banner", "bar", "barely", "bargain", "barrel", "base", "basic", "basket", "battle", "beach", "bean", + "beauty", "because", "become", "beef", "before", "begin", "behave", "behind", "believe", "below", "belt", "bench", "benefit", "best", "betray", + "better", "between", "beyond", "bicycle", "bid", "bike", "bind", "biology", "bird", "birth", "bitter", "black", "blade", "blame", "blanket", "blast", + "bleak", "bless", "blind", "blood", "blossom", "blouse", "blue", "blur", "blush", "board", "boat", "body", "boil", "bomb", "bone", "bonus", "book", + "boost", "border", "boring", "borrow", "boss", "bottom", "bounce", "box", "boy", "bracket", "brain", "brand", "brass", "brave", "bread", "breeze", + "brick", "bridge", "brief", "bright", "bring", "brisk", "broccoli", "broken", "bronze", "broom", "brother", "brown", "brush", "bubble", "buddy", + "budget", "buffalo", "build", "bulb", "bulk", "bullet", "bundle", "bunker", "burden", "burger", "burst", "bus", "business", "busy", "butter", "buyer", + "buzz", "cabbage", "cabin", "cable", "cactus", "cage", "cake", "call", "calm", "camera", "camp", "can", "canal", "cancel", "candy", "cannon", "canoe", + "canvas", "canyon", "capable", "capital", "captain", "car", "carbon", "card", "cargo", "carpet", "carry", "cart", "case", "cash", "casino", "castle", + "casual", "cat", "catalog", "catch", "category", "cattle", "caught", "cause", "caution", "cave", "ceiling", "celery", "cement", "census", "century", + "cereal", "certain", "chair", "chalk", "champion", "change", "chaos", "chapter", "charge", "chase", "chat", "cheap", "check", "cheese", "chef", + "cherry", "chest", "chicken", "chief", "child", "chimney", "choice", "choose", "chronic", "chuckle", "chunk", "churn", "cigar", "cinnamon", "circle", + "citizen", "city", "civil", "claim", "clap", "clarify", "claw", "clay", "clean", "clerk", "clever", "click", "client", "cliff", "climb", "clinic", + "clip", "clock", "clog", "close", "cloth", "cloud", "clown", "club", "clump", "cluster", "clutch", "coach", "coast", "coconut", "code", "coffee", + "coil", "coin", "collect", "color", "column", "combine", "come", "comfort", "comic", "common", "company", "concert", "conduct", "confirm", "congress", + "connect", "consider", "control", "convince", "cook", "cool", "copper", "copy", "coral", "core", "corn", "correct", "cost", "cotton", "couch", + "country", "couple", "course", "cousin", "cover", "coyote", "crack", "cradle", "craft", "cram", "crane", "crash", "crater", "crawl", "crazy", "cream", + "credit", "creek", "crew", "cricket", "crime", "crisp", "critic", "crop", "cross", "crouch", "crowd", "crucial", "cruel", "cruise", "crumble", "crunch", + "crush", "cry", "crystal", "cube", "culture", "cup", "cupboard", "curious", "current", "curtain", "curve", "cushion", "custom", "cute", "cycle", "dad", + "damage", "damp", "dance", "danger", "daring", "dash", "daughter", "dawn", "day", "deal", "debate", "debris", "decade", "december", "decide", "decline", + "decorate", "decrease", "deer", "defense", "define", "defy", "degree", "delay", "deliver", "demand", "demise", "denial", "dentist", "deny", "depart", + "depend", "deposit", "depth", "deputy", "derive", "describe", "desert", "design", "desk", "despair", "destroy", "detail", "detect", "develop", "device", + "devote", "diagram", "dial", "diamond", "diary", "dice", "diesel", "diet", "differ", "digital", "dignity", "dilemma", "dinner", "dinosaur", "direct", + "dirt", "disagree", "discover", "disease", "dish", "dismiss", "disorder", "display", "distance", "divert", "divide", "divorce", "dizzy", "doctor", + "document", "dog", "doll", "dolphin", "domain", "donate", "donkey", "donor", "door", "dose", "double", "dove", "draft", "dragon", "drama", "drastic", + "draw", "dream", "dress", "drift", "drill", "drink", "drip", "drive", "drop", "drum", "dry", "duck", "dumb", "dune", "during", "dust", "dutch", "duty", + "dwarf", "dynamic", "eager", "eagle", "early", "earn", "earth", "easily", "east", "easy", "echo", "ecology", "economy", "edge", "edit", "educate", + "effort", "egg", "eight", "either", "elbow", "elder", "electric", "elegant", "element", "elephant", "elevator", "elite", "else", "embark", "embody", + "embrace", "emerge", "emotion", "employ", "empower", "empty", "enable", "enact", "end", "endless", "endorse", "enemy", "energy", "enforce", "engage", + "engine", "enhance", "enjoy", "enlist", "enough", "enrich", "enroll", "ensure", "enter", "entire", "entry", "envelope", "episode", "equal", "equip", + "era", "erase", "erode", "erosion", "error", "erupt", "escape", "essay", "essence", "estate", "eternal", "ethics", "evidence", "evil", "evoke", + "evolve", "exact", "example", "excess", "exchange", "excite", "exclude", "excuse", "execute", "exercise", "exhaust", "exhibit", "exile", "exist", + "exit", "exotic", "expand", "expect", "expire", "explain", "expose", "express", "extend", "extra", "eye", "eyebrow", "fabric", "face", "faculty", + "fade", "faint", "faith", "fall", "false", "fame", "family", "famous", "fan", "fancy", "fantasy", "farm", "fashion", "fat", "fatal", "father", + "fatigue", "fault", "favorite", "feature", "february", "federal", "fee", "feed", "feel", "female", "fence", "festival", "fetch", "fever", "few", + "fiber", "fiction", "field", "figure", "file", "film", "filter", "final", "find", "fine", "finger", "finish", "fire", "firm", "first", "fiscal", "fish", + "fit", "fitness", "fix", "flag", "flame", "flash", "flat", "flavor", "flee", "flight", "flip", "float", "flock", "floor", "flower", "fluid", "flush", + "fly", "foam", "focus", "fog", "foil", "fold", "follow", "food", "foot", "force", "forest", "forget", "fork", "fortune", "forum", "forward", "fossil", + "foster", "found", "fox", "fragile", "frame", "frequent", "fresh", "friend", "fringe", "frog", "front", "frost", "frown", "frozen", "fruit", "fuel", + "fun", "funny", "furnace", "fury", "future", "gadget", "gain", "galaxy", "gallery", "game", "gap", "garage", "garbage", "garden", "garlic", "garment", + "gas", "gasp", "gate", "gather", "gauge", "gaze", "general", "genius", "genre", "gentle", "genuine", "gesture", "ghost", "giant", "gift", "giggle", + "ginger", "giraffe", "girl", "give", "glad", "glance", "glare", "glass", "glide", "glimpse", "globe", "gloom", "glory", "glove", "glow", "glue", "goat", + "goddess", "gold", "good", "goose", "gorilla", "gospel", "gossip", "govern", "gown", "grab", "grace", "grain", "grant", "grape", "grass", "gravity", + "great", "green", "grid", "grief", "grit", "grocery", "group", "grow", "grunt", "guard", "guess", "guide", "guilt", "guitar", "gun", "gym", "habit", + "hair", "half", "hammer", "hamster", "hand", "happy", "harbor", "hard", "harsh", "harvest", "hat", "have", "hawk", "hazard", "head", "health", "heart", + "heavy", "hedgehog", "height", "hello", "helmet", "help", "hen", "hero", "hidden", "high", "hill", "hint", "hip", "hire", "history", "hobby", "hockey", + "hold", "hole", "holiday", "hollow", "home", "honey", "hood", "hope", "horn", "horror", "horse", "hospital", "host", "hotel", "hour", "hover", "hub", + "huge", "human", "humble", "humor", "hundred", "hungry", "hunt", "hurdle", "hurry", "hurt", "husband", "hybrid", "ice", "icon", "idea", "identify", + "idle", "ignore", "ill", "illegal", "illness", "image", "imitate", "immense", "immune", "impact", "impose", "improve", "impulse", "inch", "include", + "income", "increase", "index", "indicate", "indoor", "industry", "infant", "inflict", "inform", "inhale", "inherit", "initial", "inject", "injury", + "inmate", "inner", "innocent", "input", "inquiry", "insane", "insect", "inside", "inspire", "install", "intact", "interest", "into", "invest", "invite", + "involve", "iron", "island", "isolate", "issue", "item", "ivory", "jacket", "jaguar", "jar", "jazz", "jealous", "jeans", "jelly", "jewel", "job", + "join", "joke", "journey", "joy", "judge", "juice", "jump", "jungle", "junior", "junk", "just", "kangaroo", "keen", "keep", "ketchup", "key", "kick", + "kid", "kidney", "kind", "kingdom", "kiss", "kit", "kitchen", "kite", "kitten", "kiwi", "knee", "knife", "knock", "know", "lab", "label", "labor", + "ladder", "lady", "lake", "lamp", "language", "laptop", "large", "later", "latin", "laugh", "laundry", "lava", "law", "lawn", "lawsuit", "layer", + "lazy", "leader", "leaf", "learn", "leave", "lecture", "left", "leg", "legal", "legend", "leisure", "lemon", "lend", "length", "lens", "leopard", + "lesson", "letter", "level", "liar", "liberty", "library", "license", "life", "lift", "light", "like", "limb", "limit", "link", "lion", "liquid", + "list", "little", "live", "lizard", "load", "loan", "lobster", "local", "lock", "logic", "lonely", "long", "loop", "lottery", "loud", "lounge", "love", + "loyal", "lucky", "luggage", "lumber", "lunar", "lunch", "luxury", "lyrics", "machine", "mad", "magic", "magnet", "maid", "mail", "main", "major", + "make", "mammal", "man", "manage", "mandate", "mango", "mansion", "manual", "maple", "marble", "march", "margin", "marine", "market", "marriage", + "mask", "mass", "master", "match", "material", "math", "matrix", "matter", "maximum", "maze", "meadow", "mean", "measure", "meat", "mechanic", "medal", + "media", "melody", "melt", "member", "memory", "mention", "menu", "mercy", "merge", "merit", "merry", "mesh", "message", "metal", "method", "middle", + "midnight", "milk", "million", "mimic", "mind", "minimum", "minor", "minute", "miracle", "mirror", "misery", "miss", "mistake", "mix", "mixed", + "mixture", "mobile", "model", "modify", "mom", "moment", "monitor", "monkey", "monster", "month", "moon", "moral", "more", "morning", "mosquito", + "mother", "motion", "motor", "mountain", "mouse", "move", "movie", "much", "muffin", "mule", "multiply", "muscle", "museum", "mushroom", "music", + "must", "mutual", "myself", "mystery", "myth", "naive", "name", "napkin", "narrow", "nasty", "nation", "nature", "near", "neck", "need", "negative", + "neglect", "neither", "nephew", "nerve", "nest", "net", "network", "neutral", "never", "news", "next", "nice", "night", "noble", "noise", "nominee", + "noodle", "normal", "north", "nose", "notable", "note", "nothing", "notice", "novel", "now", "nuclear", "number", "nurse", "nut", "oak", "obey", + "object", "oblige", "obscure", "observe", "obtain", "obvious", "occur", "ocean", "october", "odor", "off", "offer", "office", "often", "oil", "okay", + "old", "olive", "olympic", "omit", "once", "one", "onion", "online", "only", "open", "opera", "opinion", "oppose", "option", "orange", "orbit", + "orchard", "order", "ordinary", "organ", "orient", "original", "orphan", "ostrich", "other", "outdoor", "outer", "output", "outside", "oval", "oven", + "over", "own", "owner", "oxygen", "oyster", "ozone", "pact", "paddle", "page", "pair", "palace", "palm", "panda", "panel", "panic", "panther", "paper", + "parade", "parent", "park", "parrot", "party", "pass", "patch", "path", "patient", "patrol", "pattern", "pause", "pave", "payment", "peace", "peanut", + "pear", "peasant", "pelican", "pen", "penalty", "pencil", "people", "pepper", "perfect", "permit", "person", "pet", "phone", "photo", "phrase", + "physical", "piano", "picnic", "picture", "piece", "pig", "pigeon", "pill", "pilot", "pink", "pioneer", "pipe", "pistol", "pitch", "pizza", "place", + "planet", "plastic", "plate", "play", "please", "pledge", "pluck", "plug", "plunge", "poem", "poet", "point", "polar", "pole", "police", "pond", "pony", + "pool", "popular", "portion", "position", "possible", "post", "potato", "pottery", "poverty", "powder", "power", "practice", "praise", "predict", + "prefer", "prepare", "present", "pretty", "prevent", "price", "pride", "primary", "print", "priority", "prison", "private", "prize", "problem", + "process", "produce", "profit", "program", "project", "promote", "proof", "property", "prosper", "protect", "proud", "provide", "public", "pudding", + "pull", "pulp", "pulse", "pumpkin", "punch", "pupil", "puppy", "purchase", "purity", "purpose", "purse", "push", "put", "puzzle", "pyramid", "quality", + "quantum", "quarter", "question", "quick", "quit", "quiz", "quote", "rabbit", "raccoon", "race", "rack", "radar", "radio", "rail", "rain", "raise", + "rally", "ramp", "ranch", "random", "range", "rapid", "rare", "rate", "rather", "raven", "raw", "razor", "ready", "real", "reason", "rebel", "rebuild", + "recall", "receive", "recipe", "record", "recycle", "reduce", "reflect", "reform", "refuse", "region", "regret", "regular", "reject", "relax", + "release", "relief", "rely", "remain", "remember", "remind", "remove", "render", "renew", "rent", "reopen", "repair", "repeat", "replace", "report", + "require", "rescue", "resemble", "resist", "resource", "response", "result", "retire", "retreat", "return", "reunion", "reveal", "review", "reward", + "rhythm", "rib", "ribbon", "rice", "rich", "ride", "ridge", "rifle", "right", "rigid", "ring", "riot", "ripple", "risk", "ritual", "rival", "river", + "road", "roast", "robot", "robust", "rocket", "romance", "roof", "rookie", "room", "rose", "rotate", "rough", "round", "route", "royal", "rubber", + "rude", "rug", "rule", "run", "runway", "rural", "sad", "saddle", "sadness", "safe", "sail", "salad", "salmon", "salon", "salt", "salute", "same", + "sample", "sand", "satisfy", "satoshi", "sauce", "sausage", "save", "say", "scale", "scan", "scare", "scatter", "scene", "scheme", "school", "science", + "scissors", "scorpion", "scout", "scrap", "screen", "script", "scrub", "sea", "search", "season", "seat", "second", "secret", "section", "security", + "seed", "seek", "segment", "select", "sell", "seminar", "senior", "sense", "sentence", "series", "service", "session", "settle", "setup", "seven", + "shadow", "shaft", "shallow", "share", "shed", "shell", "sheriff", "shield", "shift", "shine", "ship", "shiver", "shock", "shoe", "shoot", "shop", + "short", "shoulder", "shove", "shrimp", "shrug", "shuffle", "shy", "sibling", "sick", "side", "siege", "sight", "sign", "silent", "silk", "silly", + "silver", "similar", "simple", "since", "sing", "siren", "sister", "situate", "six", "size", "skate", "sketch", "ski", "skill", "skin", "skirt", + "skull", "slab", "slam", "sleep", "slender", "slice", "slide", "slight", "slim", "slogan", "slot", "slow", "slush", "small", "smart", "smile", "smoke", + "smooth", "snack", "snake", "snap", "sniff", "snow", "soap", "soccer", "social", "sock", "soda", "soft", "solar", "soldier", "solid", "solution", + "solve", "someone", "song", "soon", "sorry", "sort", "soul", "sound", "soup", "source", "south", "space", "spare", "spatial", "spawn", "speak", + "special", "speed", "spell", "spend", "sphere", "spice", "spider", "spike", "spin", "spirit", "split", "spoil", "sponsor", "spoon", "sport", "spot", + "spray", "spread", "spring", "spy", "square", "squeeze", "squirrel", "stable", "stadium", "staff", "stage", "stairs", "stamp", "stand", "start", + "state", "stay", "steak", "steel", "stem", "step", "stereo", "stick", "still", "sting", "stock", "stomach", "stone", "stool", "story", "stove", + "strategy", "street", "strike", "strong", "struggle", "student", "stuff", "stumble", "style", "subject", "submit", "subway", "success", "such", + "sudden", "suffer", "sugar", "suggest", "suit", "summer", "sun", "sunny", "sunset", "super", "supply", "supreme", "sure", "surface", "surge", + "surprise", "surround", "survey", "suspect", "sustain", "swallow", "swamp", "swap", "swarm", "swear", "sweet", "swift", "swim", "swing", "switch", + "sword", "symbol", "symptom", "syrup", "system", "table", "tackle", "tag", "tail", "talent", "talk", "tank", "tape", "target", "task", "taste", + "tattoo", "taxi", "teach", "team", "tell", "ten", "tenant", "tennis", "tent", "term", "test", "text", "thank", "that", "theme", "then", "theory", + "there", "they", "thing", "this", "thought", "three", "thrive", "throw", "thumb", "thunder", "ticket", "tide", "tiger", "tilt", "timber", "time", + "tiny", "tip", "tired", "tissue", "title", "toast", "tobacco", "today", "toddler", "toe", "together", "toilet", "token", "tomato", "tomorrow", "tone", + "tongue", "tonight", "tool", "tooth", "top", "topic", "topple", "torch", "tornado", "tortoise", "toss", "total", "tourist", "toward", "tower", "town", + "toy", "track", "trade", "traffic", "tragic", "train", "transfer", "trap", "trash", "travel", "tray", "treat", "tree", "trend", "trial", "tribe", + "trick", "trigger", "trim", "trip", "trophy", "trouble", "truck", "true", "truly", "trumpet", "trust", "truth", "try", "tube", "tuition", "tumble", + "tuna", "tunnel", "turkey", "turn", "turtle", "twelve", "twenty", "twice", "twin", "twist", "two", "type", "typical", "ugly", "umbrella", "unable", + "unaware", "uncle", "uncover", "under", "undo", "unfair", "unfold", "unhappy", "uniform", "unique", "unit", "universe", "unknown", "unlock", "until", + "unusual", "unveil", "update", "upgrade", "uphold", "upon", "upper", "upset", "urban", "urge", "usage", "use", "used", "useful", "useless", "usual", + "utility", "vacant", "vacuum", "vague", "valid", "valley", "valve", "van", "vanish", "vapor", "various", "vast", "vault", "vehicle", "velvet", "vendor", + "venture", "venue", "verb", "verify", "version", "very", "vessel", "veteran", "viable", "vibrant", "vicious", "victory", "video", "view", "village", + "vintage", "violin", "virtual", "virus", "visa", "visit", "visual", "vital", "vivid", "vocal", "voice", "void", "volcano", "volume", "vote", "voyage", + "wage", "wagon", "wait", "walk", "wall", "walnut", "want", "warfare", "warm", "warrior", "wash", "wasp", "waste", "water", "wave", "way", "wealth", + "weapon", "wear", "weasel", "weather", "web", "wedding", "weekend", "weird", "welcome", "west", "wet", "whale", "what", "wheat", "wheel", "when", + "where", "whip", "whisper", "wide", "width", "wife", "wild", "will", "win", "window", "wine", "wing", "wink", "winner", "winter", "wire", "wisdom", + "wise", "wish", "witness", "wolf", "woman", "wonder", "wood", "wool", "word", "work", "world", "worry", "worth", "wrap", "wreck", "wrestle", "wrist", + "write", "wrong", "yard", "year", "yellow", "you", "young", "youth", "zebra", "zero", "zone", "zoo" + ] } diff --git a/LiskKit/Sources/Crypto/Random.swift b/LiskKit/Sources/Crypto/Random.swift index 830f0867f..765107692 100644 --- a/LiskKit/Sources/Crypto/Random.swift +++ b/LiskKit/Sources/Crypto/Random.swift @@ -7,24 +7,25 @@ // import Foundation + #if os(Linux) -import Glibc + import Glibc #endif internal struct Random { #if os(Linux) - static var initialized = false + static var initialized = false #endif static func roll(max: Int) -> Int { #if os(Linux) - if !initialized { - srandom(UInt32(time(nil))) - initialized = true - } - return Int(random() % max) + if !initialized { + srandom(UInt32(time(nil))) + initialized = true + } + return Int(random() % max) #else - return Int(arc4random_uniform(UInt32(max))) + return Int(arc4random_uniform(UInt32(max))) #endif } } diff --git a/LiskKit/Sources/Crypto/SHA256.swift b/LiskKit/Sources/Crypto/SHA256.swift index 34f14d4a2..87fed49b3 100644 --- a/LiskKit/Sources/Crypto/SHA256.swift +++ b/LiskKit/Sources/Crypto/SHA256.swift @@ -20,19 +20,19 @@ public final class SHA256 { /// The initial hash value. private static let initalHashValue: [UInt32] = [ - 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 + 0x6a09_e667, 0xbb67_ae85, 0x3c6e_f372, 0xa54f_f53a, 0x510e_527f, 0x9b05_688c, 0x1f83_d9ab, 0x5be0_cd19 ] /// The constants in the algorithm (K). private static let konstants: [UInt32] = [ - 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, - 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, - 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, - 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, - 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, - 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, - 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, - 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 + 0x428a_2f98, 0x7137_4491, 0xb5c0_fbcf, 0xe9b5_dba5, 0x3956_c25b, 0x59f1_11f1, 0x923f_82a4, 0xab1c_5ed5, + 0xd807_aa98, 0x1283_5b01, 0x2431_85be, 0x550c_7dc3, 0x72be_5d74, 0x80de_b1fe, 0x9bdc_06a7, 0xc19b_f174, + 0xe49b_69c1, 0xefbe_4786, 0x0fc1_9dc6, 0x240c_a1cc, 0x2de9_2c6f, 0x4a74_84aa, 0x5cb0_a9dc, 0x76f9_88da, + 0x983e_5152, 0xa831_c66d, 0xb003_27c8, 0xbf59_7fc7, 0xc6e0_0bf3, 0xd5a7_9147, 0x06ca_6351, 0x1429_2967, + 0x27b7_0a85, 0x2e1b_2138, 0x4d2c_6dfc, 0x5338_0d13, 0x650a_7354, 0x766a_0abb, 0x81c2_c92e, 0x9272_2c85, + 0xa2bf_e8a1, 0xa81a_664b, 0xc24b_8b70, 0xc76c_51a3, 0xd192_e819, 0xd699_0624, 0xf40e_3585, 0x106a_a070, + 0x19a4_c116, 0x1e37_6c08, 0x2748_774c, 0x34b0_bcb5, 0x391c_0cb3, 0x4ed8_aa4a, 0x5b9c_ca4f, 0x682e_6ff3, + 0x748f_82ee, 0x78a5_636f, 0x84c8_7814, 0x8cc7_0208, 0x90be_fffa, 0xa450_6ceb, 0xbef9_a3f7, 0xc671_78f2 ] /// The hash that is being computed. @@ -107,14 +107,14 @@ public final class SHA256 { case 0...15: let index = block.startIndex.advanced(by: t * 4) // Put 4 bytes in each message. - W[t] = UInt32(block[index + 0]) << 24 + W[t] = UInt32(block[index + 0]) << 24 W[t] |= UInt32(block[index + 1]) << 16 W[t] |= UInt32(block[index + 2]) << 8 W[t] |= UInt32(block[index + 3]) default: - let σ1 = W[t-2].rotateRight(by: 17) ^ W[t-2].rotateRight(by: 19) ^ (W[t-2] >> 10) - let σ0 = W[t-15].rotateRight(by: 7) ^ W[t-15].rotateRight(by: 18) ^ (W[t-15] >> 3) - W[t] = σ1 &+ W[t-7] &+ σ0 &+ W[t-16] + let σ1 = W[t - 2].rotateRight(by: 17) ^ W[t - 2].rotateRight(by: 19) ^ (W[t - 2] >> 10) + let σ0 = W[t - 15].rotateRight(by: 7) ^ W[t - 15].rotateRight(by: 18) ^ (W[t - 15] >> 3) + W[t] = σ1 &+ W[t - 7] &+ σ0 &+ W[t - 16] } } @@ -184,24 +184,24 @@ public final class SHA256 { // MARK: - Helpers -private extension UInt64 { +extension UInt64 { /// Converts the 64 bit integer into an array of single byte integers. - func toByteArray() -> [UInt8] { + fileprivate func toByteArray() -> [UInt8] { var value = self.littleEndian return withUnsafeBytes(of: &value, Array.init) } } -private extension UInt32 { +extension UInt32 { /// Rotates self by given amount. - func rotateRight(by amount: UInt32) -> UInt32 { + fileprivate func rotateRight(by amount: UInt32) -> UInt32 { return (self >> amount) | (self << (32 - amount)) } } -private extension Array { +extension Array { /// Breaks the array into the given size. - func blocks(size: Int) -> AnyIterator> { + fileprivate func blocks(size: Int) -> AnyIterator> { var currentIndex = startIndex return AnyIterator { if let nextIndex = self.index(currentIndex, offsetBy: size, limitedBy: self.endIndex) { diff --git a/LiskKit/Tests/API/Accounts/AccountsAccountTests.swift b/LiskKit/Tests/API/Accounts/AccountsAccountTests.swift index 050931c3e..28d01ce19 100644 --- a/LiskKit/Tests/API/Accounts/AccountsAccountTests.swift +++ b/LiskKit/Tests/API/Accounts/AccountsAccountTests.swift @@ -6,6 +6,7 @@ // import XCTest + @testable import LiskKit class AccountsAccountTests: LiskTestCase { diff --git a/LiskKit/Tests/API/Accounts/AccountsOpenTests.swift b/LiskKit/Tests/API/Accounts/AccountsOpenTests.swift index 2d4625fe0..20a0eb1da 100644 --- a/LiskKit/Tests/API/Accounts/AccountsOpenTests.swift +++ b/LiskKit/Tests/API/Accounts/AccountsOpenTests.swift @@ -6,6 +6,7 @@ // import XCTest + @testable import LiskKit class AccountsOpenTests: LiskTestCase { diff --git a/LiskKit/Tests/API/Accounts/AccountsPublicKeyTests.swift b/LiskKit/Tests/API/Accounts/AccountsPublicKeyTests.swift index ae9700459..b6ed88504 100644 --- a/LiskKit/Tests/API/Accounts/AccountsPublicKeyTests.swift +++ b/LiskKit/Tests/API/Accounts/AccountsPublicKeyTests.swift @@ -6,6 +6,7 @@ // import XCTest + @testable import LiskKit class AccountsPublicKeyTests: LiskTestCase { diff --git a/LiskKit/Tests/API/Blocks/BlocksGetTests.swift b/LiskKit/Tests/API/Blocks/BlocksGetTests.swift index eb9e75d39..01d86807e 100644 --- a/LiskKit/Tests/API/Blocks/BlocksGetTests.swift +++ b/LiskKit/Tests/API/Blocks/BlocksGetTests.swift @@ -6,6 +6,7 @@ // import XCTest + @testable import LiskKit class BlocksGetTests: LiskTestCase { diff --git a/LiskKit/Tests/API/Blocks/BlocksListTests.swift b/LiskKit/Tests/API/Blocks/BlocksListTests.swift index 68cc0402f..3fdae5ba1 100644 --- a/LiskKit/Tests/API/Blocks/BlocksListTests.swift +++ b/LiskKit/Tests/API/Blocks/BlocksListTests.swift @@ -6,6 +6,7 @@ // import XCTest + @testable import LiskKit class BlocksListTests: LiskTestCase { diff --git a/LiskKit/Tests/API/Delegates/DelegatesGetTests.swift b/LiskKit/Tests/API/Delegates/DelegatesGetTests.swift index d03f8c9ed..291fa1769 100644 --- a/LiskKit/Tests/API/Delegates/DelegatesGetTests.swift +++ b/LiskKit/Tests/API/Delegates/DelegatesGetTests.swift @@ -6,6 +6,7 @@ // import XCTest + @testable import LiskKit class DelegatesGetTests: LiskTestCase { diff --git a/LiskKit/Tests/API/Delegates/DelegatesListTests.swift b/LiskKit/Tests/API/Delegates/DelegatesListTests.swift index 8fae875e9..4dfbf1fb3 100644 --- a/LiskKit/Tests/API/Delegates/DelegatesListTests.swift +++ b/LiskKit/Tests/API/Delegates/DelegatesListTests.swift @@ -6,6 +6,7 @@ // import XCTest + @testable import LiskKit class DelegatesListsTests: LiskTestCase { diff --git a/LiskKit/Tests/API/Peers/PeersGetTests.swift b/LiskKit/Tests/API/Peers/PeersGetTests.swift index e5ad3d0a4..bc4c710dc 100644 --- a/LiskKit/Tests/API/Peers/PeersGetTests.swift +++ b/LiskKit/Tests/API/Peers/PeersGetTests.swift @@ -6,6 +6,7 @@ // import XCTest + @testable import LiskKit class PeersGetTests: LiskTestCase { diff --git a/LiskKit/Tests/API/Peers/PeersListTests.swift b/LiskKit/Tests/API/Peers/PeersListTests.swift index 39dd692eb..cc798e520 100644 --- a/LiskKit/Tests/API/Peers/PeersListTests.swift +++ b/LiskKit/Tests/API/Peers/PeersListTests.swift @@ -6,6 +6,7 @@ // import XCTest + @testable import LiskKit class PeersListTests: LiskTestCase { diff --git a/LiskKit/Tests/API/Signatures/SignaturesRegisterTests.swift b/LiskKit/Tests/API/Signatures/SignaturesRegisterTests.swift index 0e238c1a2..49a49cc8a 100644 --- a/LiskKit/Tests/API/Signatures/SignaturesRegisterTests.swift +++ b/LiskKit/Tests/API/Signatures/SignaturesRegisterTests.swift @@ -6,27 +6,41 @@ // import XCTest + @testable import LiskKit class SignaturesRegisterTests: LiskTestCase { -// func testMainnetRegisterNoFunds() { -// let signatures = Signatures(client: mainNetClient) -// let response = tryRequestError { signatures.register(secondSecret: exampleSecret, secret: exampleSecret, completionHandler: $0) } -// XCTAssertFalse(response.success) -// XCTAssertEqual(response.message, "Account does not have enough LSK: 5549607903333983622L balance: 0") -// } + // func testMainnetRegisterNoFunds() { + // let signatures = Signatures(client: mainNetClient) + // let response = tryRequestError { signatures.register(secondSecret: exampleSecret, secret: exampleSecret, completionHandler: $0) } + // XCTAssertFalse(response.success) + // XCTAssertEqual(response.message, "Account does not have enough LSK: 5549607903333983622L balance: 0") + // } func testRegisterTransaction() { let (publicKey, _) = try! Crypto.keys(fromPassphrase: testSecondSecret) let asset = ["signature": ["publicKey": publicKey]] - var transaction = LocalTransaction(.registerSecondPassphrase, amount: 0, timestamp: 51497510, asset: asset) + var transaction = LocalTransaction(.registerSecondPassphrase, amount: 0, timestamp: 51_497_510, asset: asset) try? transaction.sign(passphrase: testSecret) print(transaction) XCTAssertEqual(transaction.id, "6158495690989447317") XCTAssertEqual(transaction.amountBytes, [0, 0, 0, 0, 0, 0, 0, 0]) XCTAssertEqual(transaction.recipientIdBytes, [0, 0, 0, 0, 0, 0, 0, 0]) - XCTAssertEqual(transaction.assetBytes, [141, 175, 155, 74, 131, 87, 53, 107, 128, 185, 85, 166, 13, 49, 211, 238, 165, 252, 141, 32, 109, 30, 166, 238, 45, 195, 197, 232, 117, 248, 52, 182]) - XCTAssertEqual(transaction.signatureBytes, [57, 245, 208, 241, 31, 217, 229, 107, 231, 143, 128, 58, 164, 154, 117, 29, 35, 145, 191, 119, 213, 90, 132, 219, 210, 69, 222, 23, 248, 102, 252, 73, 138, 249, 250, 149, 3, 56, 88, 149, 45, 105, 29, 73, 76, 242, 245, 117, 23, 122, 104, 95, 69, 201, 101, 119, 97, 174, 186, 157, 41, 235, 106, 11]) + XCTAssertEqual( + transaction.assetBytes, + [ + 141, 175, 155, 74, 131, 87, 53, 107, 128, 185, 85, 166, 13, 49, 211, 238, 165, 252, 141, 32, 109, 30, 166, 238, 45, 195, 197, 232, 117, 248, 52, + 182 + ] + ) + XCTAssertEqual( + transaction.signatureBytes, + [ + 57, 245, 208, 241, 31, 217, 229, 107, 231, 143, 128, 58, 164, 154, 117, 29, 35, 145, 191, 119, 213, 90, 132, 219, 210, 69, 222, 23, 248, 102, + 252, 73, 138, 249, 250, 149, 3, 56, 88, 149, 45, 105, 29, 73, 76, 242, 245, 117, 23, 122, 104, 95, 69, 201, 101, 119, 97, 174, 186, 157, 41, + 235, 106, 11 + ] + ) } } diff --git a/LiskKit/Tests/API/Transactions/TransactionsGetTests.swift b/LiskKit/Tests/API/Transactions/TransactionsGetTests.swift index 8936a7e0f..05d1ae528 100644 --- a/LiskKit/Tests/API/Transactions/TransactionsGetTests.swift +++ b/LiskKit/Tests/API/Transactions/TransactionsGetTests.swift @@ -6,6 +6,7 @@ // import XCTest + @testable import LiskKit class TransactionsGetTests: LiskTestCase { diff --git a/LiskKit/Tests/API/Transactions/TransactionsListTest.swift b/LiskKit/Tests/API/Transactions/TransactionsListTest.swift index 1ab824d0a..9c4b6b723 100644 --- a/LiskKit/Tests/API/Transactions/TransactionsListTest.swift +++ b/LiskKit/Tests/API/Transactions/TransactionsListTest.swift @@ -6,6 +6,7 @@ // import XCTest + @testable import LiskKit class TransactionsListTests: LiskTestCase { diff --git a/LiskKit/Tests/API/Transactions/TransactionsSigningTest.swift b/LiskKit/Tests/API/Transactions/TransactionsSigningTest.swift index 9e65a227c..48bc0fee1 100644 --- a/LiskKit/Tests/API/Transactions/TransactionsSigningTest.swift +++ b/LiskKit/Tests/API/Transactions/TransactionsSigningTest.swift @@ -6,6 +6,7 @@ // import XCTest + @testable import LiskKit class TransactionsSigningTests: LiskTestCase { @@ -16,19 +17,28 @@ class TransactionsSigningTests: LiskTestCase { XCTAssertEqual(transaction.typeBytes, [0]) XCTAssertEqual(transaction.timestampBytes, [10, 0, 0, 0]) - XCTAssertEqual(transaction.senderPublicKeyBytes, [30, 226, 56, 16, 69, 67, 76, 11, 154, 150, 76, 190, 127, 133, 62, 29, 42, 114, 222, 13, 151, 152, 129, 230, 97, 229, 39, 142, 166, 196, 10, 72]) + XCTAssertEqual( + transaction.senderPublicKeyBytes, + [30, 226, 56, 16, 69, 67, 76, 11, 154, 150, 76, 190, 127, 133, 62, 29, 42, 114, 222, 13, 151, 152, 129, 230, 97, 229, 39, 142, 166, 196, 10, 72] + ) XCTAssertEqual(transaction.recipientIdBytes, [207, 255, 63, 233, 51, 131, 193, 241]) XCTAssertEqual(transaction.amountBytes, [0, 252, 172, 6, 0, 0, 0, 0]) - XCTAssertEqual(transaction.signature, "35d721d1524c48d32bfaf3b33fd826968d3c99d682e661cfbc666e1cd1fac48d1e58903d6e7a84fd34bcd0b2874f720aa453bc7027442adf0c29443650725106") + XCTAssertEqual( + transaction.signature, + "35d721d1524c48d32bfaf3b33fd826968d3c99d682e661cfbc666e1cd1fac48d1e58903d6e7a84fd34bcd0b2874f720aa453bc7027442adf0c29443650725106" + ) XCTAssertEqual(transaction.id, "730261182562463085") } func testSignWithRealmTimestamp() { - var transaction = LocalTransaction(.transfer, lsk: 1, recipientId: andrewAddress, timestamp: 51262230) + var transaction = LocalTransaction(.transfer, lsk: 1, recipientId: andrewAddress, timestamp: 51_262_230) try? transaction.sign(passphrase: exampleSecret) // Check bytes - XCTAssertEqual(transaction.signature, "5bcfbd0ed92df0fbb96dab8dd844b06f82aab05b89f3ac2e53b4ffc632ad96ca0a6dd9b158d754ccde08dac50a831a21bcfc16029e80710a9faf78ac94f0ec01") + XCTAssertEqual( + transaction.signature, + "5bcfbd0ed92df0fbb96dab8dd844b06f82aab05b89f3ac2e53b4ffc632ad96ca0a6dd9b158d754ccde08dac50a831a21bcfc16029e80710a9faf78ac94f0ec01" + ) XCTAssertEqual(transaction.id, "18174990303747105268") } @@ -38,11 +48,20 @@ class TransactionsSigningTests: LiskTestCase { XCTAssertEqual(transaction.typeBytes, [0]) XCTAssertEqual(transaction.timestampBytes, [10, 0, 0, 0]) - XCTAssertEqual(transaction.senderPublicKeyBytes, [30, 226, 56, 16, 69, 67, 76, 11, 154, 150, 76, 190, 127, 133, 62, 29, 42, 114, 222, 13, 151, 152, 129, 230, 97, 229, 39, 142, 166, 196, 10, 72]) + XCTAssertEqual( + transaction.senderPublicKeyBytes, + [30, 226, 56, 16, 69, 67, 76, 11, 154, 150, 76, 190, 127, 133, 62, 29, 42, 114, 222, 13, 151, 152, 129, 230, 97, 229, 39, 142, 166, 196, 10, 72] + ) XCTAssertEqual(transaction.recipientIdBytes, [207, 255, 63, 233, 51, 131, 193, 241]) XCTAssertEqual(transaction.amountBytes, [0, 252, 172, 6, 0, 0, 0, 0]) - XCTAssertEqual(transaction.signature, "35d721d1524c48d32bfaf3b33fd826968d3c99d682e661cfbc666e1cd1fac48d1e58903d6e7a84fd34bcd0b2874f720aa453bc7027442adf0c29443650725106") - XCTAssertEqual(transaction.signSignature, "e1b31c2e4b0c84d2fcd88f99ebaa0e4eba779e699501b6c9aa127869b8a5f1cb56942c661298fed76e6e25a457ebacc99ab63c3862b26d8c9b00d728fb022b08") + XCTAssertEqual( + transaction.signature, + "35d721d1524c48d32bfaf3b33fd826968d3c99d682e661cfbc666e1cd1fac48d1e58903d6e7a84fd34bcd0b2874f720aa453bc7027442adf0c29443650725106" + ) + XCTAssertEqual( + transaction.signSignature, + "e1b31c2e4b0c84d2fcd88f99ebaa0e4eba779e699501b6c9aa127869b8a5f1cb56942c661298fed76e6e25a457ebacc99ab63c3862b26d8c9b00d728fb022b08" + ) XCTAssertEqual(transaction.id, "6691152579858420672") } @@ -52,7 +71,10 @@ class TransactionsSigningTests: LiskTestCase { let options = transaction.requestOptions - XCTAssertEqual(options["signature"] as? String, "35d721d1524c48d32bfaf3b33fd826968d3c99d682e661cfbc666e1cd1fac48d1e58903d6e7a84fd34bcd0b2874f720aa453bc7027442adf0c29443650725106") + XCTAssertEqual( + options["signature"] as? String, + "35d721d1524c48d32bfaf3b33fd826968d3c99d682e661cfbc666e1cd1fac48d1e58903d6e7a84fd34bcd0b2874f720aa453bc7027442adf0c29443650725106" + ) XCTAssertEqual(options["id"] as? String, "730261182562463085") } } diff --git a/LiskKit/Tests/API/Transactions/TransactionsTransferTests.swift b/LiskKit/Tests/API/Transactions/TransactionsTransferTests.swift index 5b2d1d969..a2135b896 100644 --- a/LiskKit/Tests/API/Transactions/TransactionsTransferTests.swift +++ b/LiskKit/Tests/API/Transactions/TransactionsTransferTests.swift @@ -6,6 +6,7 @@ // import XCTest + @testable import LiskKit class TransactionsTransferTests: LiskTestCase { diff --git a/LiskKit/Tests/Crypto/MnemonicPassphraseTests.swift b/LiskKit/Tests/Crypto/MnemonicPassphraseTests.swift index adc0332ed..9afb04a28 100644 --- a/LiskKit/Tests/Crypto/MnemonicPassphraseTests.swift +++ b/LiskKit/Tests/Crypto/MnemonicPassphraseTests.swift @@ -6,6 +6,7 @@ // import XCTest + @testable import LiskKit class MnemonicPassphraseTests: LiskTestCase { diff --git a/LiskKit/Tests/Crypto/SignMessageTests.swift b/LiskKit/Tests/Crypto/SignMessageTests.swift index 249c7824f..9c011442f 100644 --- a/LiskKit/Tests/Crypto/SignMessageTests.swift +++ b/LiskKit/Tests/Crypto/SignMessageTests.swift @@ -6,6 +6,7 @@ // import XCTest + @testable import LiskKit class SignMessageTests: LiskTestCase { diff --git a/LiskKit/Tests/Crypto/WebTokenTests.swift b/LiskKit/Tests/Crypto/WebTokenTests.swift index 7a749949c..385f3b003 100644 --- a/LiskKit/Tests/Crypto/WebTokenTests.swift +++ b/LiskKit/Tests/Crypto/WebTokenTests.swift @@ -6,6 +6,7 @@ // import XCTest + @testable import LiskKit class WebTokenTests: LiskTestCase { diff --git a/LiskKit/Tests/LiskTestCase.swift b/LiskKit/Tests/LiskTestCase.swift index 0ab75da68..7caaf79b6 100644 --- a/LiskKit/Tests/LiskTestCase.swift +++ b/LiskKit/Tests/LiskTestCase.swift @@ -6,6 +6,7 @@ // import XCTest + @testable import LiskKit class LiskTestCase: XCTestCase { diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..c390ee6f4 --- /dev/null +++ b/Makefile @@ -0,0 +1,26 @@ +# Define variables +CLI_BINARY_ROOT=scripts/AdamantCLI +CLI_BINARY=scripts/AdamantCLI/adamant-cli +CLI_NAME=adamant-cli + +$(CLI_BINARY): + @$(MAKE) -C scripts/AdamantCLI + +.PHONY: configure +configure: $(CLI_BINARY) + @$(MAKE) -C scripts/AdamantCLI -f Makefile + @CLI_DIR=$(shell pwd)/$(shell dirname $(CLI_BINARY)); \ + if ! grep -q "$$CLI_DIR" $(HOME)/.zshrc; then \ + echo "path+=('$$CLI_DIR')" >> $(HOME)/.zshrc; \ + source $(HOME)/.zshrc; \ + echo "Added $$CLI_DIR/AdamantCLI to PATH"; \ + else \ + echo "Already in PATH"; \ + fi + @echo "✅ Done. Use '$(CLI_NAME)' without './'." + +.PHONY: clean +clean: + @rm -f $(CLI_BINARY) + @rm -rf $(CLI_BINARY_ROOT)/build + @echo "Removed binary: $(CLI_BINARY)" \ No newline at end of file diff --git a/MessageNotificationContentExtension/NotificationViewController.swift b/MessageNotificationContentExtension/NotificationViewController.swift index 5193f224d..270c431ff 100644 --- a/MessageNotificationContentExtension/NotificationViewController.swift +++ b/MessageNotificationContentExtension/NotificationViewController.swift @@ -6,61 +6,63 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UIKit +import CommonKit +import MarkdownKit import SnapKit +import UIKit import UserNotifications import UserNotificationsUI -import MarkdownKit -import CommonKit class NotificationViewController: UIViewController, UNNotificationContentExtension { - + private let passphraseStoreKey = "accountService.passphrase" private let sizeWithoutMessageLabel: CGFloat = 123.0 - - private lazy var securedStore: SecureStorageProtocol = { + + private lazy var SecureStore: SecureStorageProtocol = { AdamantSecureStorage() }() - + // MARK: - IBOutlets - + @IBOutlet weak var senderAvatarImageView: UIImageView! @IBOutlet weak var senderNameLabel: UILabel! @IBOutlet weak var senderAddressLabel: UILabel! - + @IBOutlet weak var messageLabel: UILabel! @IBOutlet weak var dateLabel: UILabel! - + // MARK: - Lifecycle - + override func viewDidLoad() { super.viewDidLoad() - + senderAvatarImageView.tintColor = UIColor.adamant.primary senderNameLabel.text = "" senderAddressLabel.text = "" messageLabel.text = "" dateLabel.text = "" - + updateTextColors() } - + func didReceive(_ notification: UNNotification) { // MARK: 0. Necessary services let avatarService = AdamantAvatarService() - let keychainStore = KeychainStore(secureStorage: securedStore) + let keychainStore = KeychainStore(secureStorage: SecureStore) let nativeCore = NativeAdamantCore() - + let extensionApi = ExtensionsApiFactory( core: nativeCore, - securedStore: keychainStore + SecureStore: keychainStore ).make() - + var keypair: Keypair? - + // MARK: 1. Get the transaction let trs: Transaction? - if let transactionRaw = notification.request.content.userInfo[AdamantNotificationUserInfoKeys.transaction] as? String, let data = transactionRaw.data(using: .utf8) { + if let transactionRaw = notification.request.content.userInfo[AdamantNotificationUserInfoKeys.transaction] as? String, + let data = transactionRaw.data(using: .utf8) + { trs = try? JSONDecoder().decode(Transaction.self, from: data) } else { guard let raw = notification.request.content.userInfo[AdamantNotificationUserInfoKeys.transactionId] as? String, let id = UInt64(raw) else { @@ -70,70 +72,75 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi trs = extensionApi.getTransaction(by: id) } - + guard let transaction = trs else { showError() return } - + // MARK: 2. Working with transaction - + let message: String if let raw = notification.request.content.userInfo[AdamantNotificationUserInfoKeys.decodedMessage] as? String { message = raw } else { guard let passphrase: String = keychainStore.get(passphraseStoreKey), - let keys = nativeCore.createKeypairFor(passphrase: passphrase), - let chat = transaction.asset.chat, - let raw = nativeCore.decodeMessage( + let keys = nativeCore.createKeypairFor(passphrase: passphrase, password: .empty), + let chat = transaction.asset.chat, + let raw = nativeCore.decodeMessage( rawMessage: chat.message, rawNonce: chat.ownMessage, senderPublicKey: transaction.senderPublicKey, privateKey: keys.privateKey - ) else { + ) + else { showError() return } - + message = raw keypair = keys } - + // MARK: 3. Names let senderName: String? - + // We have cached name if let name = notification.request.content.userInfo[AdamantNotificationUserInfoKeys.partnerDisplayName] as? String { senderName = name } // No name, but we have flag - skip it - else if let flag = notification.request.content.userInfo[AdamantNotificationUserInfoKeys.partnerNoDislpayNameKey] as? String, flag == AdamantNotificationUserInfoKeys.partnerNoDisplayNameValue { + else if let flag = notification.request.content.userInfo[AdamantNotificationUserInfoKeys.partnerNoDislpayNameKey] as? String, + flag == AdamantNotificationUserInfoKeys.partnerNoDisplayNameValue + { senderName = nil } // No name, no flag - something broke. Check sender name, if we have a recipient address else if let recipient = notification.request.content.userInfo[AdamantNotificationUserInfoKeys.pushRecipient] as? String { - + let key: Keypair? if let keypair = keypair { key = keypair - } else if let passphrase: String = keychainStore.get(passphraseStoreKey), let keypair = nativeCore.createKeypairFor(passphrase: passphrase) { + } else if let passphrase: String = keychainStore.get(passphraseStoreKey), + let keypair = nativeCore.createKeypairFor(passphrase: passphrase, password: .empty) + { key = keypair } else { key = nil } - + let id = transaction.senderId if let key = key { checkName(of: id, for: recipient, api: extensionApi, core: nativeCore, keypair: key) } - + senderName = nil } else { senderName = nil } - + // MARK: 3. Setting UI - + if let name = senderName { senderNameLabel.text = name senderAddressLabel.text = transaction.senderId @@ -141,18 +148,18 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi senderNameLabel.text = transaction.senderId senderAddressLabel.text = nil } - + senderAvatarImageView.image = avatarService.avatar(for: transaction.senderPublicKey, size: Double(senderAvatarImageView.frame.height)) - + let parsed = MarkdownParser(font: messageLabel.font).parse(message) if parsed.string.count != message.count { messageLabel.attributedText = parsed } else { messageLabel.text = message } - + dateLabel.text = transaction.date.humanizedDateTime() - + // MARK: 4. View size messageLabel.setNeedsLayout() messageLabel.layoutIfNeeded() @@ -175,43 +182,48 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi dateLabel.textColor = .black } } - + private func showError(with message: String? = nil) { let warningView = NotificationWarningView() - - warningView.message = message.map { .adamant.notifications.error(with: $0) } + + warningView.message = + message.map { .adamant.notifications.error(with: $0) } ?? .adamant.notifications.error - + view.addSubview(warningView) warningView.snp.makeConstraints { $0.directionalEdges.equalToSuperview() } } - + private func checkName(of sender: String, for recipient: String, api: ExtensionsApi, core: NativeAdamantCore, keypair: Keypair) { DispatchQueue.global(qos: .userInitiated).async { [weak self] in guard let addressBook = api.getAddressBook(for: recipient, core: core, keypair: keypair), let name = addressBook[sender]?.displayName else { return } - + DispatchQueue.main.async { guard let vc = self else { return } - + let address = self?.senderNameLabel.text - - UIView.transition(with: vc.senderAddressLabel, - duration: 0.1, - options: .transitionCrossDissolve, - animations: { vc.senderAddressLabel.text = address }, - completion: nil) - - UIView.transition(with: vc.senderNameLabel, - duration: 0.1, - options: .transitionCrossDissolve, - animations: { vc.senderNameLabel.text = name }, - completion: nil) + + UIView.transition( + with: vc.senderAddressLabel, + duration: 0.1, + options: .transitionCrossDissolve, + animations: { vc.senderAddressLabel.text = address }, + completion: nil + ) + + UIView.transition( + with: vc.senderNameLabel, + duration: 0.1, + options: .transitionCrossDissolve, + animations: { vc.senderNameLabel.text = name }, + completion: nil + ) } } } diff --git a/NotificationServiceExtension/NotificationService.swift b/NotificationServiceExtension/NotificationService.swift index bc689cefd..5dd260063 100644 --- a/NotificationServiceExtension/NotificationService.swift +++ b/NotificationServiceExtension/NotificationService.swift @@ -6,19 +6,19 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UserNotifications -import MarkdownKit import CommonKit +import MarkdownKit +import UserNotifications class NotificationService: UNNotificationServiceExtension { private let passphraseStoreKey = "accountService.passphrase" - + // MARK: - Rich providers private lazy var adamantProvider: AdamantProvider = { return AdamantProvider() }() - - private lazy var securedStore: SecuredStore = { + + private lazy var SecureStore: SecureStore = { KeychainStore(secureStorage: AdamantSecureStorage()) }() @@ -31,15 +31,15 @@ class NotificationService: UNNotificationServiceExtension { DashProvider.richMessageType: DashProvider(), BtcProvider.richMessageType: BtcProvider() ] - + for token in ERC20Token.supportedTokens { let key = "\(token.symbol)_transaction".lowercased() providers[key] = ERC20Provider(token) } - + return providers }() - + // MARK: - Hanlder var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? @@ -50,41 +50,42 @@ class NotificationService: UNNotificationServiceExtension { request.content.userInfo.debugDescription, separator: "\n" ) - + self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) - + guard let bestAttemptContent = bestAttemptContent, let raw = bestAttemptContent.userInfo[AdamantNotificationUserInfoKeys.transactionId] as? String, let id = UInt64(raw), - let pushRecipient = bestAttemptContent.userInfo[AdamantNotificationUserInfoKeys.pushRecipient] as? String else { + let pushRecipient = bestAttemptContent.userInfo[AdamantNotificationUserInfoKeys.pushRecipient] as? String + else { contentHandler(request.content) return } - + // MARK: 1. Getting services let core = NativeAdamantCore() - let api = ExtensionsApiFactory(core: core, securedStore: securedStore).make() - + let api = ExtensionsApiFactory(core: core, SecureStore: SecureStore).make() + // No passphrase - no point of trying to get and decode guard - let passphrase: String = securedStore.get(passphraseStoreKey), - let keypair = core.createKeypairFor(passphrase: passphrase), + let passphrase: String = SecureStore.get(passphraseStoreKey), + let keypair = core.createKeypairFor(passphrase: passphrase, password: .empty), AdamantUtilities.generateAddress(publicKey: keypair.publicKey) == pushRecipient else { return } - + // MARK: 2. Get transaction guard let transaction = api.getTransaction(by: id) else { contentHandler(bestAttemptContent) return } - + // MARK: 3. Working on transaction let partnerAddress: String let partnerPublicKey: String var partnerName: String? var decodedMessage: String? - + if transaction.senderId == pushRecipient { partnerAddress = transaction.recipientId partnerPublicKey = transaction.recipientPublicKey ?? keypair.publicKey @@ -92,99 +93,107 @@ class NotificationService: UNNotificationServiceExtension { partnerAddress = transaction.senderId partnerPublicKey = transaction.senderPublicKey } - - let contactsBlockList: [String] = securedStore.get(StoreKey.accountService.blockList) ?? [] + + let contactsBlockList: [String] = SecureStore.get(StoreKey.accountService.blockList) ?? [] guard !contactsBlockList.contains(partnerAddress) else { return } - + // MARK: 4. Address book - if - let addressBook = api.getAddressBook(for: pushRecipient, core: core, keypair: keypair), + if let addressBook = api.getAddressBook(for: pushRecipient, core: core, keypair: keypair), let displayName = addressBook[partnerAddress]?.displayName { partnerName = displayName.checkAndReplaceSystemWallets() bestAttemptContent.userInfo[AdamantNotificationUserInfoKeys.partnerDisplayName] = displayName } else { partnerName = partnerAddress.checkAndReplaceSystemWallets() - bestAttemptContent.userInfo[AdamantNotificationUserInfoKeys.partnerNoDislpayNameKey] - = AdamantNotificationUserInfoKeys.partnerNoDisplayNameValue + bestAttemptContent.userInfo[AdamantNotificationUserInfoKeys.partnerNoDislpayNameKey] = AdamantNotificationUserInfoKeys.partnerNoDisplayNameValue } - + var shouldIgnoreNotification = false - + var isReaction = false - + // MARK: 5. Content switch transaction.type { // MARK: Messages case .chatMessage: guard let chat = transaction.asset.chat, - let message = core.decodeMessage( + let message = core.decodeMessage( rawMessage: chat.message, rawNonce: chat.ownMessage, senderPublicKey: partnerPublicKey, privateKey: keypair.privateKey - ) else { + ) + else { break } - + decodedMessage = message - + switch chat.type { // MARK: Simple messages case .messageOld: fallthrough case .message: // Strip markdown symbols - if transaction.amount > 0 { // ADM Transfer with comments + if transaction.amount > 0 { // ADM Transfer with comments // Also will strip markdown - handleAdamantTransfer(notificationContent: bestAttemptContent, partnerAddress: partnerAddress, partnerName: partnerName, amount: transaction.amount, comment: message) - } else { // Message + handleAdamantTransfer( + notificationContent: bestAttemptContent, + partnerAddress: partnerAddress, + partnerName: partnerName, + amount: transaction.amount, + comment: message + ) + } else { // Message bestAttemptContent.title = partnerName ?? partnerAddress var text = MarkdownParser().parse(message).string text = MessageProcessHelper.process(text) - + bestAttemptContent.body = text bestAttemptContent.categoryIdentifier = AdamantNotificationCategories.message } - + // MARK: Rich messages case .richMessage: var content: NotificationContent? - + // base rich if let data = message.data(using: String.Encoding.utf8), - let richContent = RichMessageTools.richContent(from: data), - let type = (richContent[RichContentKeys.type] as? String)?.lowercased(), - let provider = richMessageProviders[type], - let notificationContent = provider.notificationContent( - for: transaction, - partnerAddress: partnerAddress, - partnerName: partnerName, - richContent: richContent - ) { + let richContent = RichMessageTools.richContent(from: data), + let type = (richContent[RichContentKeys.type] as? String)?.lowercased(), + let provider = richMessageProviders[type], + let notificationContent = provider.notificationContent( + for: transaction, + partnerAddress: partnerAddress, + partnerName: partnerName, + richContent: richContent + ) + { content = notificationContent } - + // adm transfer reply if let data = message.data(using: String.Encoding.utf8), - let richContent = RichMessageTools.richContent(from: data), - richContent[RichContentKeys.reply.replyToId] != nil, - transaction.amount > 0, - let notificationContent = adamantProvider.notificationContent( - partnerAddress: partnerAddress, - partnerName: partnerName, - amount: transaction.amount, - comment: richContent[RichContentKeys.reply.replyMessage] as? String - ) { + let richContent = RichMessageTools.richContent(from: data), + richContent[RichContentKeys.reply.replyToId] != nil, + transaction.amount > 0, + let notificationContent = adamantProvider.notificationContent( + partnerAddress: partnerAddress, + partnerName: partnerName, + amount: transaction.amount, + comment: richContent[RichContentKeys.reply.replyMessage] as? String + ) + { content = notificationContent } - + // message reply if let data = message.data(using: String.Encoding.utf8), - let richContent = RichMessageTools.richContent(from: data), - let message = richContent[RichContentKeys.reply.replyMessage] as? String, - richContent[RichContentKeys.reply.replyToId] != nil, - transaction.amount <= 0 { + let richContent = RichMessageTools.richContent(from: data), + let message = richContent[RichContentKeys.reply.replyMessage] as? String, + richContent[RichContentKeys.reply.replyToId] != nil, + transaction.amount <= 0 + { var text = MarkdownParser().parse(message).string text = MessageProcessHelper.process(text) content = NotificationContent( @@ -195,39 +204,42 @@ class NotificationService: UNNotificationServiceExtension { categoryIdentifier: AdamantNotificationCategories.message ) } - + // rich transfer reply if let data = message.data(using: String.Encoding.utf8), - let richContent = RichMessageTools.richContent(from: data), - let transferContent = richContent[RichContentKeys.reply.replyMessage] as? [String: String], - let type = transferContent[RichContentKeys.type]?.lowercased(), - let provider = richMessageProviders[type], - let notificationContent = provider.notificationContent( - for: transaction, - partnerAddress: partnerAddress, - partnerName: partnerName, - richContent: transferContent - ) { + let richContent = RichMessageTools.richContent(from: data), + let transferContent = richContent[RichContentKeys.reply.replyMessage] as? [String: String], + let type = transferContent[RichContentKeys.type]?.lowercased(), + let provider = richMessageProviders[type], + let notificationContent = provider.notificationContent( + for: transaction, + partnerAddress: partnerAddress, + partnerName: partnerName, + richContent: transferContent + ) + { content = notificationContent } - + // reaction if let data = message.data(using: String.Encoding.utf8), - let richContent = RichMessageTools.richContent(from: data), - let reaction = richContent[RichContentKeys.react.react_message] as? String, - richContent[RichContentKeys.react.reactto_id] != nil { - + let richContent = RichMessageTools.richContent(from: data), + let reaction = richContent[RichContentKeys.react.react_message] as? String, + richContent[RichContentKeys.react.reactto_id] != nil + { + /* Ignoring will be later guard !reaction.isEmpty else { shouldIgnoreNotification = true break } */ - - let text = reaction.isEmpty - ? NotificationStrings.modifiedReaction - : "\(NotificationStrings.reacted) \(reaction)" - + + let text = + reaction.isEmpty + ? NotificationStrings.modifiedReaction + : "\(NotificationStrings.reacted) \(reaction)" + content = NotificationContent( title: partnerName ?? partnerAddress, subtitle: nil, @@ -235,16 +247,17 @@ class NotificationService: UNNotificationServiceExtension { attachments: nil, categoryIdentifier: AdamantNotificationCategories.message ) - + isReaction = true } - + // rich file reply if let data = message.data(using: String.Encoding.utf8), - let richContent = RichMessageTools.richContent(from: data), - let replyMessage = richContent[RichContentKeys.reply.replyMessage] as? [String: Any], - replyMessage[RichContentKeys.file.files] is [[String: Any]] { - + let richContent = RichMessageTools.richContent(from: data), + let replyMessage = richContent[RichContentKeys.reply.replyMessage] as? [String: Any], + replyMessage[RichContentKeys.file.files] is [[String: Any]] + { + let text = FilePresentationHelper.getFilePresentationText(richContent) content = NotificationContent( title: partnerName ?? partnerAddress, @@ -254,12 +267,13 @@ class NotificationService: UNNotificationServiceExtension { categoryIdentifier: AdamantNotificationCategories.message ) } - + // rich file if let data = message.data(using: String.Encoding.utf8), - let richContent = RichMessageTools.richContent(from: data), - richContent[RichContentKeys.file.files] is [[String: Any]] { - + let richContent = RichMessageTools.richContent(from: data), + richContent[RichContentKeys.file.files] is [[String: Any]] + { + let text = FilePresentationHelper.getFilePresentationText(richContent) content = NotificationContent( title: partnerName ?? partnerAddress, @@ -269,72 +283,79 @@ class NotificationService: UNNotificationServiceExtension { categoryIdentifier: AdamantNotificationCategories.message ) } - + guard let content = content else { break } - + bestAttemptContent.title = content.title bestAttemptContent.body = content.body - + if let subtitle = content.subtitle { bestAttemptContent.subtitle = subtitle } if let attachments = content.attachments { bestAttemptContent.attachments = attachments } if let categoryIdentifier = content.categoryIdentifier { bestAttemptContent.categoryIdentifier = categoryIdentifier } - + case .unknown: break case .signal: break } - + // MARK: Transfers case .send: - handleAdamantTransfer(notificationContent: bestAttemptContent, partnerAddress: partnerAddress, partnerName: partnerName, amount: transaction.amount, comment: nil) - + handleAdamantTransfer( + notificationContent: bestAttemptContent, + partnerAddress: partnerAddress, + partnerName: partnerName, + amount: transaction.amount, + comment: nil + ) + default: break } - + guard !shouldIgnoreNotification else { contentHandler(UNNotificationContent()) return } - + bestAttemptContent.sound = getSound( - securedStore: securedStore, + SecureStore: SecureStore, isReaction: isReaction ) - + // MARK: 6. Other configurations bestAttemptContent.threadIdentifier = partnerAddress - + // MARK: 7. Caching downloaded transaction, to avoid downloading ang decoding it in ContentExtensions if let data = try? JSONEncoder().encode(transaction), let transactionRaw = String(data: data, encoding: .utf8) { bestAttemptContent.userInfo[AdamantNotificationUserInfoKeys.transaction] = transactionRaw } bestAttemptContent.userInfo[AdamantNotificationUserInfoKeys.decodedMessage] = decodedMessage - + contentHandler(bestAttemptContent) } - + override func serviceExtensionTimeWillExpire() { // Called just before the extension will be terminated by the system. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. - if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { + if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { contentHandler(bestAttemptContent) } } - - private func getSound(securedStore: SecuredStore, isReaction: Bool) -> UNNotificationSound? { - let key = isReaction - ? StoreKey.notificationsService.notificationsReactionSound - : StoreKey.notificationsService.notificationsSound - - let sound: String = securedStore.get(key) ?? .empty - - return sound.isEmpty - ? nil - : UNNotificationSound(named: UNNotificationSoundName(sound)) + + private func getSound(SecureStore: SecureStore, isReaction: Bool) -> UNNotificationSound? { + let key = + isReaction + ? StoreKey.notificationsService.notificationsReactionSound + : StoreKey.notificationsService.notificationsSound + + let sound: String = SecureStore.get(key) ?? .empty + + return sound.isEmpty + ? nil + : UNNotificationSound(named: UNNotificationSoundName(sound)) } - + private func handleAdamantTransfer( notificationContent: UNMutableNotificationContent, partnerAddress address: String, @@ -345,18 +366,18 @@ class NotificationService: UNNotificationServiceExtension { guard let content = adamantProvider.notificationContent(partnerAddress: address, partnerName: name, amount: amount, comment: comment) else { return } - + notificationContent.title = content.title notificationContent.body = content.body - + if let subtitle = content.subtitle { notificationContent.subtitle = subtitle } - + if let attachments = content.attachments { notificationContent.attachments = attachments } - + notificationContent.categoryIdentifier = AdamantNotificationCategories.transfer } } diff --git a/NotificationServiceExtension/WalletImages/lisk_notificationContent.png b/NotificationServiceExtension/WalletImages/lisk_notificationContent.png deleted file mode 100644 index f079d9577..000000000 Binary files a/NotificationServiceExtension/WalletImages/lisk_notificationContent.png and /dev/null differ diff --git a/NotificationsShared/Localization/NotificationStrings.swift b/NotificationsShared/Localization/NotificationStrings.swift index 73552f4e0..ade16bfbb 100644 --- a/NotificationsShared/Localization/NotificationStrings.swift +++ b/NotificationsShared/Localization/NotificationStrings.swift @@ -13,47 +13,47 @@ enum NotificationStrings { "NotificationsService.Error.RegistrationRemotesFormat", comment: "Notifications: Something went wrong while registering remote notifications. %@ for description" ) - + static let newMessageTitle = NSLocalizedString( "NotificationsService.NewMessage.Title", comment: "Notifications: New message notification title" ) - + static let newMessageBodySingle = NSLocalizedString( "Notifications: New single message notification body", comment: "Notifications: Something went wrong while registering remote notifications. %@ for description" ) - + static let newTransferTitle = NSLocalizedString( "NotificationsService.NewTransfer.Title", comment: "Notifications: New transfer transaction title" ) - + static let newTransferBodySingle = NSLocalizedString( "NotificationsService.NewTransfer.BodySingle", comment: "Notifications: New single transfer transaction body" ) - + static let notificationsDisabled = NSLocalizedString( "NotificationsService.NotificationsDisabled", comment: "Notifications disabled. You can enable notifications in Settings" ) - + static let notStayedLoggedIn = NSLocalizedString( "NotificationsService.NotStayedLoggedIn", comment: "Can't turn on notifications without staying logged in" ) - + static let reacted = NSLocalizedString( "NotificationsService.Reacted", comment: "Notifications: Reacted" ) - + static let modifiedReaction = NSLocalizedString( "NotificationsService.ModifiedReaction", comment: "Notifications: Modified Reaction" ) - + static func newTransferBody(_ count: Int) -> String { .localizedStringWithFormat( NSLocalizedString( @@ -63,7 +63,7 @@ enum NotificationStrings { count ) } - + static func newMessageBody(_ count: Int) -> String { .localizedStringWithFormat( NSLocalizedString( diff --git a/Podfile b/Podfile index e37c92120..7d93044be 100644 --- a/Podfile +++ b/Podfile @@ -5,14 +5,13 @@ target 'Adamant' do pod 'FreakingSimpleRoundImageView' # Round avatars pod 'MyLittlePinpad' # Pinpad - pod 'SwiftLint' end post_install do |installer| installer.generated_projects.each do |project| project.targets.each do |target| target.build_configurations.each do |config| - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0' end end end diff --git a/Podfile.lock b/Podfile.lock index 4e31a8ce2..31535575c 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,24 +1,20 @@ PODS: - FreakingSimpleRoundImageView (1.3) - MyLittlePinpad (0.3.1) - - SwiftLint (0.49.1) DEPENDENCIES: - FreakingSimpleRoundImageView - MyLittlePinpad - - SwiftLint SPEC REPOS: trunk: - FreakingSimpleRoundImageView - MyLittlePinpad - - SwiftLint SPEC CHECKSUMS: FreakingSimpleRoundImageView: 0d687cb05da8684e85c4c2ae9945bafcbe89d2a2 MyLittlePinpad: d75682f2d817b490c49acc70a9ebd37da752281b - SwiftLint: 32ee33ded0636d0905ef6911b2b67bbaeeedafa5 -PODFILE CHECKSUM: a30619b79caa4b5a7497b0600d449f34b5620eec +PODFILE CHECKSUM: 8e9944b5cb2a51ad287543269d72da9608440839 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/PopupKit/Sources/PopupKit/Implementation/Common/Constants.swift b/PopupKit/Sources/PopupKit/Implementation/Common/Constants.swift index bb1fc42e9..de5afa931 100644 --- a/PopupKit/Sources/PopupKit/Implementation/Common/Constants.swift +++ b/PopupKit/Sources/PopupKit/Implementation/Common/Constants.swift @@ -1,6 +1,6 @@ // // Constants.swift -// +// // // Created by Andrey Golubenko on 06.12.2022. // diff --git a/PopupKit/Sources/PopupKit/Implementation/Models/AdvancedAlertModel.swift b/PopupKit/Sources/PopupKit/Implementation/Models/AdvancedAlertModel.swift index fbfdb12e1..856a9a13f 100644 --- a/PopupKit/Sources/PopupKit/Implementation/Models/AdvancedAlertModel.swift +++ b/PopupKit/Sources/PopupKit/Implementation/Models/AdvancedAlertModel.swift @@ -1,12 +1,12 @@ // // AdvancedAlertModel.swift -// +// // // Created by Andrey Golubenko on 12.04.2023. // -import UIKit import CommonKit +import UIKit public struct AdvancedAlertModel: Equatable, Hashable { public let icon: UIImage @@ -14,7 +14,7 @@ public struct AdvancedAlertModel: Equatable, Hashable { public let text: String public let secondaryButton: Button? public let primaryButton: Button - + public init( icon: UIImage, title: String?, @@ -30,11 +30,11 @@ public struct AdvancedAlertModel: Equatable, Hashable { } } -public extension AdvancedAlertModel { - struct Button: Equatable, Hashable { +extension AdvancedAlertModel { + public struct Button: Equatable, Hashable { public let title: String public let action: IDWrapper<() -> Void> - + public init(title: String, action: IDWrapper<() -> Void>) { self.title = title self.action = action diff --git a/PopupKit/Sources/PopupKit/Implementation/Models/AlertModel.swift b/PopupKit/Sources/PopupKit/Implementation/Models/AlertModel.swift index 73263299f..1b76edec4 100644 --- a/PopupKit/Sources/PopupKit/Implementation/Models/AlertModel.swift +++ b/PopupKit/Sources/PopupKit/Implementation/Models/AlertModel.swift @@ -1,6 +1,6 @@ // // AlertModel.swift -// +// // // Created by Andrey Golubenko on 06.12.2022. // diff --git a/PopupKit/Sources/PopupKit/Implementation/Models/NotificationModel.swift b/PopupKit/Sources/PopupKit/Implementation/Models/NotificationModel.swift index 26a138f78..45ab09b4a 100644 --- a/PopupKit/Sources/PopupKit/Implementation/Models/NotificationModel.swift +++ b/PopupKit/Sources/PopupKit/Implementation/Models/NotificationModel.swift @@ -1,12 +1,12 @@ // // NotificationModel.swift -// +// // // Created by Andrey Golubenko on 06.12.2022. // -import UIKit import CommonKit +import UIKit struct NotificationModel: Equatable, Hashable { let icon: UIImage? diff --git a/PopupKit/Sources/PopupKit/Implementation/Models/PopupCoordinatorModel.swift b/PopupKit/Sources/PopupKit/Implementation/Models/PopupCoordinatorModel.swift index 2c7420fd7..17966e3e8 100644 --- a/PopupKit/Sources/PopupKit/Implementation/Models/PopupCoordinatorModel.swift +++ b/PopupKit/Sources/PopupKit/Implementation/Models/PopupCoordinatorModel.swift @@ -1,6 +1,6 @@ // // PopupCoordinatorModel.swift -// +// // // Created by Andrey Golubenko on 06.12.2022. // diff --git a/PopupKit/Sources/PopupKit/Implementation/Services/AutoDismissManager.swift b/PopupKit/Sources/PopupKit/Implementation/Services/AutoDismissManager.swift index 2f625f49d..5a3f8aca4 100644 --- a/PopupKit/Sources/PopupKit/Implementation/Services/AutoDismissManager.swift +++ b/PopupKit/Sources/PopupKit/Implementation/Services/AutoDismissManager.swift @@ -1,6 +1,6 @@ // // File.swift -// +// // // Created by Andrey Golubenko on 07.12.2022. // @@ -10,29 +10,29 @@ import Foundation final class AutoDismissManager { private let popupCoordinatorModel: PopupCoordinatorModel - + private(set) var notificationDismissSubscription: AnyCancellable? private(set) var alertDismissSubscription: AnyCancellable? private(set) var toastDismissSubscription: AnyCancellable? - + init(popupCoordinatorModel: PopupCoordinatorModel) { self.popupCoordinatorModel = popupCoordinatorModel } - + func dismissNotification() { notificationDismissSubscription = setTimer { [weak self] in self?.notificationDismissSubscription = nil self?.popupCoordinatorModel.notification = nil } } - + func dismissAlert() { alertDismissSubscription = setTimer { [weak self] in self?.alertDismissSubscription = nil self?.popupCoordinatorModel.alert = nil } } - + func dismissToast() { toastDismissSubscription = setTimer { [weak self] in self?.toastDismissSubscription = nil @@ -41,10 +41,10 @@ final class AutoDismissManager { } } -private extension AutoDismissManager { - func setTimer(handler: @escaping () -> Void) -> AnyCancellable { - Timer.publish(every: autoDismissTimeInterval, on: .main, in: .common) - .autoconnect() +extension AutoDismissManager { + fileprivate func setTimer(handler: @escaping () -> Void) -> AnyCancellable { + Just(()) + .delay(for: .seconds(autoDismissTimeInterval), scheduler: DispatchQueue.main) .sink { _ in handler() } } } diff --git a/PopupKit/Sources/PopupKit/Implementation/Views/AdvancedAlertView.swift b/PopupKit/Sources/PopupKit/Implementation/Views/AdvancedAlertView.swift index 14859819b..ba37e5c52 100644 --- a/PopupKit/Sources/PopupKit/Implementation/Views/AdvancedAlertView.swift +++ b/PopupKit/Sources/PopupKit/Implementation/Views/AdvancedAlertView.swift @@ -1,18 +1,18 @@ // // AdvancedAlertView.swift -// +// // // Created by Andrey Golubenko on 12.04.2023. // -import SwiftUI import CommonKit +import SwiftUI struct AdvancedAlertView: View { @State private var width: CGFloat = .zero - + let model: AdvancedAlertModel - + var body: some View { VStack(spacing: .zero) { VStack(spacing: .zero) { @@ -24,15 +24,15 @@ struct AdvancedAlertView: View { } textView .padding(.bottom, bigSpacing) - + if let secondaryButton = model.secondaryButton { makeSecondaryButton(model: secondaryButton) .padding(.bottom, bigSpacing) } }.padding(.horizontal, bigSpacing) - .background(widthReader) - .onPreferenceChange(ViewPreferenceKey.self) { width = $0 } - + .background(widthReader) + .onPreferenceChange(ViewPreferenceKey.self) { width = $0 } + Group { Divider() primaryButton @@ -44,42 +44,42 @@ struct AdvancedAlertView: View { } } -private extension AdvancedAlertView { - struct ViewPreferenceKey: PreferenceKey { +extension AdvancedAlertView { + fileprivate struct ViewPreferenceKey: PreferenceKey { static var defaultValue: CGFloat { .zero } - + static func reduce(value: inout Value, nextValue: () -> Value) { value += nextValue() } } - - var iconView: some View { + + fileprivate var iconView: some View { Image(uiImage: model.icon) .renderingMode(.template) .foregroundColor(.secondary) .frame(squareSize: 37) } - - func makeTitleView(title: String) -> some View { + + fileprivate func makeTitleView(title: String) -> some View { Text(title) .font(.system(size: 17, weight: .bold)) } - - var textView: some View { + + fileprivate var textView: some View { Text(model.text) .multilineTextAlignment(.center) .font(.system(size: 13)) } - - var primaryButton: some View { + + fileprivate var primaryButton: some View { Button(action: model.primaryButton.action.value) { Text(model.primaryButton.title) .padding(.vertical, bigSpacing) .expanded(axes: .horizontal) } } - - var widthReader: some View { + + fileprivate var widthReader: some View { GeometryReader { Color.clear.preference( key: ViewPreferenceKey.self, @@ -87,8 +87,8 @@ private extension AdvancedAlertView { ) } } - - func makeSecondaryButton(model: AdvancedAlertModel.Button) -> some View { + + fileprivate func makeSecondaryButton(model: AdvancedAlertModel.Button) -> some View { Button(model.title, action: model.action.value) } } diff --git a/PopupKit/Sources/PopupKit/Implementation/Views/AlertView.swift b/PopupKit/Sources/PopupKit/Implementation/Views/AlertView.swift index 0a251a405..c686d028f 100644 --- a/PopupKit/Sources/PopupKit/Implementation/Views/AlertView.swift +++ b/PopupKit/Sources/PopupKit/Implementation/Views/AlertView.swift @@ -1,16 +1,16 @@ // // AlertView.swift -// +// // // Created by Andrey Golubenko on 06.12.2022. // -import SwiftUI import CommonKit +import SwiftUI struct AlertView: View { let model: AlertModel - + var body: some View { VStack(spacing: 8) { iconView @@ -28,8 +28,8 @@ struct AlertView: View { } } -private extension AlertView { - var iconView: some View { +extension AlertView { + fileprivate var iconView: some View { Group { switch model.icon { case .loading: diff --git a/PopupKit/Sources/PopupKit/Implementation/Views/NotificationPresenterView.swift b/PopupKit/Sources/PopupKit/Implementation/Views/NotificationPresenterView.swift index 2bb645388..514ad33a8 100644 --- a/PopupKit/Sources/PopupKit/Implementation/Views/NotificationPresenterView.swift +++ b/PopupKit/Sources/PopupKit/Implementation/Views/NotificationPresenterView.swift @@ -5,8 +5,8 @@ // Created by Yana Silosieva on 02.12.2024. // -import SwiftUI import CommonKit +import SwiftUI struct NotificationPresenterView: View { enum DragDirection { @@ -19,11 +19,11 @@ struct NotificationPresenterView: View { @State private var dynamicHeight: CGFloat = 0 @State private var notificationHeight: CGFloat = 0 @State private var offset: CGSize = .zero - + let model: NotificationModel let safeAreaInsets: EdgeInsets let dismissAction: () -> Void - + var body: some View { VStack { NotificationView( @@ -37,7 +37,6 @@ struct NotificationPresenterView: View { Color.clear .onAppear { notificationHeight = geometry.size.height - print("onappier") } .onChange(of: geometry.size.height) { newValue in notificationHeight = newValue @@ -48,7 +47,7 @@ struct NotificationPresenterView: View { .frame(minHeight: notificationHeight + dynamicHeight) .overlay( RoundedRectangle(cornerRadius: 10) - .stroke(Color.init(uiColor:.adamant.chatInputBarBorderColor), lineWidth: 1) + .stroke(Color.init(uiColor: .adamant.chatInputBarBorderColor), lineWidth: 1) ) .background(GeometryReader(content: processGeometry)) .onTapGesture(perform: onTap) @@ -61,19 +60,19 @@ struct NotificationPresenterView: View { .transition(.move(edge: dismissEdge)) } } -private extension NotificationPresenterView { - func processGeometry(_ geometry: GeometryProxy) -> some View { +extension NotificationPresenterView { + fileprivate func processGeometry(_ geometry: GeometryProxy) -> some View { return Color.init(uiColor: .adamant.swipeBlockColor) .cornerRadius(10) } - func onTap() { + fileprivate func onTap() { model.tapHandler?.value() dismissAction() dismissEdge = .top } } -private extension NotificationPresenterView { - var dragGesture: some Gesture { +extension NotificationPresenterView { + fileprivate var dragGesture: some Gesture { DragGesture() .onChanged { value in if dragDirection == nil || (abs(value.translation.width) <= 5 && abs(value.translation.height) <= 5) { @@ -107,9 +106,10 @@ private extension NotificationPresenterView { offset = .zero } } - - func detectDragDirection(value: DragGesture.Value) { - let horizontalDistance = abs(value.translation.width), verticalDistance = abs(value.translation.height) + + fileprivate func detectDragDirection(value: DragGesture.Value) { + let horizontalDistance = abs(value.translation.width) + let verticalDistance = abs(value.translation.height) dragDirection = verticalDistance > horizontalDistance ? .vertical : .horizontal } } diff --git a/PopupKit/Sources/PopupKit/Implementation/Views/NotificationView.swift b/PopupKit/Sources/PopupKit/Implementation/Views/NotificationView.swift index bf2515df2..3093dd025 100644 --- a/PopupKit/Sources/PopupKit/Implementation/Views/NotificationView.swift +++ b/PopupKit/Sources/PopupKit/Implementation/Views/NotificationView.swift @@ -5,13 +5,13 @@ // Created by Andrey Golubenko on 06.12.2022. // -import SwiftUI import CommonKit +import SwiftUI struct NotificationView: View { @Binding var isTextLimited: Bool let model: NotificationModel - + var body: some View { VStack(alignment: .center, spacing: 5) { HStack(alignment: .top, spacing: 10) { @@ -28,8 +28,8 @@ struct NotificationView: View { } } -private extension NotificationView { - func makeIcon(image: UIImage) -> some View { +extension NotificationView { + fileprivate func makeIcon(image: UIImage) -> some View { Image(uiImage: image) .resizable() .renderingMode(.original) @@ -38,8 +38,8 @@ private extension NotificationView { .frame(squareSize: 30) .padding(.top, 2) } - - var textStack: some View { + + fileprivate var textStack: some View { VStack(alignment: .leading, spacing: 3) { if let title = model.title { Text(title) diff --git a/PopupKit/Sources/PopupKit/Implementation/Views/PopupCoordinatorView.swift b/PopupKit/Sources/PopupKit/Implementation/Views/PopupCoordinatorView.swift index 36b9914fa..dafe395fb 100644 --- a/PopupKit/Sources/PopupKit/Implementation/Views/PopupCoordinatorView.swift +++ b/PopupKit/Sources/PopupKit/Implementation/Views/PopupCoordinatorView.swift @@ -1,23 +1,23 @@ // // PopupCoordinatorView.swift -// +// // // Created by Andrey Golubenko on 06.12.2022. // -import SwiftUI import CommonKit +import SwiftUI struct PopupCoordinatorView: View { @ObservedObject var model: PopupCoordinatorModel - + var body: some View { GeometryReader { geomerty in ZStack { if !(model.alert?.userInteractionEnabled ?? true) { BlockingView() } - + makeNotificationView(safeAreaInsets: geomerty.safeAreaInsets) makeAlertView() makeAdvancedAlertView() @@ -29,8 +29,8 @@ struct PopupCoordinatorView: View { } } -private extension PopupCoordinatorView { - func makeNotificationView(safeAreaInsets: EdgeInsets) -> some View { +extension PopupCoordinatorView { + fileprivate func makeNotificationView(safeAreaInsets: EdgeInsets) -> some View { VStack { if let notificationModel = model.notification { NotificationPresenterView( @@ -46,8 +46,8 @@ private extension PopupCoordinatorView { } .animation(.easeInOut(duration: animationDuration), value: model.notification?.hashValue) } - - func makeAlertView() -> some View { + + fileprivate func makeAlertView() -> some View { VStack { if let alertModel = model.alert, model.advancedAlert == nil { AlertView(model: alertModel) @@ -57,8 +57,8 @@ private extension PopupCoordinatorView { } .animation(.easeInOut(duration: animationDuration), value: model.alert?.hashValue) } - - func makeAdvancedAlertView() -> some View { + + fileprivate func makeAdvancedAlertView() -> some View { VStack { if let advancedAlertModel = model.advancedAlert { AdvancedAlertView(model: advancedAlertModel) @@ -68,8 +68,8 @@ private extension PopupCoordinatorView { } .animation(.easeInOut(duration: animationDuration), value: model.advancedAlert?.hashValue) } - - func makeToastView(safeAreaInsets: EdgeInsets) -> some View { + + fileprivate func makeToastView(safeAreaInsets: EdgeInsets) -> some View { VStack { Spacer() if let message = model.toastMessage { diff --git a/PopupKit/Sources/PopupKit/Implementation/Views/ToastView.swift b/PopupKit/Sources/PopupKit/Implementation/Views/ToastView.swift index d1dcf15e2..82de90410 100644 --- a/PopupKit/Sources/PopupKit/Implementation/Views/ToastView.swift +++ b/PopupKit/Sources/PopupKit/Implementation/Views/ToastView.swift @@ -1,16 +1,16 @@ // // ToastView.swift -// +// // // Created by Andrey Golubenko on 07.12.2022. // -import SwiftUI import CommonKit +import SwiftUI struct ToastView: View { let message: String - + var body: some View { Text(message) .multilineTextAlignment(.center) diff --git a/PopupKit/Sources/PopupKit/PopupManager.swift b/PopupKit/Sources/PopupKit/PopupManager.swift index 341a8ecb8..a2c90de0c 100644 --- a/PopupKit/Sources/PopupKit/PopupManager.swift +++ b/PopupKit/Sources/PopupKit/PopupManager.swift @@ -1,15 +1,15 @@ -import SwiftUI import CommonKit +import SwiftUI @MainActor public final class PopupManager { private let window = TransparentWindow(frame: UIScreen.main.bounds) private let coordinatorModel = PopupCoordinatorModel() - + private lazy var autoDismissManager = AutoDismissManager( popupCoordinatorModel: coordinatorModel ) - + public func setup() { let rootView = PopupCoordinatorView(model: coordinatorModel) let rootVC = UIHostingController(rootView: rootView) @@ -17,31 +17,31 @@ public final class PopupManager { window.rootViewController = rootVC window.isHidden = false } - + public init() {} } // MARK: - Toast -public extension PopupManager { - func showToastMessage(_ message: String) { +extension PopupManager { + public func showToastMessage(_ message: String) { coordinatorModel.toastMessage = message autoDismissManager.dismissToast() } - - func dismissToast() { + + public func dismissToast() { coordinatorModel.toastMessage = nil } } // MARK: - Alert -public extension PopupManager { - func dismissAlert() { +extension PopupManager { + public func dismissAlert() { coordinatorModel.alert = nil } - - func showProgressAlert(message: String?, userInteractionEnabled: Bool) { + + public func showProgressAlert(message: String?, userInteractionEnabled: Bool) { autoDismissManager.alertDismissSubscription?.cancel() coordinatorModel.alert = .init( icon: .loading, @@ -49,8 +49,8 @@ public extension PopupManager { userInteractionEnabled: userInteractionEnabled ) } - - func showSuccessAlert(message: String?) { + + public func showSuccessAlert(message: String?) { coordinatorModel.alert = .init( icon: .image(successImage), message: message, @@ -58,8 +58,8 @@ public extension PopupManager { ) autoDismissManager.dismissAlert() } - - func showWarningAlert(message: String?) { + + public func showWarningAlert(message: String?) { coordinatorModel.alert = .init( icon: .image(warningImage), message: message, @@ -71,8 +71,8 @@ public extension PopupManager { // MARK: - Notification -public extension PopupManager { - func showNotification( +extension PopupManager { + public func showNotification( icon: UIImage?, title: String?, description: String?, @@ -84,31 +84,34 @@ public extension PopupManager { title: title, description: description, tapHandler: tapHandler.map { .init(id: .empty, value: $0) }, - cancelAutoDismiss: .init(id: .empty, value: { [weak self] in - self?.autoDismissManager.notificationDismissSubscription?.cancel() - }) + cancelAutoDismiss: .init( + id: .empty, + value: { [weak self] in + self?.autoDismissManager.notificationDismissSubscription?.cancel() + } + ) ) - + if autoDismiss { autoDismissManager.dismissNotification() } else { autoDismissManager.notificationDismissSubscription?.cancel() } } - - func dismissNotification() { + + public func dismissNotification() { coordinatorModel.notification = nil } } // MARK: - Advanced alert -public extension PopupManager { - func dismissAdvancedAlert() { +extension PopupManager { + public func dismissAdvancedAlert() { coordinatorModel.advancedAlert = nil } - - func showAdvancedAlert(model: AdvancedAlertModel) { + + public func showAdvancedAlert(model: AdvancedAlertModel) { coordinatorModel.advancedAlert = model } } diff --git a/Templates/sourcery/Mock.swifttemplate b/Templates/sourcery/Mock.swifttemplate new file mode 100644 index 000000000..3db06369e --- /dev/null +++ b/Templates/sourcery/Mock.swifttemplate @@ -0,0 +1,2281 @@ +<%_ +let mockTypeName = "Mock" +func swiftLintRules(_ arguments: [String: Any]) -> [String] { + return stringArray(fromArguments: arguments, forKey: "excludedSwiftLintRules").map { rule in + return "//swiftlint:disable \(rule)" + } +} + +func projectImports(_ arguments: [String: Any]) -> [String] { + return imports(arguments) + testableImports(arguments) +} + +func imports(_ arguments: [String: Any]) -> [String] { + return stringArray(fromArguments: arguments, forKey: "import") + .map { return "import \($0)" } +} + +func testableImports(_ arguments: [String: Any]) -> [String] { + return stringArray(fromArguments: arguments, forKey: "testable") + .map { return "@testable import \($0)" } +} + +/// [Internal] Get value from dictionary +/// - Parameters: +/// - fromArguments: dictionary +/// - forKey: dictionary key +/// - Returns: array of strings, if key not found, returns empty array. +/// - Note: If sourcery arguments containts only one element, then single value is stored, otherwise array of elements. This method always gets array of elements. +func stringArray(fromArguments arguments: [String: Any], forKey key: String) -> [String] { + + if let argument = arguments[key] as? String { + return [argument] + } else if let manyArguments = arguments[key] as? [String] { + return manyArguments + } else { + return [] + } +} +_%> + +<%_ +struct Current { + var selfType: String = "Self" + var accessModifier: String = "open" + let type: Type +} +// Collision management +func areThereCollisions(between methods: [MethodWrapper]) -> Bool { + let givenSet = Set(methods.map({ $0.givenConstructorName(prefix: "") })) + guard givenSet.count == methods.count else { return true } // there would be conflicts in Given + let verifySet = Set(methods.map({ $0.verificationProxyConstructorName(prefix: "") })) + guard verifySet.count == methods.count else { return true } // there would be conflicts in Verify + return false +} + +// herlpers +func uniques(methods: [SourceryRuntime.Method]) -> [SourceryRuntime.Method] { + var filteredMethods: [SourceryRuntime.Method] = [] + for method in methods { + var isParentPrivate: Bool = false + if let typeModifiers = method.definedInTypeName?.modifiers { + isParentPrivate = typeModifiers.contains { + $0.name == "private" + || $0.name == "fileprivate" + } + } + if !isParentPrivate + && method.accessLevel != "private" + && method.accessLevel != "fileprivate" { + filteredMethods.append(method) + } + } + func returnTypeStripped(_ method: SourceryRuntime.Method) -> String { + let returnTypeRaw = "\(method.returnTypeName)" + var stripped: String = { + guard let range = returnTypeRaw.range(of: "where") else { return returnTypeRaw } + var stripped = returnTypeRaw + stripped.removeSubrange((range.lowerBound)...) + return stripped + }() + stripped = stripped.trimmingCharacters(in: CharacterSet(charactersIn: " ")) + return stripped + } + + func areSameParams(_ p1: SourceryRuntime.MethodParameter, _ p2: SourceryRuntime.MethodParameter) -> Bool { + guard p1.argumentLabel == p2.argumentLabel else { return false } + guard p1.name == p2.name else { return false } + guard p1.argumentLabel == p2.argumentLabel else { return false } + guard p1.typeName.name == p2.typeName.name else { return false } + guard p1.actualTypeName?.name == p2.actualTypeName?.name else { return false } + return true + } + + func areSameMethods(_ m1: SourceryRuntime.Method, _ m2: SourceryRuntime.Method) -> Bool { + guard m1.name != m2.name else { return m1.returnTypeName == m2.returnTypeName } + guard m1.selectorName == m2.selectorName else { return false } + guard m1.parameters.count == m2.parameters.count else { return false } + + let p1 = m1.parameters + let p2 = m2.parameters + + for i in 0.. [SourceryRuntime.Method] in + guard !result.contains(where: { areSameMethods($0,element) }) else { return result } + return result + [element] + }) +} + +func uniquesWithoutGenericConstraints(methods: [SourceryRuntime.Method]) -> [SourceryRuntime.Method] { + var filteredMethods: [SourceryRuntime.Method] = [] + for method in methods { + var isParentPrivate: Bool = false + if let typeModifiers = method.definedInTypeName?.modifiers { + isParentPrivate = typeModifiers.contains { + $0.name == "private" + || $0.name == "fileprivate" + } + } + if !isParentPrivate + && method.accessLevel != "private" + && method.accessLevel != "fileprivate" { + filteredMethods.append(method) + } + } + func returnTypeStripped(_ method: SourceryRuntime.Method) -> String { + let returnTypeRaw = "\(method.returnTypeName)" + var stripped: String = { + guard let range = returnTypeRaw.range(of: "where") else { return returnTypeRaw } + var stripped = returnTypeRaw + stripped.removeSubrange((range.lowerBound)...) + return stripped + }() + stripped = stripped.trimmingCharacters(in: CharacterSet(charactersIn: " ")) + return stripped + } + + func areSameParams(_ p1: SourceryRuntime.MethodParameter, _ p2: SourceryRuntime.MethodParameter) -> Bool { + guard p1.argumentLabel == p2.argumentLabel else { return false } + guard p1.name == p2.name else { return false } + guard p1.argumentLabel == p2.argumentLabel else { return false } + guard p1.typeName.name == p2.typeName.name else { return false } + guard p1.actualTypeName?.name == p2.actualTypeName?.name else { return false } + return true + } + + func areSameMethods(_ m1: SourceryRuntime.Method, _ m2: SourceryRuntime.Method) -> Bool { + guard m1.name != m2.name else { return returnTypeStripped(m1) == returnTypeStripped(m2) } + guard m1.selectorName == m2.selectorName else { return false } + guard m1.parameters.count == m2.parameters.count else { return false } + + let p1 = m1.parameters + let p2 = m2.parameters + + for i in 0.. [SourceryRuntime.Method] in + guard !result.contains(where: { areSameMethods($0,element) }) else { return result } + return result + [element] + }) +} + +func uniques(variables: [SourceryRuntime.Variable]) -> [SourceryRuntime.Variable] { + return variables.reduce([], { (result, element) -> [SourceryRuntime.Variable] in + guard !result.contains(where: { $0.name == element.name }) else { return result } + return result + [element] + }) +} + +func wrapMethod(_ method: SourceryRuntime.Method, current: Current, methodRegistrar: MethodRegistrar) -> MethodWrapper { + return MethodWrapper(method, current: current, methodRegistrar: methodRegistrar) +} + +func wrapSubscript(_ wrapped: SourceryRuntime.Subscript, current: Current, subscriptRegistrar: SubscriptRegistrar) -> SubscriptWrapper { + return SubscriptWrapper(wrapped, current: current, subscriptRegistrar: subscriptRegistrar) +} + +func justWrap(_ variable: SourceryRuntime.Variable, current: Current) -> VariableWrapper { return wrapProperty(variable, current: current) } +func wrapProperty(_ variable: SourceryRuntime.Variable, _ scope: String = "", current: Current) -> VariableWrapper { + return VariableWrapper(variable, scope: scope, current: current) +} + +func stubProperty(_ variable: SourceryRuntime.Variable, _ scope: String, current: Current) -> String { + let wrapper = VariableWrapper(variable, scope: scope, current: current) + return "\(wrapper.prototype)\n\t\(wrapper.privatePrototype)" +} + +func propertyTypes(_ variable: SourceryRuntime.Variable, current: Current) -> String { + let wrapper = VariableWrapper(variable, scope: "scope", current: current) + return "\(wrapper.propertyGet())" + (wrapper.readonly ? "" : "\n\t\t\(wrapper.propertySet())") +} + +func propertyMethodTypes(_ variable: SourceryRuntime.Variable, current: Current) -> String { + let wrapper = VariableWrapper(variable, scope: "", current: current) + return "\(wrapper.propertyCaseGet())" + (wrapper.readonly ? "" : "\n\t\t\(wrapper.propertyCaseSet())") +} + +func propertyMethodTypesIntValue(_ variable: SourceryRuntime.Variable, current: Current) -> String { + let wrapper = VariableWrapper(variable, scope: "", current: current) + return "\(wrapper.propertyCaseGetIntValue())" + (wrapper.readonly ? "" : "\n\t\t\t\(wrapper.propertyCaseSetIntValue())") +} + +func propertyRegister(_ variable: SourceryRuntime.Variable, methodRegistrar: MethodRegistrar, current: Current) { + let wrapper = VariableWrapper(variable, scope: "", current: current) + methodRegistrar.register(wrapper.propertyCaseGetName,wrapper.propertyCaseGetName,wrapper.propertyCaseGetName) + guard !wrapper.readonly else { return } + methodRegistrar.register(wrapper.propertyCaseSetName,wrapper.propertyCaseSetName,wrapper.propertyCaseGetName) +} + +class Helpers { + static func split(_ string: String, byFirstOccurenceOf word: String) -> (String, String) { + guard let wordRange = string.range(of: word) else { return (string, "") } + let selfRange = string.range(of: string)! + let before = String(string[selfRange.lowerBound.. [String]? { + if let types = annotated.annotations["associatedtype"] as? [String] { + return types.reversed() + } else if let type = annotated.annotations["associatedtype"] as? String { + return [type] + } else { + return nil + } + } + static func extractWhereClause(from annotated: SourceryRuntime.Annotated) -> String? { + if let constraints = annotated.annotations["where"] as? [String] { + return " where \(constraints.reversed().joined(separator: ", "))" + } else if let constraint = annotated.annotations["where"] as? String { + return " where \(constraint)" + } else { + return nil + } + } + /// Extract all typealiases from "annotations" + static func extractTypealiases(from annotated: SourceryRuntime.Annotated) -> [String] { + if let types = annotated.annotations["typealias"] as? [String] { + return types.reversed() + } else if let type = annotated.annotations["typealias"] as? String { + return [type] + } else { + return [] + } + } + static func extractGenericsList(_ associatedTypes: [String]?) -> [String] { + return associatedTypes?.compactMap { + split($0, byFirstOccurenceOf: " where ").0.replacingOccurrences(of: " ", with: "").split(separator: ":").map(String.init).first + }.map { "\($0)" } ?? [] + } + static func extractGenericTypesModifier(_ associatedTypes: [String]?) -> String { + let all = extractGenericsList(associatedTypes) + guard !all.isEmpty else { return "" } + return "<\(all.joined(separator: ","))>" + } + static func extractGenericTypesConstraints(_ associatedTypes: [String]?) -> String { + guard let all = associatedTypes else { return "" } + let constraints = all.compactMap { t -> String? in + let splitted = split(t, byFirstOccurenceOf: " where ") + let constraint = splitted.0.replacingOccurrences(of: " ", with: "").split(separator: ":").map(String.init) + guard constraint.count == 2 else { return nil } + let adopts = constraint[1].split(separator: ",").map(String.init) + var mapped = adopts.map { "\(constraint[0]): \($0)" } + if !splitted.1.isEmpty { + mapped.append(splitted.1) + } + return mapped.joined(separator: ", ") + } + .joined(separator: ", ") + guard !constraints.isEmpty else { return "" } + return " where \(constraints)" + } + static func extractAttributes( + from attributes: [String: [SourceryRuntime.Attribute]], + filterOutStartingWith disallowedPrefixes: [String] = [] + ) -> String { + return attributes + .reduce([SourceryRuntime.Attribute]()) { $0 + $1.1 } + .map { $0.description } + .filter { !["private", "internal", "public", "open", "optional"].contains($0) } + .filter { element in + !disallowedPrefixes.contains(where: element.hasPrefix) + } + .sorted() + .joined(separator: " ") + } +} + +class ParameterWrapper { + let parameter: MethodParameter + + var isVariadic = false + let current: Current + + var wrappedForCall: String { + let typeString = "\(type.actualTypeName ?? type)" + let isEscaping = typeString.contains("@escaping") + let isOptional = (type.actualTypeName ?? type).isOptional + if parameter.isClosure && !isEscaping && !isOptional { + return "\(nestedType).any" + } else { + return "\(nestedType).value(\(escapedName))" + } + } + var nestedType: String { + return "\(TypeWrapper(type, isVariadic, current: current).nestedParameter)" + } + var justType: String { + return "\(TypeWrapper(type, isVariadic, current: current).replacingSelf())" + } + var justPerformType: String { + return "\(TypeWrapper(type, isVariadic, current: current).replacingSelfRespectingVariadic())".replacingOccurrences(of: "!", with: "?") + } + var genericType: String { + return isVariadic ? "Parameter<[GenericAttribute]>" : "Parameter" + } + var typeErasedType: String { + return isVariadic ? "Parameter<[TypeErasedAttribute]>" : "Parameter" + } + var type: SourceryRuntime.TypeName { + return parameter.typeName + } + var name: String { + return parameter.name + } + var escapedName: String { + return "`\(parameter.name)`" + } + var comparator: String { + return "guard Parameter.compare(lhs: lhs\(parameter.name.capitalized), rhs: rhs\(parameter.name.capitalized), with: matcher) else { return false }" + } + func comparatorResult() -> String { + let lhsName = "lhs\(parameter.name.capitalized)" + let rhsName = "rhs\(parameter.name.capitalized)" + return "results.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: \(lhsName), rhs: \(rhsName), with: matcher), \(lhsName), \(rhsName), \"\(labelAndName())\"))" + } + + init(_ parameter: SourceryRuntime.MethodParameter, _ variadics: [String] = [], current: Current) { + self.parameter = parameter + self.isVariadic = !variadics.isEmpty && variadics.contains(parameter.name) + self.current = current + } + + func isGeneric(_ types: [String]) -> Bool { + return TypeWrapper(type, current: current).isGeneric(types) + } + + func wrappedForProxy(_ generics: [String], _ availability: Bool = false) -> String { + if isGeneric(generics) { + return "\(escapedName).wrapAsGeneric()" + } + if (availability) { + return "\(escapedName).typeErasedAttribute()" + } + return "\(escapedName)" + } + func wrappedForCalls(_ generics: [String], _ availability: Bool = false) -> String { + if isGeneric(generics) { + return "\(wrappedForCall).wrapAsGeneric()" + } + if (availability) { + return "\(wrappedForCall).typeErasedAttribute()" + } + return "\(wrappedForCall)" + } + + func asMethodArgument() -> String { + if parameter.argumentLabel != parameter.name { + return "\(parameter.argumentLabel ?? "_") \(parameter.name): \(parameter.typeName)" + } else { + return "\(parameter.name): \(parameter.typeName)" + } + } + func labelAndName() -> String { + let label = parameter.argumentLabel ?? "_" + return label != parameter.name ? "\(label) \(parameter.name)" : label + } + func sanitizedForEnumCaseName() -> String { + if let label = parameter.argumentLabel, label != parameter.name { + return "\(label)_\(parameter.name)".replacingOccurrences(of: "`", with: "") + } else { + return "\(parameter.name)".replacingOccurrences(of: "`", with: "") + } + } +} + +class TypeWrapper { + let type: SourceryRuntime.TypeName + let isVariadic: Bool + let current: Current + + var vPref: String { return isVariadic ? "[" : "" } + var vSuff: String { return isVariadic ? "]" : "" } + + var unwrapped: String { + return type.unwrappedTypeName + } + var unwrappedReplacingSelf: String { + return replacingSelf(unwrap: true) + } + var stripped: String { + if type.isImplicitlyUnwrappedOptional { + return "\(vPref)\(unwrappedReplacingSelf)?\(vSuff)" + } else if type.isOptional { + return "\(vPref)\(unwrappedReplacingSelf)?\(vSuff)" + } else { + return "\(vPref)\(unwrappedReplacingSelf)\(vSuff)" + } + } + var nestedParameter: String { + if type.isImplicitlyUnwrappedOptional { + return "Parameter<\(vPref)\(unwrappedReplacingSelf)?\(vSuff)>" + } else if type.isOptional { + return "Parameter<\(vPref)\(unwrappedReplacingSelf)?\(vSuff)>" + } else { + return "Parameter<\(vPref)\(unwrappedReplacingSelf)\(vSuff)>" + } + } + var isSelfType: Bool { + return unwrapped == "Self" + } + func isSelfTypeRecursive() -> Bool { + if let tuple = type.tuple { + for element in tuple.elements { + guard !TypeWrapper(element.typeName, current: current).isSelfTypeRecursive() else { return true } + } + } else if let array = type.array { + return TypeWrapper(array.elementTypeName, current: current).isSelfTypeRecursive() + } else if let dictionary = type.dictionary { + guard !TypeWrapper(dictionary.valueTypeName, current: current).isSelfTypeRecursive() else { return true } + guard !TypeWrapper(dictionary.keyTypeName, current: current).isSelfTypeRecursive() else { return true } + } else if let closure = type.closure { + guard !TypeWrapper(closure.actualReturnTypeName, current: current).isSelfTypeRecursive() else { return true } + for parameter in closure.parameters { + guard !TypeWrapper(parameter.typeName, current: current).isSelfTypeRecursive() else { return true } + } + } + + return isSelfType + } + + init(_ type: SourceryRuntime.TypeName, _ isVariadic: Bool = false, current: Current) { + self.type = type + self.isVariadic = isVariadic + self.current = current + } + + func isGeneric(_ types: [String]) -> Bool { + guard !type.isVoid else { return false } + + return isGeneric(name: unwrapped, generics: types) + } + + private func isGeneric(name: String, generics: [String]) -> Bool { + let name = "(\(name.replacingOccurrences(of: " ", with: "")))" + let modifiers = "[\\?\\!]*" + return generics.contains(where: { generic in + let wrapped = "([\\(]\(generic)\(modifiers)[\\)\\.])" + let constraint = "([<,]\(generic)\(modifiers)[>,\\.])" + let arrays = "([\\[:]\(generic)\(modifiers)[\\],\\.:])" + let tuples = "([\\(,]\(generic)\(modifiers)[,\\.\\)])" + let closures = "((\\-\\>)\(generic)\(modifiers)[,\\.\\)])" + let pattern = "\(wrapped)|\(constraint)|\(arrays)|\(tuples)|\(closures)" + guard let regex = try? NSRegularExpression(pattern: pattern) else { return false } + return regex.firstMatch(in: name, options: [], range: NSRange(location: 0, length: (name as NSString).length)) != nil + }) + } + + func replacingSelf(unwrap: Bool = false) -> String { + guard isSelfTypeRecursive() else { + return unwrap ? self.unwrapped : "\(type)" + } + + if isSelfType { + let optionality: String = { + if type.isImplicitlyUnwrappedOptional { + return "!" + } else if type.isOptional { + return "?" + } else { + return "" + } + }() + return unwrap ? current.selfType : current.selfType + optionality + } else if let tuple = type.tuple { + let inner = tuple.elements.map({ TypeWrapper($0.typeName, current: current).replacingSelf() }).joined(separator: ",") + let value = "(\(inner))" + return value + } else if let array = type.array { + let value = "[\(TypeWrapper(array.elementTypeName, current: current).replacingSelf())]" + return value + } else if let dictionary = type.dictionary { + let value = "[" + + "\(TypeWrapper(dictionary.valueTypeName, current: current).replacingSelf())" + + ":" + + "\(TypeWrapper(dictionary.keyTypeName, current: current).replacingSelf())" + + "]" + return value + } else if let closure = type.closure { + let returnType = TypeWrapper(closure.actualReturnTypeName, current: current).replacingSelf() + let inner = closure.parameters + .map { TypeWrapper($0.typeName, current: current).replacingSelf() } + .joined(separator: ",") + let throwing = closure.throws ? "throws " : "" + let value = "(\(inner)) \(throwing)-> \(returnType)" + return value + } else { + return (unwrap ? self.unwrapped : "\(type)") + } + } + + func replacingSelfRespectingVariadic() -> String { + return "\(vPref)\(replacingSelf())\(vSuff)" + } +} + +func replacingSelf(_ value: String, current: Current) -> String { + return value + // TODO: proper regex here + // default < case > + .replacingOccurrences(of: "", with: "<\(current.selfType)>") + .replacingOccurrences(of: "", with: " \(current.selfType)>") + .replacingOccurrences(of: ",Self>", with: ",\(current.selfType)>") + // (Self) -> Case + .replacingOccurrences(of: "(Self)", with: "(\(current.selfType))") + .replacingOccurrences(of: "(Self ", with: "(\(current.selfType) ") + .replacingOccurrences(of: "(Self.", with: "(\(current.selfType).") + .replacingOccurrences(of: "(Self,", with: "(\(current.selfType),") + .replacingOccurrences(of: "(Self?", with: "(\(current.selfType)?") + .replacingOccurrences(of: " Self)", with: " \(current.selfType))") + .replacingOccurrences(of: ",Self)", with: ",\(current.selfType))") + // literals + .replacingOccurrences(of: "[Self]", with: "[\(current.selfType)]") + // right + .replacingOccurrences(of: "[Self ", with: "[\(current.selfType) ") + .replacingOccurrences(of: "[Self.", with: "[\(current.selfType).") + .replacingOccurrences(of: "[Self,", with: "[\(current.selfType),") + .replacingOccurrences(of: "[Self:", with: "[\(current.selfType):") + .replacingOccurrences(of: "[Self?", with: "[\(current.selfType)?") + // left + .replacingOccurrences(of: " Self]", with: " \(current.selfType)]") + .replacingOccurrences(of: ",Self]", with: ",\(current.selfType)]") + .replacingOccurrences(of: ":Self]", with: ":\(current.selfType)]") + // unknown + .replacingOccurrences(of: " Self ", with: " \(current.selfType) ") + .replacingOccurrences(of: " Self.", with: " \(current.selfType).") + .replacingOccurrences(of: " Self,", with: " \(current.selfType),") + .replacingOccurrences(of: " Self:", with: " \(current.selfType):") + .replacingOccurrences(of: " Self?", with: " \(current.selfType)?") + .replacingOccurrences(of: ",Self ", with: ",\(current.selfType) ") + .replacingOccurrences(of: ",Self,", with: ",\(current.selfType),") + .replacingOccurrences(of: ",Self?", with: ",\(current.selfType)?") +} + +class MethodRegistrar { + var registered: [String: Int] = [:] + var suffixes: [String: Int] = [:] + var suffixesWithoutReturnType: [String: Int] = [:] + + func register(_ name: String, _ uniqueName: String, _ uniqueNameWithReturnType: String) { + if let count = registered[name] { + registered[name] = count + 1 + suffixes[uniqueNameWithReturnType] = count + 1 + } else { + registered[name] = 1 + suffixes[uniqueNameWithReturnType] = 1 + } + + if let count = suffixesWithoutReturnType[uniqueName] { + suffixesWithoutReturnType[uniqueName] = count + 1 + } else { + suffixesWithoutReturnType[uniqueName] = 1 + } + } + + func returnTypeMatters(uniqueName: String) -> Bool { + let count = suffixesWithoutReturnType[uniqueName] ?? 0 + return count > 1 + } +} + +class MethodWrapper { + private var noStubDefinedMessage: String { + let methodName = method.name.condenseWhitespace() + .replacingOccurrences(of: "( ", with: "(") + .replacingOccurrences(of: " )", with: ")") + return "Stub return value not specified for \(methodName). Use given" + } + + let method: SourceryRuntime.Method + var accessModifier: String { + guard !method.isStatic else { return "public static" } + guard !returnsGenericConstrainedToSelf else { return "public" } + guard !parametersContainsSelf else { return "public" } + return current.accessModifier + } + var hasAvailability: Bool { method.attributes["available"]?.isEmpty == false } + var isAsync: Bool { + self.method.annotations["async"] != nil + } + + private var registrationName: String { + var rawName = (method.isStatic ? "sm*\(method.selectorName)" : "m*\(method.selectorName)") + .replacingOccurrences(of: "_", with: "") + .replacingOccurrences(of: "(", with: "__") + .replacingOccurrences(of: ")", with: "") + + var parametersNames = method.parameters.map { "\($0.name)" } + + while let range = rawName.range(of: ":"), let name = parametersNames.first { + parametersNames.removeFirst() + rawName.replaceSubrange(range, with: "_\(name)") + } + + let trimSet = CharacterSet(charactersIn: "_") + + return rawName + .replacingOccurrences(of: ":", with: "") + .replacingOccurrences(of: "m*", with: "m_") + .replacingOccurrences(of: "___", with: "__").trimmingCharacters(in: trimSet) + } + private var uniqueName: String { + var rawName = (method.isStatic ? "sm_\(method.selectorName)" : "m_\(method.selectorName)") + var parametersNames = method.parameters.map { "\($0.name)_of_\($0.typeName.name)" } + + while let range = rawName.range(of: ":"), let name = parametersNames.first { + parametersNames.removeFirst() + rawName.replaceSubrange(range, with: "_\(name)") + } + + return rawName.trimmingCharacters(in: CharacterSet(charactersIn: "_")) + } + private var uniqueNameWithReturnType: String { + let returnTypeRaw = "\(method.returnTypeName)" + var returnTypeStripped: String = { + guard let range = returnTypeRaw.range(of: "where") else { return returnTypeRaw } + var stripped = returnTypeRaw + stripped.removeSubrange((range.lowerBound)...) + return stripped + }() + returnTypeStripped = returnTypeStripped.trimmingCharacters(in: CharacterSet(charactersIn: " ")) + return "\(uniqueName)->\(returnTypeStripped)" + } + private var nameSuffix: String { + guard let count = methodRegistrar.registered[registrationName] else { return "" } + guard count > 1 else { return "" } + guard let index = methodRegistrar.suffixes[uniqueNameWithReturnType] else { return "" } + return "_\(index)" + } + private var methodAttributes: String { + return Helpers.extractAttributes(from: self.method.attributes, filterOutStartingWith: ["mutating", "@inlinable"]) + } + private var methodAttributesNonObjc: String { + return Helpers.extractAttributes(from: self.method.attributes, filterOutStartingWith: ["mutating", "@inlinable", "@objc"]) + } + + var prototype: String { + return "\(registrationName)\(nameSuffix)".replacingOccurrences(of: "`", with: "") + } + var parameters: [ParameterWrapper] { + return filteredParameters.map { ParameterWrapper($0, self.getVariadicParametersNames(), current: current) } + } + var filteredParameters: [MethodParameter] { + return method.parameters.filter { $0.name != "" } + } + func functionPrototype(_ forceAsync: Bool = false) -> String { + let throwing: String = { + if method.throws { + return "throws " + } else if method.rethrows { + return "rethrows " + } else { + return "" + } + }() + + let staticModifier: String = "\(accessModifier) " + let params = replacingSelf(parametersForStubSignature(), current: current) + var attributes = self.methodAttributes + attributes = attributes.isEmpty ? "" : "\(attributes)\n\t" + var asyncModifier = (self.isAsync || forceAsync) ? "async " : "" + + if method.isInitializer { + return "\(attributes)public required \(method.name) \(asyncModifier)\(throwing)" + } else if method.returnTypeName.isVoid { + let wherePartIfNeeded: String = { + if method.returnTypeName.name.hasPrefix("Void") { + let range = method.returnTypeName.name.range(of: "Void")! + return "\(method.returnTypeName.name[range.upperBound...])" + } else { + return !method.returnTypeName.name.isEmpty ? "\(method.returnTypeName.name) " : "" + } + }() + return "\(attributes)\(staticModifier)func \(method.shortName)\(params) \(asyncModifier)\(throwing)\(wherePartIfNeeded)" + } else if returnsGenericConstrainedToSelf { + return "\(attributes)\(staticModifier)func \(method.shortName)\(params) \(asyncModifier)\(throwing)-> \(returnTypeReplacingSelf) " + } else { + return "\(attributes)\(staticModifier)func \(method.shortName)\(params) \(asyncModifier)\(throwing)-> \(method.returnTypeName.name) " + } + } + var invocation: String { + guard !method.isInitializer else { return "" } + if filteredParameters.isEmpty { + return (isMainActorInActor ? "await " : "") + "addInvocation(.\(prototype))" + } else { + return (isMainActorInActor ? "await " : "") + "addInvocation(.\(prototype)(\(parametersForMethodCall())))" + } + } + var givenDeclaration: String { + let returnType: String = returnsSelf ? "__Self__" : "\(TypeWrapper(method.returnTypeName, current: current).stripped)" + let defaultValue = method.returnTypeName.isOptional ? " = nil" : "" + return method.returnTypeName.isVoid + ? "" + : "var __value: \(returnType)\(defaultValue)\n" + } + var givenValue: String { + guard !method.isInitializer else { return "" } + guard method.throws || !method.returnTypeName.isVoid else { return "" } + + let methodType = filteredParameters.isEmpty ? ".\(prototype)" : ".\(prototype)(\(parametersForMethodCall()))" + + if method.returnTypeName.isVoid { + return """ + \n\t\tdo { + \t\t _ = try \(isMainActorInActor ? "await" : "") methodReturnValue(\(methodType)).casted() as Void + \t\t}\(" ") + """ + } else { + return """ + \n\t\tdo { + \t\t __value = try \(isMainActorInActor ? "await" : "") methodReturnValue(\(methodType)).casted() + \t\t}\(" ") + """ + } + } + var throwValue: String { + guard !method.isInitializer else { return "" } + guard method.throws || !method.returnTypeName.isVoid else { return "" } + let safeFailure = method.isStatic ? "" : "\t\t\t\(isMainActorInActor ? "await " : "")onFatalFailure(\"\(noStubDefinedMessage)\")\n" + // For Void and Returning optionals - we allow not stubbed case to happen, as we are still able to return + let noStubHandling = method.returnTypeName.isVoid || method.returnTypeName.isOptional ? "\t\t\t// do nothing" : "\(safeFailure)\t\t\tFailure(\"\(noStubDefinedMessage)\")" + guard method.throws else { + return """ + catch { + \(noStubHandling) + \t\t} + """ + } + + return """ + catch MockError.notStubed { + \(noStubHandling) + \t\t} catch { + \t\t throw error + \t\t} + """ + } + var returnValue: String { + guard !method.isInitializer else { return "" } + guard !method.returnTypeName.isVoid else { return "" } + + return "\n\t\treturn __value" + } + var equalCase: String { + guard !method.isInitializer else { return "" } + + if filteredParameters.isEmpty { + return "case (.\(prototype), .\(prototype)):" + } else { + let lhsParams = filteredParameters.map { "let lhs\($0.name.capitalized)" }.joined(separator: ", ") + let rhsParams = filteredParameters.map { "let rhs\($0.name.capitalized)" }.joined(separator: ", ") + return "case (.\(prototype)(\(lhsParams)), .\(prototype)(\(rhsParams))):" + } + } + func equalCases() -> String { + var results = self.equalCase + + guard !parameters.isEmpty else { + results += " return .match" + return results + } + + results += "\n\t\t\t\tvar results: [Matcher.ParameterComparisonResult] = []\n" + results += parameters.map { "\t\t\t\t\($0.comparatorResult())" }.joined(separator: "\n") + results += "\n\t\t\t\treturn Matcher.ComparisonResult(results)" + return results + } + var intValueCase: String { + if filteredParameters.isEmpty { + return "case .\(prototype): return 0" + } else { + let params = filteredParameters.enumerated().map { offset, _ in + return "p\(offset)" + } + let definitions = params.joined(separator: ", ") + let paramsSum = params.map({ "\($0).intValue" }).joined(separator: " + ") + return "case let .\(prototype)(\(definitions)): return \(paramsSum)" + } + } + var assertionName: String { + return "case .\(prototype): return \".\(method.selectorName)\(method.parameters.isEmpty ? "()" : "")\"" + } + + var returnsSelf: Bool { + guard !returnsGenericConstrainedToSelf else { return true } + return !method.returnTypeName.isVoid && TypeWrapper(method.returnTypeName, current: current).isSelfType + } + var returnsGenericConstrainedToSelf: Bool { + let defaultReturnType = "\(method.returnTypeName.name) " + return defaultReturnType != returnTypeReplacingSelf + } + var returnTypeReplacingSelf: String { + return replacingSelf("\(method.returnTypeName.name) ", current: current) + } + var parametersContainsSelf: Bool { + return replacingSelf(parametersForStubSignature(), current: current) != parametersForStubSignature() + } + var isMainActorInActor: Bool { + current.type.inheritedTypes.contains("Actor") + && method.attributes["MainActor"] != nil + } + + let current: Current + let methodRegistrar: MethodRegistrar + + var replaceSelf: String { + return current.selfType + } + + init(_ method: SourceryRuntime.Method, current: Current, methodRegistrar: MethodRegistrar) { + self.method = method + self.current = current + self.methodRegistrar = methodRegistrar + } + + func register() { + methodRegistrar.register(registrationName,uniqueName,uniqueNameWithReturnType) + } + + func wrappedInMethodType() -> Bool { + return !method.isInitializer + } + + func returningParameter(_ multiple: Bool, _ front: Bool) -> String { + guard methodRegistrar.returnTypeMatters(uniqueName: uniqueName) else { return "" } + let returning: String = "returning: \(returnTypeStripped(method, type: true))" + guard multiple else { return returning } + + return front ? ", \(returning)" : "\(returning), " + } + + func taskPrefix(_ forceAsync: Bool = false) -> String { + isMainActorInActor && !forceAsync ? "Task {\n\t\t" : "\t\t" + } + + func taskSuffix(_ forceAsync: Bool = false) -> String { + isMainActorInActor && !forceAsync ? "\n}" : "" + } + + // Stub + func stubBody(_ forceAsync: Bool = false) -> String { + let body: String = { + if method.isInitializer || !returnsSelf { + return + givenDeclaration + + taskPrefix(forceAsync) + + invocation + + performCall() + + givenValue + + throwValue + + taskSuffix(forceAsync) + + returnValue + } else { + return wrappedStubPrefix() + + "\t\t" + invocation + + performCall() + + givenValue + + throwValue + + returnValue + + wrappedStubPostfix() + } + }() + return replacingSelf(body, current: current) + } + + func wrappedStubPrefix() -> String { + guard !method.isInitializer, returnsSelf else { + return "" + } + + let throwing: String = { + if method.throws { + return "throws " + } else if method.rethrows { + return "rethrows " + } else { + return "" + } + }() + + return "func _wrapped<__Self__>() \(throwing)-> __Self__ {\n" + } + + func wrappedStubPostfix() -> String { + guard !method.isInitializer, returnsSelf else { + return "" + } + + let throwing: String = (method.throws || method.rethrows) ? "try ": "" + + return "\n\t\t}" + + "\n\t\treturn \(throwing)_wrapped()" + } + + // Method Type + func methodTypeDeclarationWithParameters() -> String { + if filteredParameters.isEmpty { + return "case \(prototype)" + } else { + return "case \(prototype)(\(parametersForMethodTypeDeclaration(availability: hasAvailability)))" + } + } + + // Given + func containsEmptyArgumentLabels() -> Bool { + return parameters.contains(where: { $0.parameter.argumentLabel == nil }) + } + + func givenReturnTypeString() -> String { + let returnTypeString: String = { + guard !returnsGenericConstrainedToSelf else { return returnTypeReplacingSelf } + guard !returnsSelf else { return replaceSelf } + return TypeWrapper(method.returnTypeName, current: current).stripped + }() + return returnTypeString + } + + func givenConstructorName(prefix: String = "") -> String { + let returnTypeString = givenReturnTypeString() + let (annotation, _, _) = methodInfo() + let clauseConstraints = whereClauseExpression() + + if filteredParameters.isEmpty { + return "\(annotation)public static func \(method.shortName)(willReturn: \(returnTypeString)...) -> \(prefix)MethodStub" + clauseConstraints + } else { + return "\(annotation)public static func \(method.shortName)(\(parametersForProxySignature()), willReturn: \(returnTypeString)...) -> \(prefix)MethodStub" + clauseConstraints + } + } + + func givenConstructorNameThrows(prefix: String = "") -> String { + let (annotation, _, _) = methodInfo() + let clauseConstraints = whereClauseExpression() + + let genericsArray = getGenericsConstraints(getGenericsAmongParameters(), filterSingle: false) + let generics = genericsArray.isEmpty ? "" : "<\(genericsArray.joined(separator: ", "))>" + + if filteredParameters.isEmpty { + return "\(annotation)public static func \(method.callName)\(generics)(willThrow: Error...) -> \(prefix)MethodStub" + clauseConstraints + } else { + return "\(annotation)public static func \(method.callName)\(generics)(\(parametersForProxySignature()), willThrow: Error...) -> \(prefix)MethodStub" + clauseConstraints + } + } + + func givenConstructor(prefix: String = "") -> String { + if filteredParameters.isEmpty { + return "return \(prefix)Given(method: .\(prototype), products: willReturn.map({ StubProduct.return($0 as Any) }))" + } else { + return "return \(prefix)Given(method: .\(prototype)(\(parametersForProxyInit())), products: willReturn.map({ StubProduct.return($0 as Any) }))" + } + } + + func givenConstructorThrows(prefix: String = "") -> String { + if filteredParameters.isEmpty { + return "return \(prefix)Given(method: .\(prototype), products: willThrow.map({ StubProduct.throw($0) }))" + } else { + return "return \(prefix)Given(method: .\(prototype)(\(parametersForProxyInit())), products: willThrow.map({ StubProduct.throw($0) }))" + } + } + + // Given willProduce + func givenProduceConstructorName(prefix: String = "") -> String { + let returnTypeString = givenReturnTypeString() + let (annotation, _, _) = methodInfo() + let produceClosure = "(Stubber<\(returnTypeString)>) -> Void" + let clauseConstraints = whereClauseExpression() + + if filteredParameters.isEmpty { + return "\(annotation)public static func \(method.shortName)(willProduce: \(produceClosure)) -> \(prefix)MethodStub" + clauseConstraints + } else { + return "\(annotation)public static func \(method.shortName)(\(parametersForProxySignature()), willProduce: \(produceClosure)) -> \(prefix)MethodStub" + clauseConstraints + } + } + + func givenProduceConstructorNameThrows(prefix: String = "") -> String { + let returnTypeString = givenReturnTypeString() + let (annotation, _, _) = methodInfo() + let produceClosure = "(StubberThrows<\(returnTypeString)>) -> Void" + let clauseConstraints = whereClauseExpression() + + if filteredParameters.isEmpty { + return "\(annotation)public static func \(method.shortName)(willProduce: \(produceClosure)) -> \(prefix)MethodStub" + clauseConstraints + } else { + return "\(annotation)public static func \(method.shortName)(\(parametersForProxySignature()), willProduce: \(produceClosure)) -> \(prefix)MethodStub" + clauseConstraints + } + } + + func givenProduceConstructor(prefix: String = "") -> String { + let returnTypeString = givenReturnTypeString() + return """ + let willReturn: [\(returnTypeString)] = [] + \t\t\tlet given: \(prefix)Given = { \(givenConstructor(prefix: prefix)) }() + \t\t\tlet stubber = given.stub(for: (\(returnTypeString)).self) + \t\t\twillProduce(stubber) + \t\t\treturn given + """ + } + + func givenProduceConstructorThrows(prefix: String = "") -> String { + let returnTypeString = givenReturnTypeString() + return """ + let willThrow: [Error] = [] + \t\t\tlet given: \(prefix)Given = { \(givenConstructorThrows(prefix: prefix)) }() + \t\t\tlet stubber = given.stubThrows(for: (\(returnTypeString)).self) + \t\t\twillProduce(stubber) + \t\t\treturn given + """ + } + + // Verify + func verificationProxyConstructorName(prefix: String = "") -> String { + let (annotation, methodName, genericConstrains) = methodInfo() + + if filteredParameters.isEmpty { + return "\(annotation)public static func \(methodName)(\(returningParameter(false,true))) -> \(prefix)Verify\(genericConstrains)" + } else { + return "\(annotation)public static func \(methodName)(\(parametersForProxySignature())\(returningParameter(true,true))) -> \(prefix)Verify\(genericConstrains)" + } + } + + func verificationProxyConstructor(prefix: String = "") -> String { + if filteredParameters.isEmpty { + return "return \(prefix)Verify(method: .\(prototype))" + } else { + return "return \(prefix)Verify(method: .\(prototype)(\(parametersForProxyInit())))" + } + } + + var isAnyParameterAsync: Bool { + var isAsync: Bool = false + for parameter in filteredParameters { + let typeName = parameter.typeName + if let closure = typeName.closure { + if closure.isAsync { + isAsync = true + } + } + } + return isAsync + } + + var isAnyParameterThrowing: Bool { + var isThrowing: Bool = false + for parameter in filteredParameters { + let typeName = parameter.typeName + if let closure = typeName.closure { + if closure.throws { + isThrowing = true + break + } + } + } + return isThrowing + } + + // Perform + func performProxyConstructorName(prefix: String = "") -> String { + let body: String = { + let (annotation, methodName, genericConstrains) = methodInfo() + + if filteredParameters.isEmpty { + return "\(annotation)public static func \(methodName)(\(returningParameter(true,false))perform: @escaping \(performProxyClosureType())) -> \(prefix)Perform\(genericConstrains)" + } else { + return "\(annotation)public static func \(methodName)(\(parametersForProxySignature()), \(returningParameter(true,false))perform: @escaping \(performProxyClosureType())) -> \(prefix)Perform\(genericConstrains)" + } + }() + return replacingSelf(body, current: current) + } + + func performProxyConstructor(prefix: String = "") -> String { + if filteredParameters.isEmpty { + return "return \(prefix)Perform(method: .\(prototype), performs: perform)" + } else { + return "return \(prefix)Perform(method: .\(prototype)(\(parametersForProxyInit())), performs: perform)" + } + } + + func performProxyClosureType() -> String { + let isAsync = isAnyParameterAsync + let isThrowing = isAnyParameterThrowing + if filteredParameters.isEmpty { + return "()\(isAsync ? " async" : "")\(isThrowing ? " throws" : "") -> Void" + } else { + let parameters = self.parameters + .map { "\($0.justPerformType)" } + .joined(separator: ", ") + return "(\(parameters))\(isAsync ? " async" : "")\(isThrowing ? " throws" : "") -> Void" + } + } + + func performProxyClosureCall() -> String { + let isAsync = isAnyParameterAsync + let isThrowing = isAnyParameterThrowing + let performCall: String + if filteredParameters.isEmpty { + performCall = "perform?()" + } else { + let parameters = filteredParameters + .map { p in + let wrapped = ParameterWrapper(p, self.getVariadicParametersNames(), current: current) + let isAutolosure = wrapped.justType.hasPrefix("@autoclosure") + return "\(p.inout ? "&" : "")`\(p.name)`\(isAutolosure ? "()" : "")" + } + .joined(separator: ", ") + performCall = "perform?(\(parameters))" + } + return isAsync ? + """ + let semaphore = DispatchSemaphore(value: .zero) + Task { + \(isThrowing ? "try " : "")await \(performCall) + semaphore.signal() + } + semaphore.wait() + """ + : performCall + } + + func performCall() -> String { + guard !method.isInitializer else { return "" } + let type = performProxyClosureType() + var proxy = filteredParameters.isEmpty ? "\(prototype)" : "\(prototype)(\(parametersForMethodCall()))" + let cast = "let perform = \(isMainActorInActor ? "await" : "") methodPerformValue(.\(proxy)) as? \(type)" + let call = performProxyClosureCall() + + return "\n\t\t\(cast)\n\t\t\(call)" + } + + // Helpers + private func parametersForMethodCall() -> String { + let generics = getGenericsWithoutConstraints() + return parameters.map { $0.wrappedForCalls(generics, hasAvailability) }.joined(separator: ", ") + } + + private func parametersForMethodTypeDeclaration(availability: Bool) -> String { + let generics = getGenericsWithoutConstraints() + return parameters.map { param in + if param.isGeneric(generics) { return param.genericType } + if availability { return param.typeErasedType } + return replacingSelf(param.nestedType, current: current) + }.joined(separator: ", ") + } + + private func parametersForProxySignature() -> String { + return parameters.map { p in + return "\(p.labelAndName()): \(replacingSelf(p.nestedType, current: current))" + }.joined(separator: ", ") + } + + private func parametersForStubSignature() -> String { + func replacing(first: String, in full: String, with other: String) -> String { + guard let range = full.range(of: first) else { return full } + return full.replacingCharacters(in: range, with: other) + } + let prefix = method.shortName + let full = method.name + let range = full.range(of: prefix)! + var unrefined = "\(full[range.upperBound...])" + parameters.map { p -> (String,String) in + return ("\(p.type)","\(p.justType)") + }.forEach { + unrefined = replacing(first: $0, in: unrefined, with: $1) + } + return unrefined + } + + private func parametersForProxyInit() -> String { + let generics = getGenericsWithoutConstraints() + return parameters.map { "\($0.wrappedForProxy(generics, hasAvailability))" }.joined(separator: ", ") + } + + private func isGeneric() -> Bool { + return method.shortName.contains("<") && method.shortName.contains(">") + } + + private func getVariadicParametersNames() -> [String] { + let pattern = "[\\(|,]( *[_|\\w]* )? *(\\w+) *\\: *(.+?\\.\\.\\.)" + let str = method.name + let range = NSRange(location: 0, length: (str as NSString).length) + + guard let regex = try? NSRegularExpression(pattern: pattern) else { return [] } + + var result: [String] = regex + .matches(in: str, options: [], range: range) + .compactMap { match -> String? in + guard let nameRange = Range(match.range(at: 2), in: str) else { return nil } + return String(str[nameRange]) + } + return result + } + + /// Returns list of generics used in method signature, without their constraints (like [T,U,V]) + /// + /// - Returns: Array of strings, where each strings represent generic name + private func getGenericsWithoutConstraints() -> [String] { + let name = method.shortName + guard let start = name.firstIndex(of: "<"), let end = name.firstIndex(of: ">") else { return [] } + + var genPart = name[start...end] + genPart.removeFirst() + genPart.removeLast() + + let parts = genPart.replacingOccurrences(of: " ", with: "").split(separator: ",").map(String.init) + return parts.map { stripGenPart(part: $0) } + } + + /// Returns list of generic constraintes from method signature. Does only contain stuff between '<' and '>' + /// + /// - Returns: Array of strings, like ["T: Codable", "U: Whatever"] + private func getGenericsConstraints(_ generics: [String], filterSingle: Bool = true) -> [String] { + let name = method.shortName + guard let start = name.firstIndex(of: "<"), let end = name.firstIndex(of: ">") else { return [] } + + var genPart = name[start...end] + genPart.removeFirst() + genPart.removeLast() + + let parts = genPart.replacingOccurrences(of: " ", with: "").split(separator: ",").map(String.init) + return parts.filter { + let components = $0.components(separatedBy: ":") + return (components.count == 2 || !filterSingle) && generics.contains(components[0]) + } + } + + private func getGenericsAmongParameters() -> [String] { + return getGenericsWithoutConstraints().filter { + for param in self.parameters { + if param.isGeneric([$0]) { return true } + } + return false + } + } + + private func wrapGenerics(_ generics: [String]) -> String { + guard !generics.isEmpty else { return "" } + return "<\(generics.joined(separator:","))>" + } + + private func stripGenPart(part: String) -> String { + return part.split(separator: ":").map(String.init).first! + } + + private func returnTypeStripped(_ method: SourceryRuntime.Method, type: Bool = false) -> String { + let returnTypeRaw = "\(method.returnTypeName)" + var stripped: String = { + guard let range = returnTypeRaw.range(of: "where") else { return returnTypeRaw } + var stripped = returnTypeRaw + stripped.removeSubrange((range.lowerBound)...) + return stripped + }() + stripped = stripped.trimmingCharacters(in: CharacterSet(charactersIn: " ")) + guard type else { return stripped } + return "(\(stripped)).Type" + } + + private func whereClauseConstraints() -> [String] { + let returnTypeRaw = method.returnTypeName.name + guard let range = returnTypeRaw.range(of: "where") else { return [] } + var whereClause = returnTypeRaw + whereClause.removeSubrange(...(range.upperBound)) + return whereClause + .trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + .components(separatedBy: ",") + } + + private func whereClauseExpression() -> String { + let constraints = whereClauseConstraints() + if constraints.isEmpty { + return "" + } + return " where " + constraints.joined(separator: ", ") + } + + private func methodInfo() -> (annotation: String, methodName: String, genericConstrains: String) { + let generics = getGenericsAmongParameters() + let methodName = methodRegistrar.returnTypeMatters(uniqueName: uniqueName) ? method.shortName : "\(method.callName)\(wrapGenerics(generics))" + let constraints: String = { + let constraints: [String] + if methodRegistrar.returnTypeMatters(uniqueName: uniqueName) { + constraints = whereClauseConstraints() + } else { + constraints = getGenericsConstraints(generics) + } + guard !constraints.isEmpty else { return "" } + + return " where \(constraints.joined(separator: ", "))" + }() + var attributes = self.methodAttributesNonObjc + attributes = attributes.condenseWhitespace() + attributes = attributes.isEmpty ? "" : "\(attributes)\n\t\t" + return (attributes, methodName, constraints) + } +} + +extension String { + func condenseWhitespace() -> String { + let components = self.components(separatedBy: .whitespacesAndNewlines) + return components.filter { !$0.isEmpty }.joined(separator: " ") + } +} + +class SubscriptRegistrar { + var registered: [String: Int] = [:] + var namesWithoutReturnType: [String: Int] = [:] + var suffixes: [String: Int] = [:] + + func register(_ name: String, _ uniqueName: String) { + let count = registered[name] ?? 0 + registered[name] = count + 1 + suffixes[uniqueName] = count + 1 + } + func register(short name: String) { + let count = namesWithoutReturnType[name] ?? 0 + namesWithoutReturnType[name] = count + 1 + } +} + +class SubscriptWrapper { + let wrapped: SourceryRuntime.Subscript + var readonly: Bool { return !wrapped.isMutable } + var wrappedParameters: [ParameterWrapper] { return wrapped.parameters.map { ParameterWrapper($0, current: current) } } + var casesCount: Int { return readonly ? 1 : 2 } + var nestedType: String { return "\(TypeWrapper(wrapped.returnTypeName, current: current).nestedParameter)" } + let associatedTypes: [String]? + let genericTypesList: [String] + let genericTypesModifier: String? + let whereClause: String + var hasAvailability: Bool { wrapped.attributes["available"]?.isEmpty == false } + let current: Current + let subscriptRegistrar: SubscriptRegistrar + + private var methodAttributes: String { + return Helpers.extractAttributes(from: self.wrapped.attributes, filterOutStartingWith: ["mutating", "@inlinable"]) + } + private var methodAttributesNonObjc: String { + return Helpers.extractAttributes(from: self.wrapped.attributes, filterOutStartingWith: ["mutating", "@inlinable", "@objc"]) + } + + private let noStubDefinedMessage = "Stub return value not specified for subscript. Use given first." + + func register() { + subscriptRegistrar.register(registrationName("get"),uniqueName) + subscriptRegistrar.register(short: shortName) + guard !readonly else { return } + subscriptRegistrar.register(registrationName("set"),uniqueName) + } + + init(_ wrapped: SourceryRuntime.Subscript, current: Current, subscriptRegistrar: SubscriptRegistrar) { + self.wrapped = wrapped + self.current = current + self.subscriptRegistrar = subscriptRegistrar + associatedTypes = Helpers.extractAssociatedTypes(from: wrapped) + genericTypesList = Helpers.extractGenericsList(associatedTypes) + whereClause = Helpers.extractWhereClause(from: wrapped) ?? "" + if let types = associatedTypes { + genericTypesModifier = "<\(types.joined(separator: ","))>" + } else { + genericTypesModifier = nil + } + } + + func registrationName(_ accessor: String) -> String { + return "subscript_\(accessor)_\(wrappedParameters.map({ $0.sanitizedForEnumCaseName() }).joined(separator: "_"))" + } + var shortName: String { return "public subscript\(genericTypesModifier ?? " ")(\(wrappedParameters.map({ $0.asMethodArgument() }).joined(separator: ", ")))" } + var uniqueName: String { return "\(shortName) -> \(wrapped.returnTypeName)\(self.whereClause)" } + + private func nameSuffix(_ accessor: String) -> String { + guard let count = subscriptRegistrar.registered[registrationName(accessor)] else { return "" } + guard count > 1 else { return "" } + guard let index = subscriptRegistrar.suffixes[uniqueName] else { return "" } + return "_\(index)" + } + + // call + func subscriptCall() -> String { + let get = "\n\t\tget {\(getter())\n\t\t}" + let set = readonly ? "" : "\n\t\tset {\(setter())\n\t\t}" + var attributes = self.methodAttributesNonObjc + attributes = attributes.isEmpty ? "" : "\(attributes)\n\t" + return "\(attributes)\(uniqueName) {\(get)\(set)\n\t}" + } + private func getter() -> String { + let method = ".\(subscriptCasePrefix("get"))(\(parametersForMethodCall()))" + let optionalReturnWorkaround = "\(wrapped.returnTypeName)".hasSuffix("?") + let noStubDefined = (optionalReturnWorkaround || wrapped.returnTypeName.isOptional) ? "return nil" : "onFatalFailure(\"\(noStubDefinedMessage)\"); Failure(\"noStubDefinedMessage\")" + return + "\n\t\t\taddInvocation(\(method))" + + "\n\t\t\tdo {" + + "\n\t\t\t\treturn try methodReturnValue(\(method)).casted()" + + "\n\t\t\t} catch {" + + "\n\t\t\t\t\(noStubDefined)" + + "\n\t\t\t}" + } + private func setter() -> String { + let method = ".\(subscriptCasePrefix("set"))(\(parametersForMethodCall(set: true)))" + return "\n\t\t\taddInvocation(\(method))" + } + + var assertionName: String { + return readonly ? assertionName("get") : "\(assertionName("get"))\n\t\t\t\(assertionName("set"))" + } + private func assertionName(_ accessor: String) -> String { + return "case .\(subscriptCasePrefix(accessor)): return " + + "\"[\(accessor)] `subscript`\(genericTypesModifier ?? "")[\(parametersForAssertionName())]\"" + } + + // method type + func subscriptCasePrefix(_ accessor: String) -> String { + return "\(registrationName(accessor))\(nameSuffix(accessor))" + } + func subscriptCaseName(_ accessor: String, availability: Bool = false) -> String { + return "\(subscriptCasePrefix(accessor))(\(parametersForMethodTypeDeclaration(availability: availability, set: accessor == "set")))" + } + func subscriptCases() -> String { + if readonly { + return "case \(subscriptCaseName("get", availability: hasAvailability))" + } else { + return "case \(subscriptCaseName("get", availability: hasAvailability))\n\t\tcase \(subscriptCaseName("set", availability: hasAvailability))" + } + } + func equalCase(_ accessor: String) -> String { + var lhsParams = wrapped.parameters.map { "lhs\($0.name.capitalized)" }.joined(separator: ", ") + var rhsParams = wrapped.parameters.map { "rhs\($0.name.capitalized)" }.joined(separator: ", ") + var comparators = "\t\t\t\tvar results: [Matcher.ParameterComparisonResult] = []\n" + comparators += wrappedParameters.map { "\t\t\t\t\($0.comparatorResult())" }.joined(separator: "\n") + + if accessor == "set" { + lhsParams += ", lhsDidSet" + rhsParams += ", rhsDidSet" + comparators += "\n\t\t\t\tresults.append(Matcher.ParameterComparisonResult(Parameter.compare(lhs: lhsDidSet, rhs: rhsDidSet, with: matcher), lhsDidSet, rhsDidSet, \"newValue\"))" + } + + comparators += "\n\t\t\t\treturn Matcher.ComparisonResult(results)" + + // comparatorResult() + return "case (let .\(subscriptCasePrefix(accessor))(\(lhsParams)), let .\(subscriptCasePrefix(accessor))(\(rhsParams))):\n" + comparators + } + func equalCases() -> String { + return readonly ? equalCase("get") : "\(equalCase("get"))\n\t\t\t\(equalCase("set"))" + } + func intValueCase() -> String { + return readonly ? intValueCase("get") : "\(intValueCase("get"))\n\t\t\t\(intValueCase("set"))" + } + func intValueCase(_ accessor: String) -> String { + let params = wrappedParameters.enumerated().map { offset, _ in + return "p\(offset)" + } + let definitions = params.joined(separator: ", ") + (accessor == "set" ? ", _" : "") + let paramsSum = params.map({ "\($0).intValue" }).joined(separator: " + ") + return "case let .\(subscriptCasePrefix(accessor))(\(definitions)): return \(paramsSum)" + } + + // Given + func givenConstructorName() -> String { + let returnTypeString = returnsSelf ? replaceSelf : TypeWrapper(wrapped.returnTypeName, current: current).stripped + var attributes = self.methodAttributesNonObjc + attributes = attributes.isEmpty ? "" : "\(attributes)\n\t\t" + return "\(attributes)public static func `subscript`\(genericTypesModifier ?? "")(\(parametersForProxySignature()), willReturn: \(returnTypeString)...) -> SubscriptStub" + } + func givenConstructor() -> String { + return "return Given(method: .\(subscriptCasePrefix("get"))(\(parametersForProxyInit())), products: willReturn.map({ StubProduct.return($0 as Any) }))" + } + + // Verify + func verifyConstructorName(set: Bool = false) -> String { + let returnTypeString = returnsSelf ? replaceSelf : nestedType + let returning = set ? "" : returningParameter(true, true) + var attributes = self.methodAttributesNonObjc + attributes = attributes.isEmpty ? "" : "\(attributes)\n\t\t" + return "\(attributes)public static func `subscript`\(genericTypesModifier ?? "")(\(parametersForProxySignature())\(returning)\(set ? ", set newValue: \(returnTypeString)" : "")) -> Verify" + } + func verifyConstructor(set: Bool = false) -> String { + return "return Verify(method: .\(subscriptCasePrefix(set ? "set" : "get"))(\(parametersForProxyInit(set: set))))" + } + + // Generics + private func getGenerics() -> [String] { + return genericTypesList + } + + // Helpers + private var returnsSelf: Bool { return TypeWrapper(wrapped.returnTypeName, current: current).isSelfType } + private var replaceSelf: String { return current.selfType } + private func returnTypeStripped(type: Bool = false) -> String { + let returnTypeRaw = "\(wrapped.returnTypeName)" + var stripped: String = { + guard let range = returnTypeRaw.range(of: "where") else { return returnTypeRaw } + var stripped = returnTypeRaw + stripped.removeSubrange((range.lowerBound)...) + return stripped + }() + stripped = stripped.trimmingCharacters(in: CharacterSet(charactersIn: " ")) + guard type else { return stripped } + return "(\(stripped)).Type" + } + private func returnTypeMatters() -> Bool { + let count = subscriptRegistrar.namesWithoutReturnType[shortName] ?? 0 + return count > 1 + } + + // params + private func returningParameter(_ multiple: Bool, _ front: Bool) -> String { + guard returnTypeMatters() else { return "" } + let returning: String = "returning: \(returnTypeStripped(type: true))" + guard multiple else { return returning } + return front ? ", \(returning)" : "\(returning), " + } + private func parametersForMethodTypeDeclaration(availability: Bool = false, set: Bool = false) -> String { + let generics: [String] = getGenerics() + let params = wrappedParameters.map { param in + if param.isGeneric(generics) { return param.genericType } + if availability { return param.typeErasedType } + return param.nestedType + }.joined(separator: ", ") + guard set else { return params } + let newValue = TypeWrapper(wrapped.returnTypeName, current: current).isGeneric(generics) ? "Parameter" : nestedType + return "\(params), \(newValue)" + } + private func parametersForProxyInit(set: Bool = false) -> String { + let generics = getGenerics() + let newValue = TypeWrapper(wrapped.returnTypeName, current: current).isGeneric(generics) ? "newValue.wrapAsGeneric()" : "newValue" + return wrappedParameters.map { "\($0.wrappedForProxy(generics, hasAvailability))" }.joined(separator: ", ") + (set ? ", \(newValue)" : "") + } + private func parametersForProxySignature(set: Bool = false) -> String { + return wrappedParameters.map { "\($0.labelAndName()): \($0.nestedType)" }.joined(separator: ", ") + (set ? ", set newValue: \(nestedType)" : "") + } + private func parametersForAssertionName() -> String { + return wrappedParameters.map { "\($0.labelAndName())" }.joined(separator: ", ") + } + private func parametersForMethodCall(set: Bool = false) -> String { + let generics = getGenerics() + let params = wrappedParameters.map { $0.wrappedForCalls(generics, hasAvailability) }.joined(separator: ", ") + let postfix = TypeWrapper(wrapped.returnTypeName, current: current).isGeneric(generics) ? ".wrapAsGeneric()" : "" + return !set ? params : "\(params), \(nestedType).value(newValue)\(postfix)" + } +} + +class VariableWrapper { + let variable: SourceryRuntime.Variable + let scope: String + var readonly: Bool { return variable.writeAccess.isEmpty } + var privatePrototypeName: String { return "__p_\(variable.name)".replacingOccurrences(of: "`", with: "") } + var casesCount: Int { return readonly ? 1 : 2 } + let current: Current + + var accessModifier: String { + // TODO: Fix access levels for SwiftyPrototype + // guard variable.type?.accessLevel != "internal" else { return "" } + return "public " + } + var attributes: String { + let value = Helpers.extractAttributes(from: self.variable.attributes) + return value.isEmpty ? "\(accessModifier)" : "\(value)\n\t\t\(accessModifier)" + } + var noStubDefinedMessage: String { return "\(scope) - stub value for \(variable.name) was not defined" } + + var getter: String { + let staticModifier = variable.isStatic ? "\(scope)." : "" + let returnValue = variable.isOptional ? "optionalGivenGetterValue(.\(propertyCaseGetName), \"\(noStubDefinedMessage)\")" : "givenGetterValue(.\(propertyCaseGetName), \"\(noStubDefinedMessage)\")" + return "\n\t\tget {\t\(staticModifier)invocations.append(.\(propertyCaseGetName)); return \(staticModifier)\(privatePrototypeName) ?? \(returnValue) }" + } + var setter: String { + let staticModifier = variable.isStatic ? "\(scope)." : "" + if readonly { + return "" + } else { + return "\n\t\tset {\t\(staticModifier)invocations.append(.\(propertyCaseSetName)(.value(newValue))); \(variable.isStatic ? "\(scope)." : "")\(privatePrototypeName) = newValue }" + } + } + var prototype: String { + let staticModifier = variable.isStatic ? "static " : "" + + return "\(attributes)\(staticModifier)var \(variable.name): \(variable.typeName.name) {" + + "\(getter)" + + "\(setter)" + + "\n\t}" + } + var assertionName: String { + var result = "case .\(propertyCaseGetName): return \"[get] .\(variable.name)\"" + if !readonly { + result += "\n\t\t\tcase .\(propertyCaseSetName): return \"[set] .\(variable.name)\"" + } + return result + } + + var privatePrototype: String { + let staticModifier = variable.isStatic ? "static " : "" + var typeName = "\(variable.typeName.unwrappedTypeName)" + let isWrappedInBrackets = typeName.hasPrefix("(") && typeName.hasSuffix(")") + if !isWrappedInBrackets { + typeName = "(\(typeName))" + } + return "private \(staticModifier)var \(privatePrototypeName): \(typeName)?" + } + var nestedType: String { return "\(TypeWrapper(variable.typeName, current: current).nestedParameter)" } + + init(_ variable: SourceryRuntime.Variable, scope: String, current: Current) { + self.variable = variable + self.scope = scope + self.current = current + } + + func compareCases() -> String { + var result = propertyCaseGetCompare() + if !readonly { + result += "\n\t\t\t\(propertyCaseSetCompare())" + } + return result + } + + func propertyGet() -> String { + let staticModifier = variable.isStatic ? "Static" : "" + return "public static var \(variable.name): \(staticModifier)Verify { return \(staticModifier)Verify(method: .\(propertyCaseGetName)) }" + } + + func propertySet() -> String { + let staticModifier = variable.isStatic ? "Static" : "" + return "public static func \(variable.name)(set newValue: \(nestedType)) -> \(staticModifier)Verify { return \(staticModifier)Verify(method: .\(propertyCaseSetName)(newValue)) }" + } + + var propertyCaseGetName: String { return "p_\(variable.name)_get".replacingOccurrences(of: "`", with: "") } + func propertyCaseGet() -> String { + return "case \(propertyCaseGetName)" + } + func propertyCaseGetCompare() -> String { + return "case (.\(propertyCaseGetName),.\(propertyCaseGetName)): return Matcher.ComparisonResult.match" + } + func propertyCaseGetIntValue() -> String { + return "case .\(propertyCaseGetName): return 0" + } + + var propertyCaseSetName: String { return "p_\(variable.name)_set".replacingOccurrences(of: "`", with: "") } + func propertyCaseSet() -> String { + return "case \(propertyCaseSetName)(\(nestedType))" + } + func propertyCaseSetCompare() -> String { + let lhsName = "left" + let rhsName = "right" + let comaprison = "Matcher.ParameterComparisonResult(\(nestedType).compare(lhs: \(lhsName), rhs: \(rhsName), with: matcher), \(lhsName), \(rhsName), \"newValue\")" + let result = "Matcher.ComparisonResult([\(comaprison)])" + return "case (.\(propertyCaseSetName)(let left),.\(propertyCaseSetName)(let right)): return \(result)" + } + func propertyCaseSetIntValue() -> String { + return "case .\(propertyCaseSetName)(let newValue): return newValue.intValue" + } + + // Given + func givenConstructorName(prefix: String = "") -> String { + return "\(attributes)static func \(variable.name)(getter defaultValue: \(TypeWrapper(variable.typeName, current: current).stripped)...) -> \(prefix)PropertyStub" + } + + func givenConstructor(prefix: String = "") -> String { + return "return \(prefix)Given(method: .\(propertyCaseGetName), products: defaultValue.map({ StubProduct.return($0 as Any) }))" + } +} + +_%> +<%_ +func generate(type: Type, idx: Int, methodRegistrar: MethodRegistrar, subscriptRegistrar: SubscriptRegistrar) async -> (Int, String) { + var sourceryBuffer: String = "" + var mockedCount = 0 + let autoMockable: Bool = type.inheritedTypes.contains("AutoMockable") || type.annotations["AutoMockable"] != nil + let protocolToDecorate = types.protocols.first(where: { $0.name == (type.annotations["mock"] as? String) }) + guard let aProtocol = autoMockable ? type : protocolToDecorate else { return (idx, sourceryBuffer) } + mockedCount += 1 + var current = Current(type: type) + let associatedTypes: [String]? = Helpers.extractAssociatedTypes(from: aProtocol) + let attributes: String = Helpers.extractAttributes(from: type.attributes) + let typeAliases: [String] = Helpers.extractTypealiases(from: aProtocol) + let genericTypesModifier: String = Helpers.extractGenericTypesModifier(associatedTypes) + let genericTypesConstraints: String = Helpers.extractGenericTypesConstraints(associatedTypes) + let allSubscripts = aProtocol.allSubscripts + let allVariables = uniques(variables: aProtocol.allVariables.filter({ !$0.isStatic })) + let allStaticVariables = uniques(variables: aProtocol.allVariables.filter({ $0.isStatic })) + let allMethods = uniques(methods: aProtocol.allMethods.filter({ !$0.isStatic || $0.isInitializer })) + let selfConstrained = allMethods.map { wrapMethod($0, current: current, methodRegistrar: methodRegistrar) }.contains(where: { $0.returnsGenericConstrainedToSelf || $0.parametersContainsSelf }) + let isActor = type.inheritedTypes.contains("Actor") // Check for Actor inheritance + let accessModifier: String = isActor ? "public" : (selfConstrained ? "public final" : "open") // Use actor if inherited + current.accessModifier = accessModifier // TODO: Temporary workaround for access modifiers + let inheritFromNSObject = type.annotations["ObjcProtocol"] != nil || attributes.contains("@objc") + let allMethodsForMethodType = uniquesWithoutGenericConstraints(methods: aProtocol.allMethods.filter({ !$0.isStatic })) + let allStaticMethods = uniques(methods: aProtocol.allMethods.filter({ $0.isStatic && !$0.isInitializer })) + let allStaticMethodsForMethodType = uniquesWithoutGenericConstraints(methods: aProtocol.allMethods.filter({ $0.isStatic })) + let conformsToStaticMock = !allStaticMethods.isEmpty || !allStaticVariables.isEmpty-%><%_ -%><%_ -%> +<%_ if autoMockable { -%> +// MARK: - <%= type.name %> +<%= attributes %> +<%= accessModifier %> <%= isActor ? "actor" : "class" %> <%= type.name %><%= mockTypeName %><%= genericTypesModifier %>:<%= inheritFromNSObject ? " NSObject," : "" %> <%= type.name %>, <%= isActor ? "@preconcurrency" : "" %> Mock<%= conformsToStaticMock ? ", StaticMock" : "" %><%= genericTypesConstraints %> { + public init(sequencing sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst, stubbing stubbingPolicy: StubbingPolicy = .wrap, file: StaticString = #file, line: UInt = #line) { + SwiftyMockyTestObserver.setup() + self.sequencingPolicy = sequencingPolicy + self.stubbingPolicy = stubbingPolicy + self.file = file + self.line = line + } + +<%_ } else { -%> +// sourcery:inline:auto:<%= type.name %>.autoMocked +<%_ } -%> +<%# ================================================== MAIN CLASS -%><%_ -%> + <%# ================================================== MOCK INTERNALS -%><%_ -%> + var matcher: Matcher = Matcher.default + var stubbingPolicy: StubbingPolicy = .wrap + var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + + private var queue = DispatchQueue(label: "com.swiftymocky.invocations", qos: .userInteractive) + private var invocations: [MethodType] = [] + private var methodReturnValues: [Given] = [] + private var methodPerformValues: [Perform] = [] + private var file: StaticString? + private var line: UInt? + + public typealias PropertyStub = Given + public typealias MethodStub = Given + public typealias SubscriptStub = Given + <%_ for typeAlias in typeAliases { -%> + public typealias <%= typeAlias %> + <%_ } %> <%_ -%> + + /// Convenience method - call setupMock() to extend debug information when failure occurs + public func setupMock(file: StaticString = #file, line: UInt = #line) { + self.file = file + self.line = line + } + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + <%_ -%> + <%# ================================================== STATIC MOCK INTERNALS -%><%_ -%> + <%_ if conformsToStaticMock { -%> + static var matcher: Matcher = Matcher.default + static var stubbingPolicy: StubbingPolicy = .wrap + static var sequencingPolicy: SequencingPolicy = .lastWrittenResolvedFirst + static private var queue = DispatchQueue(label: "com.swiftymocky.invocations.static", qos: .userInteractive) + static private var invocations: [StaticMethodType] = [] + static private var methodReturnValues: [StaticGiven] = [] + static private var methodPerformValues: [StaticPerform] = [] + public typealias StaticPropertyStub = StaticGiven + public typealias StaticMethodStub = StaticGiven + + /// Clear mock internals. You can specify what to reset (invocations aka verify, givens or performs) or leave it empty to clear all mock internals + public static func resetMock(_ scopes: MockScope...) { + let scopes: [MockScope] = scopes.isEmpty ? [.invocation, .given, .perform] : scopes + if scopes.contains(.invocation) { invocations = [] } + if scopes.contains(.given) { methodReturnValues = [] } + if scopes.contains(.perform) { methodPerformValues = [] } + } + <%_ } -%> + + <%# ================================================== VARIABLES -%><%_ -%> + <%_ for variable in allVariables { -%> + <%_ if autoMockable { -%> + <%= stubProperty(variable,"\(type.name)\(mockTypeName)", current: current) %> + <%_ } else { %> + <%= stubProperty(variable,"\(type.name)", current: current) %> + <%_ } %> + <%_ } %> <%_ -%> + + <%# ================================================== STATIC VARIABLES -%><%_ -%> + <%_ for variable in allStaticVariables { -%> + <%_ if autoMockable { -%> + <%= stubProperty(variable,"\(type.name)\(mockTypeName)", current: current) %> + <%_ } else { %> + <%= stubProperty(variable,"\(type.name)", current: current) %> + <%_ } %> + <%_ } %> <%_ -%> + + <%# ================================================== METHOD REGISTRATIONS -%><%_ -%> + <%_ if autoMockable { -%> + <%_ current.selfType = "\(type.name)\(mockTypeName)\(genericTypesModifier)" -%> + <%_ } else { %> + <%_ current.selfType = "\(type.name)\(mockTypeName)\(genericTypesModifier)" -%> + <%_ } %> + <%_ let wrappedSubscripts = allSubscripts.map { wrapSubscript($0, current: current, subscriptRegistrar: subscriptRegistrar) } -%> + <%_ let wrappedMethods = allMethods.map { wrapMethod($0, current: current, methodRegistrar: methodRegistrar) }.filter({ $0.wrappedInMethodType() }) -%> + <%_ let wrappedVariables = allVariables.map { justWrap($0, current: current) } -%> + <%_ let wrappedMethodsForMethodType = allMethodsForMethodType.map { wrapMethod($0, current: current, methodRegistrar: methodRegistrar) }.filter({ $0.wrappedInMethodType() }) -%> + <%_ let wrappedInitializers = allMethods.map { wrapMethod($0, current: current, methodRegistrar: methodRegistrar) }.filter({ $0.method.isInitializer }) -%> + <%_ let wrappedStaticMethods = allStaticMethods.map { wrapMethod($0, current: current, methodRegistrar: methodRegistrar) }.filter({ $0.wrappedInMethodType() }) -%> + <%_ let wrappedStaticVariables = allStaticVariables.map { justWrap($0, current: current) } -%> + <%_ let wrappedStaticMethodsForMethodType = allStaticMethodsForMethodType.map { wrapMethod($0, current: current, methodRegistrar: methodRegistrar) }.filter({ $0.wrappedInMethodType() }) -%> + <%_ for variable in allVariables { propertyRegister(variable, methodRegistrar: methodRegistrar, current: current) } -%> + <%_ for variable in allStaticVariables { propertyRegister(variable, methodRegistrar: methodRegistrar, current: current) } -%> + <%_ for method in wrappedMethods { method.register() } -%> + <%_ for wrapped in wrappedSubscripts { wrapped.register() } -%> + <%_ for method in wrappedStaticMethods { method.register() } -%><%_ -%> + <%_ let variableCasesCount: Int = wrappedVariables.reduce(0) { return $0 + $1.casesCount } -%><%_ -%> + <%_ let subscriptsCasesCount: Int = wrappedSubscripts.reduce(0) { return $0 + $1.casesCount } -%><%_ -%> + <%_ let staticVariableCasesCount: Int = wrappedStaticVariables.reduce(0) { return $0 + $1.casesCount } -%><%_ -%> + + <%# ================================================== STATIC STUBS -%><%_ -%> + <%_ for method in wrappedStaticMethods { -%> + <%= method.functionPrototype() _%> { + <%= method.stubBody() _%> + } + + <%_ } %><%_ -%> + <%_ -%> + <%# ================================================== INITIALIZERS -%><%_ -%> + <%_ for method in wrappedInitializers { -%> + <%= method.functionPrototype() _%> { } + + <%_ } -%><%_ -%> + <%_ -%><%_ -%> + <%# ================================================== STUBS -%><%_ -%> + <%_ for method in wrappedMethods { -%> + + <%_ if method.isMainActorInActor { -%> + + <%= method.functionPrototype() _%> { + fatalError("This method is not implemented. Use async version instead.") + } + + <%= method.functionPrototype(true) _%> { + <%= method.stubBody(true) _%> + } + <%_ } else { -%> + <%= method.functionPrototype() _%> { + <%= method.stubBody() _%> + } + <%_ } -%> + <%_ } -%> + <%_ for wrapped in wrappedSubscripts { -%> + <%= wrapped.subscriptCall() _%> + + <%_ } -%> + <%# ================================================== STATIC METHOD TYPE -%><%_ -%> + <%_ if conformsToStaticMock { -%> + fileprivate enum StaticMethodType { + <%_ for method in wrappedStaticMethodsForMethodType { -%> + <%= method.methodTypeDeclarationWithParameters() _%> + <%_ } %> <%_ for variable in allStaticVariables { -%> + <%= propertyMethodTypes(variable, current: current) %> + <%_ } %> <%_ %> + <%_ -%> + static func compareParameters(lhs: StaticMethodType, rhs: StaticMethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { <%_ for method in wrappedStaticMethodsForMethodType { %> + <%= method.equalCases() %> + <%_ } %> <%_ for variable in wrappedStaticVariables { -%> + <%= variable.compareCases() %> + <%_ } %> <%_ -%> <%_ if wrappedStaticMethods.count + staticVariableCasesCount > 1 { -%> + default: return .none + <%_ } -%> + } + } + <%_ %> + func intValue() -> Int { + switch self { <%_ for method in wrappedStaticMethodsForMethodType { %> + <%= method.intValueCase -%><% } %> + <%_ for variable in allStaticVariables { -%> + <%= propertyMethodTypesIntValue(variable, current: current) %> + <%_ } %> <%_ -%> + } + } + func assertionName() -> String { + switch self { <%_ for method in wrappedStaticMethodsForMethodType { %> + <%= method.assertionName -%><% } %> + <%_ for variable in wrappedStaticVariables { -%> + <%= variable.assertionName %> + <%_ } %> + } + } + } + + open class StaticGiven: StubbedMethod { + fileprivate var method: StaticMethodType + + private init(method: StaticMethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + <%_ for variable in allStaticVariables { -%> + <%= wrapProperty(variable, current: current).givenConstructorName(prefix: "Static") -%> { + <%= wrapProperty(variable, current: current).givenConstructor(prefix: "Static") _%> + } + <%_ } %> <%_ %> + <%_ for method in wrappedStaticMethodsForMethodType.filter({ !$0.method.returnTypeName.isVoid && !$0.method.isInitializer }) { -%> + <%= method.givenConstructorName(prefix: "Static") -%> { + <%= method.givenConstructor(prefix: "Static") _%> + } + <%_ } -%> + <%_ for method in wrappedStaticMethodsForMethodType.filter({ !$0.method.throws && !$0.method.rethrows && !$0.method.returnTypeName.isVoid && !$0.method.isInitializer }) { -%> + <%= method.givenProduceConstructorName(prefix: "Static") -%> { + <%= method.givenProduceConstructor(prefix: "Static") _%> + } + <%_ } -%> + <%_ for method in wrappedStaticMethodsForMethodType.filter({ ($0.method.throws || $0.method.rethrows) && !$0.method.isInitializer }) { -%> + <%= method.givenConstructorNameThrows(prefix: "Static") -%> { + <%= method.givenConstructorThrows(prefix: "Static") _%> + } + <%= method.givenProduceConstructorNameThrows(prefix: "Static") -%> { + <%= method.givenProduceConstructorThrows(prefix: "Static") _%> + } + <%_ } %> <%_ -%> + } + + public struct StaticVerify { + fileprivate var method: StaticMethodType + + <%_ for method in wrappedStaticMethodsForMethodType { -%> + <%= method.verificationProxyConstructorName(prefix: "Static") -%> { <%= method.verificationProxyConstructor(prefix: "Static") _%> } + <%_ } %> <%_ -%> + <%_ for variable in allStaticVariables { -%> + <%= propertyTypes(variable, current: current) %> + <%_ } %> <%_ -%> + } + + public struct StaticPerform { + fileprivate var method: StaticMethodType + var performs: Any + + <%_ for method in wrappedStaticMethodsForMethodType { -%> + <%= method.performProxyConstructorName(prefix: "Static") -%> { + <%= method.performProxyConstructor(prefix: "Static") _%> + } + <%_ } %> <%_ -%> + } + + <% } -%> + <%# ================================================== METHOD TYPE -%><%_ -%> + <%_ if !wrappedMethods.isEmpty || !allVariables.isEmpty || !allSubscripts.isEmpty { -%> + + fileprivate enum MethodType { + <%_ for method in wrappedMethodsForMethodType { -%> + <%= method.methodTypeDeclarationWithParameters() _%> + <%_ } -%> <%_ for variable in allVariables { -%> + <%= propertyMethodTypes(variable, current: current) %> + <%_ } %> <%_ %> <%_ for wrapped in wrappedSubscripts { -%> + <%= wrapped.subscriptCases() _%> + <%_ } %> <%_ %> + <%_ -%> + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { + switch (lhs, rhs) { <%_ for method in wrappedMethodsForMethodType { %> + <%= method.equalCases() %> + <%_ } %> <%_ for variable in wrappedVariables { -%> + <%= variable.compareCases() %> + <%_ } %> <%_ -%> <%_ for wrapped in wrappedSubscripts { -%> + <%= wrapped.equalCases() %> + <%_ } %> <%_ if wrappedMethods.count + variableCasesCount + subscriptsCasesCount > 1 { -%> + default: return .none + <%_ } -%> + } + } + <%_ %> + func intValue() -> Int { + switch self { <%_ for method in wrappedMethodsForMethodType { %> + <%= method.intValueCase -%><% } %> + <%_ for variable in allVariables { -%> + <%= propertyMethodTypesIntValue(variable, current: current) %> + <%_ } %> <%_ for wrapped in wrappedSubscripts { -%> + <%= wrapped.intValueCase() %> + <%_ } -%> + } + } + func assertionName() -> String { + switch self { <%_ for method in wrappedMethodsForMethodType { %> + <%= method.assertionName -%><% } %> + <%_ for variable in wrappedVariables { -%> + <%= variable.assertionName %> + <%_ } %> <%_ for wrapped in wrappedSubscripts { -%> + <%= wrapped.assertionName %> + <%_ } -%> + } + } + } + <%_ } else { %> + fileprivate struct MethodType { + static func compareParameters(lhs: MethodType, rhs: MethodType, matcher: Matcher) -> Matcher.ComparisonResult { return .match } + func intValue() -> Int { return 0 } + func assertionName() -> String { return "" } + } + <%_ } -%><%_ -%> + + open class Given: StubbedMethod { + fileprivate var method: MethodType + + private init(method: MethodType, products: [StubProduct]) { + self.method = method + super.init(products) + } + + <%_ for variable in allVariables { -%> + <%= wrapProperty(variable, current: current).givenConstructorName() -%> { + <%= wrapProperty(variable, current: current).givenConstructor() _%> + } + <%_ } %> <%_ %> + <%_ for method in wrappedMethodsForMethodType.filter({ !$0.method.returnTypeName.isVoid && !$0.method.isInitializer }) { -%> + <%= method.givenConstructorName() -%> { + <%= method.givenConstructor() _%> + } + <%_ } -%> + <%_ for method in wrappedMethodsForMethodType.filter({ !$0.method.throws && !$0.method.rethrows && !$0.method.returnTypeName.isVoid && !$0.method.isInitializer }) { -%> + <%= method.givenProduceConstructorName() -%> { + <%= method.givenProduceConstructor() _%> + } + <%_ } -%> + <%_ for wrapped in wrappedSubscripts { -%> + <%= wrapped.givenConstructorName() -%> { + <%= wrapped.givenConstructor() _%> + } + <%_ } -%> + <%_ for method in wrappedMethodsForMethodType.filter({ ($0.method.throws || $0.method.rethrows) && !$0.method.isInitializer }) { -%> + <%= method.givenConstructorNameThrows() -%> { + <%= method.givenConstructorThrows() _%> + } + <%= method.givenProduceConstructorNameThrows() -%> { + <%= method.givenProduceConstructorThrows() _%> + } + <%_ } %> <%_ -%> + } + + public struct Verify { + fileprivate var method: MethodType + + <%_ for method in wrappedMethodsForMethodType { -%> + <%= method.verificationProxyConstructorName() -%> { <%= method.verificationProxyConstructor() _%> } + <%_ } %> <%_ -%> + <%_ for variable in allVariables { -%> + <%= propertyTypes(variable, current: current) %> + <%_ } %> <%_ -%> + <%_ for wrapped in wrappedSubscripts { -%> + <%= wrapped.verifyConstructorName() -%> { <%= wrapped.verifyConstructor() _%> } + <%_ if !wrapped.readonly { -%> + <%= wrapped.verifyConstructorName(set: true) -%> { <%= wrapped.verifyConstructor(set: true) _%> } + <%_ } -%> + <%_ } %> <%_ -%> + } + + public struct Perform { + fileprivate var method: MethodType + var performs: Any + + <%_ for method in wrappedMethodsForMethodType { -%> + <%= method.performProxyConstructorName() -%> { + <%= method.performProxyConstructor() _%> + } + <%_ } %> <%_ -%> + } + + <%# ================================================== MOCK METHODS -%><%_ -%> + public func given(_ method: Given) { + methodReturnValues.append(method) + } + + public func perform(_ method: Perform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + public func verify(_ method: Verify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return MethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + private func addInvocation(_ call: MethodType) { + self.queue.sync { invocations.append(call) } + } + private func methodReturnValue(_ method: MethodType) throws -> StubProduct { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + private func methodPerformValue(_ method: MethodType) -> Any? { + matcher.set(file: self.file, line: self.line) + defer { matcher.clearFileAndLine() } + let matched = methodPerformValues.reversed().first { MethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + private func matchingCalls(_ method: MethodType, file: StaticString?, line: UInt?) -> [MethodType] { + matcher.set(file: file ?? self.file, line: line ?? self.line) + defer { matcher.clearFileAndLine() } + return invocations.filter { MethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + private func matchingCalls(_ method: Verify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + private func givenGetterValue(_ method: MethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + onFatalFailure(message) + Failure(message) + } + } + private func optionalGivenGetterValue(_ method: MethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + private func onFatalFailure(_ message: String) { + guard let file = self.file, let line = self.line else { return } // Let if fail if cannot handle gratefully + SwiftyMockyTestObserver.handleFatalError(message: message, file: file, line: line) + } + <%# ================================================== STATIC MOCK METHODS -%><%_ -%> + <%_ if conformsToStaticMock { -%> + + static public func given(_ method: StaticGiven) { + methodReturnValues.append(method) + } + + static public func perform(_ method: StaticPerform) { + methodPerformValues.append(method) + methodPerformValues.sort { $0.method.intValue() < $1.method.intValue() } + } + + static public func verify(_ method: StaticVerify, count: Count = Count.moreOrEqual(to: 1), file: StaticString = #file, line: UInt = #line) { + let fullMatches = matchingCalls(method, file: file, line: line) + let success = count.matches(fullMatches) + let assertionName = method.method.assertionName() + let feedback: String = { + guard !success else { return "" } + return Utils.closestCallsMessage( + for: self.invocations.map { invocation in + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return StaticMethodType.compareParameters(lhs: invocation, rhs: method.method, matcher: matcher) + }, + name: assertionName + ) + }() + MockyAssert(success, "Expected: \(count) invocations of `\(assertionName)`, but was: \(fullMatches).\(feedback)", file: file, line: line) + } + + static private func addInvocation(_ call: StaticMethodType) { + self.queue.sync { invocations.append(call) } + } + static private func methodReturnValue(_ method: StaticMethodType) throws -> StubProduct { + let candidates = sequencingPolicy.sorted(methodReturnValues, by: { $0.method.intValue() > $1.method.intValue() }) + let matched = candidates.first(where: { $0.isValid && StaticMethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch }) + guard let product = matched?.getProduct(policy: self.stubbingPolicy) else { throw MockError.notStubed } + return product + } + static private func methodPerformValue(_ method: StaticMethodType) -> Any? { + let matched = methodPerformValues.reversed().first { StaticMethodType.compareParameters(lhs: $0.method, rhs: method, matcher: matcher).isFullMatch } + return matched?.performs + } + static private func matchingCalls(_ method: StaticMethodType, file: StaticString?, line: UInt?) -> [StaticMethodType] { + matcher.set(file: file, line: line) + defer { matcher.clearFileAndLine() } + return invocations.filter { StaticMethodType.compareParameters(lhs: $0, rhs: method, matcher: matcher).isFullMatch } + } + static private func matchingCalls(_ method: StaticVerify, file: StaticString?, line: UInt?) -> Int { + return matchingCalls(method.method, file: file, line: line).count + } + static private func givenGetterValue(_ method: StaticMethodType, _ message: String) -> T { + do { + return try methodReturnValue(method).casted() + } catch { + Failure(message) + } + } + static private func optionalGivenGetterValue(_ method: StaticMethodType, _ message: String) -> T? { + do { + return try methodReturnValue(method).casted() + } catch { + return nil + } + } + <%_ } -%> +<%_ if autoMockable { -%> +} + +<%_ } else { -%> +<%_ } -%> +<%_ return (idx, sourceryBuffer) -%> +<%_ } -%> + +<%# ================================================== SETUP -%><%_ -%> +<%_ var all = types.all + all += types.protocols.map { $0 } + all += types.protocolCompositions.map { $0 } +-%> + +<%_ +let contents = try await withThrowingTaskGroup(of: (String, String).self) { group in + for (idx, type) in all.enumerated() { + group.addTask { + let result = await generate(type: type, idx: idx, methodRegistrar: MethodRegistrar(), subscriptRegistrar: SubscriptRegistrar()) + return (type.name, result.1) + } + } + var results = [(String, String)]() + for try await (name, content) in group { + if !content.isEmpty { + results.append((name, content)) + } + } + return results +} + +if contents.isEmpty { +-%> +// SwiftyMocky: no AutoMockable found. +// Please define and inherit from AutoMockable, or annotate protocols to be mocked +<%_ +} else { +-%> +<%_ for (name, content) in contents { -%> +// sourcery:file:Mock+<%= name %>.generated.swift +// Generated with SwiftyMocky 4.2.1 +// Required Sourcery: 2.2.6 +// swiftlint:disable all + +<%_ for rule in swiftLintRules(argument) { -%> + <%_ %><%= rule %> +<%_ } -%> + +<%# ================================================== IMPORTS -%><%_ -%> + <%_ for projectImport in projectImports(argument) { -%> + <%_ %><%= projectImport %> + <%_ } -%> + <%# ============================ IMPORTS InAPP (aggregated argument) -%><%_ -%> + <%_ if let swiftyMockyArgs = argument["swiftyMocky"] as? [String: Any] { -%> + <%_ for projectImport in projectImports(swiftyMockyArgs) { -%> + <%_ %><%= projectImport %> + <%_ } -%> + <%_ } -%> +<%= content %> +// sourcery:end +<%_ } -%> +<%_ +} +-%> \ No newline at end of file diff --git a/TransferNotificationContentExtension/NotificationViewController.swift b/TransferNotificationContentExtension/NotificationViewController.swift index fcb42c158..316b79378 100644 --- a/TransferNotificationContentExtension/NotificationViewController.swift +++ b/TransferNotificationContentExtension/NotificationViewController.swift @@ -6,26 +6,26 @@ // Copyright © 2019 Adamant. All rights reserved. // -import UIKit +import CommonKit +import MarkdownKit import SnapKit +import UIKit import UserNotifications import UserNotificationsUI -import MarkdownKit -import CommonKit class NotificationViewController: UIViewController, UNNotificationContentExtension { private let passphraseStoreKey = "accountService.passphrase" private let sizeWithoutCommentLabel: CGFloat = 350.0 - + // MARK: - Rich providers private lazy var adamantProvider: AdamantProvider = { return AdamantProvider() }() - - private lazy var keychain: SecuredStore = { + + private lazy var keychain: SecureStore = { KeychainStore(secureStorage: AdamantSecureStorage()) }() - + /// Lazy contstructors private lazy var richMessageProviders: [String: TransferNotificationContentProvider] = { var providers: [String: TransferNotificationContentProvider] = [ @@ -35,45 +35,45 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi DashProvider.richMessageType: DashProvider(), BtcProvider.richMessageType: BtcProvider() ] - + for token in ERC20Token.supportedTokens { let key = "\(token.symbol)_transaction".lowercased() providers[key] = ERC20Provider(token) } - + return providers }() - + // MARK: - IBOutlets - + @IBOutlet weak var senderAddressLabel: UILabel! @IBOutlet weak var senderNameLabel: UILabel! @IBOutlet weak var senderImageView: UIImageView! - + @IBOutlet weak var recipientAddressLabel: UILabel! @IBOutlet weak var recipientNameLabel: UILabel! @IBOutlet weak var recipientImageView: UIImageView! - + @IBOutlet weak var outcomeArrowImageView: UIImageView! @IBOutlet weak var outcomeArrowView: UIView! @IBOutlet weak var incomeArrowImageView: UIImageView! @IBOutlet weak var incomeArrowView: UIView! - + @IBOutlet weak var amountLabel: UILabel! @IBOutlet weak var currencySymbolLabel: UILabel! @IBOutlet weak var currencyImageView: UIImageView! @IBOutlet weak var dateLabel: UILabel! @IBOutlet weak var commentLabel: UILabel! - + override func viewDidLoad() { super.viewDidLoad() - - incomeArrowView.layer.cornerRadius = incomeArrowView.frame.height/2 + + incomeArrowView.layer.cornerRadius = incomeArrowView.frame.height / 2 incomeArrowView.isHidden = true - - outcomeArrowView.layer.cornerRadius = outcomeArrowView.frame.height/2 + + outcomeArrowView.layer.cornerRadius = outcomeArrowView.frame.height / 2 outcomeArrowView.isHidden = true - + senderAddressLabel.text = "" senderNameLabel.text = "" recipientAddressLabel.text = "" @@ -82,10 +82,10 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi currencySymbolLabel.text = "" dateLabel.text = "" commentLabel.text = "" - + setColors() } - + private func setColors() { let color = UIColor.adamant.textColor.resolvedColor(with: .current) senderAddressLabel.textColor = color @@ -96,21 +96,21 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi currencySymbolLabel.textColor = color dateLabel.textColor = color commentLabel.textColor = color - + senderImageView.tintColor = UIColor.adamant.primary recipientImageView.tintColor = UIColor.adamant.primary currencyImageView.tintColor = UIColor.adamant.primary - + incomeArrowImageView.tintColor = UIColor.white outcomeArrowImageView.tintColor = UIColor.white - + incomeArrowView.backgroundColor = UIColor.adamant.incomeArrowBackgroundColor createBorder(for: incomeArrowView, width: 2.5, color: UIColor.white) - + outcomeArrowView.backgroundColor = UIColor.adamant.outcomeArrowBackgroundColor createBorder(for: outcomeArrowView, width: 2.5, color: UIColor.white) } - + private func createBorder(for view: UIView, width: CGFloat, color: UIColor) { let border = CAShapeLayer() border.frame = view.bounds @@ -120,129 +120,142 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi border.fillColor = UIColor.clear.cgColor view.layer.addSublayer(border) } - + func didReceive(_ notification: UNNotification) { // MARK: 0. Services let core = NativeAdamantCore() let avatarService = AdamantAvatarService() - let api = ExtensionsApiFactory(core: core, securedStore: keychain).make() - + let api = ExtensionsApiFactory(core: core, SecureStore: keychain).make() + guard let passphrase: String = keychain.get(passphraseStoreKey), - let keypair = core.createKeypairFor(passphrase: passphrase) + let keypair = core.createKeypairFor(passphrase: passphrase, password: .empty) else { showError() return } - + // MARK: 1. Get the transaction let trs: Transaction? - if let transactionRaw = notification.request.content.userInfo[AdamantNotificationUserInfoKeys.transaction] as? String, let data = transactionRaw.data(using: .utf8) { + if let transactionRaw = notification.request.content.userInfo[AdamantNotificationUserInfoKeys.transaction] as? String, + let data = transactionRaw.data(using: .utf8) + { trs = try? JSONDecoder().decode(Transaction.self, from: data) } else { guard let raw = notification.request.content.userInfo[AdamantNotificationUserInfoKeys.transactionId] as? String, let id = UInt64(raw) else { showError() return } - + trs = api.getTransaction(by: id) } - + guard let transaction = trs else { showError() return } - + // MARK: 2.1 Variables for UI let date = transaction.date let comments: String? let amount: Decimal let provider: TransferNotificationContentProvider - + // MARK: 2.2 Working on transaction switch transaction.type { case .send: amount = transaction.amount comments = nil provider = adamantProvider - + case .chatMessage: guard let chat = transaction.asset.chat else { showError() return } - + let message: String if let raw = notification.request.content.userInfo[AdamantNotificationUserInfoKeys.decodedMessage] as? String { message = raw } else { - guard let raw = core.decodeMessage(rawMessage: chat.message, rawNonce: chat.ownMessage, senderPublicKey: transaction.senderPublicKey, privateKey: keypair.privateKey) else { + guard + let raw = core.decodeMessage( + rawMessage: chat.message, + rawNonce: chat.ownMessage, + senderPublicKey: transaction.senderPublicKey, + privateKey: keypair.privateKey + ) + else { showError() return } message = raw } - + // Adamant 'transfer with comment' switch chat.type { - case .messageOld: fallthrough - case .message: + case .messageOld, .message: comments = message amount = transaction.amount provider = adamantProvider - + // Rich message case .richMessage: guard let data = message.data(using: String.Encoding.utf8), let richContent = RichMessageTools.richContent(from: data), - let key = (richContent[RichContentKeys.type] as? String)?.lowercased(), - let p = richMessageProviders[key] else { - showError() - return + let key = (richContent[RichContentKeys.type] as? String)?.lowercased(), + let p = richMessageProviders[key] + else { + showError() + return } - + provider = p - + if let raw = richContent[RichContentKeys.transfer.comments] as? String, - raw.count > 0 { + raw.count > 0 + { comments = raw } else { comments = nil } - + if let raw = richContent[RichContentKeys.transfer.amount] as? String, - let decimal = Decimal(string: raw) { + let decimal = Decimal(string: raw) + { amount = decimal } else { amount = 0 } - + default: showError() return } - + default: showError() return } - + // MARK: 3. Names let senderName: String? - + // Cached if let name = notification.request.content.userInfo[AdamantNotificationUserInfoKeys.partnerDisplayName] as? String { senderName = name } // No name, but we have flag - skip it - else if let flag = notification.request.content.userInfo[AdamantNotificationUserInfoKeys.partnerNoDislpayNameKey] as? String, flag == AdamantNotificationUserInfoKeys.partnerNoDisplayNameValue { + else if let flag = notification.request.content.userInfo[AdamantNotificationUserInfoKeys.partnerNoDislpayNameKey] as? String, + flag == AdamantNotificationUserInfoKeys.partnerNoDisplayNameValue + { senderName = nil } else { checkName(of: transaction.senderId, for: transaction.recipientId, api: api, core: core, keypair: keypair) senderName = nil } - + // MARK: 3. Setting up UI - + if let name = senderName { senderNameLabel.text = name senderAddressLabel.text = transaction.senderId @@ -250,19 +263,19 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi senderNameLabel.text = transaction.senderId senderAddressLabel.text = nil } - + recipientNameLabel.text = String.adamant.notifications.yourAddress recipientAddressLabel.text = transaction.recipientId - + dateLabel.text = date.humanizedDateTime() currencyImageView.image = provider.currencyLogoLarge amountLabel.text = AdamantBalanceFormat.full.format(amount) currencySymbolLabel.text = provider.currencySymbol - + if let comments = comments { let parsed = MarkdownParser(font: commentLabel.font).parse(comments) - + if parsed.string.count != comments.count { commentLabel.attributedText = parsed } else { @@ -271,11 +284,11 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi } else { commentLabel.isHidden = true } - + let size = Double(senderImageView.frame.height) senderImageView.image = avatarService.avatar(for: transaction.senderPublicKey, size: size) recipientImageView.image = avatarService.avatar(for: keypair.publicKey, size: size) - + // MARK: 4. View size if comments != nil { commentLabel.setNeedsLayout() @@ -285,51 +298,56 @@ class NotificationViewController: UIViewController, UNNotificationContentExtensi commentLabel.isHidden = true preferredContentSize = CGSize(width: view.frame.width, height: sizeWithoutCommentLabel) } - + incomeArrowView.isHidden = false outcomeArrowView.isHidden = false - + view.setNeedsUpdateConstraints() view.setNeedsLayout() } - + // MARK: - UI private func showError(with message: String? = nil) { let warningView = NotificationWarningView() - - warningView.message = message.map { .adamant.notifications.error(with: $0) } + + warningView.message = + message.map { .adamant.notifications.error(with: $0) } ?? .adamant.notifications.error - + view.addSubview(warningView) warningView.snp.makeConstraints { $0.directionalEdges.equalToSuperview() } } - + private func checkName(of sender: String, for recipient: String, api: ExtensionsApi, core: NativeAdamantCore, keypair: Keypair) { DispatchQueue.global(qos: .userInitiated).async { [weak self] in guard let addressBook = api.getAddressBook(for: recipient, core: core, keypair: keypair), let name = addressBook[sender]?.displayName else { return } - + DispatchQueue.main.async { guard let vc = self else { return } - + let address = self?.senderNameLabel.text - - UIView.transition(with: vc.senderAddressLabel, - duration: 0.1, - options: .transitionCrossDissolve, - animations: { vc.senderAddressLabel.text = address }, - completion: nil) - - UIView.transition(with: vc.senderNameLabel, - duration: 0.1, - options: .transitionCrossDissolve, - animations: { vc.senderNameLabel.text = name }, - completion: nil) + + UIView.transition( + with: vc.senderAddressLabel, + duration: 0.1, + options: .transitionCrossDissolve, + animations: { vc.senderAddressLabel.text = address }, + completion: nil + ) + + UIView.transition( + with: vc.senderNameLabel, + duration: 0.1, + options: .transitionCrossDissolve, + animations: { vc.senderNameLabel.text = name }, + completion: nil + ) } } } diff --git a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/AdvancedContextMenuManager.swift b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/AdvancedContextMenuManager.swift index 0ca2e8529..4d03cddee 100644 --- a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/AdvancedContextMenuManager.swift +++ b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/AdvancedContextMenuManager.swift @@ -1,13 +1,13 @@ // // AdvancedContextMenuManager.swift -// +// // // Created by Stanislav Jelezoglo on 23.06.2023. // +import CommonKit import SwiftUI import UIKit -import CommonKit @MainActor public final class AdvancedContextMenuManager: NSObject { @@ -18,13 +18,13 @@ public final class AdvancedContextMenuManager: NSObject { private var locationOnScreen: CGPoint = .zero private var getPositionOnScreen: (() -> CGPoint)? private var messageId: String = "" - + public var didAppearMenuAction: ((_ messageId: String) -> Void)? public var didPresentMenuAction: ((_ messageId: String) -> Void)? public var didDismissMenuAction: ((_ messageId: String) -> Void)? - + // MARK: Public - + public func presentMenu( arg: ChatContextMenuArguments, upperView: AnyView?, @@ -33,14 +33,14 @@ public final class AdvancedContextMenuManager: NSObject { self.messageId = arg.messageId self.locationOnScreen = arg.location self.getPositionOnScreen = arg.getPositionOnScreen - + let containerCopyView = ContanierPreviewView( contentView: arg.copyView, scale: 1.0, size: arg.size, animationInDuration: animationOutDuration ) - + guard !isMacOS else { presentOverlayForMac( contentView: containerCopyView, @@ -53,9 +53,9 @@ public final class AdvancedContextMenuManager: NSObject { ) return } - + let menuVC = getMenuVC(content: arg.menu) - + self.presentOverlay( view: containerCopyView, location: arg.location, @@ -65,13 +65,13 @@ public final class AdvancedContextMenuManager: NSObject { upperViewSize: upperViewSize ) } - + @MainActor public func dismiss() async { await viewModel?.dismiss() await viewModelMac?.dismiss() } - + @MainActor public func presentOver(_ vc: UIViewController, animated: Bool) { vcOverlay?.present(vc, animated: animated) @@ -80,7 +80,7 @@ public final class AdvancedContextMenuManager: NSObject { // MARK: Private -private extension AdvancedContextMenuManager { +extension AdvancedContextMenuManager { private func presentOverlay( view: UIView, location: CGPoint, @@ -99,26 +99,26 @@ private extension AdvancedContextMenuManager { animationDuration: animationOutDuration ) viewModel.delegate = self - + let overlay = ContextMenuOverlayView( viewModel: viewModel ) - + let overlayVC = OverlayHostingController( rootView: overlay, dismissAction: { [weak self] in self?.dismissSync() } ) - + overlayVC.modalPresentationStyle = .overFullScreen overlayVC.modalPresentationCapturesStatusBarAppearance = true overlayVC.view.backgroundColor = .clear - + self.viewModel = viewModel - + present(vc: overlayVC) } - - func presentOverlayForMac( + + fileprivate func presentOverlayForMac( contentView: UIView, contentViewSize: CGSize, location: CGPoint, @@ -128,7 +128,7 @@ private extension AdvancedContextMenuManager { upperViewSize: CGSize ) { let menuVC = getMenuVC(content: menu) - + let viewModel = ContextMenuOverlayViewModelMac( contentView: contentView, contentViewSize: contentViewSize, @@ -140,37 +140,37 @@ private extension AdvancedContextMenuManager { animationDuration: animationOutDuration, delegate: self ) - + let overlay = ContextMenuOverlayViewMac( viewModel: viewModel ) - + let overlayVC = OverlayHostingController( rootView: overlay, dismissAction: { [weak self] in self?.dismissSync() } ) - + overlayVC.modalPresentationStyle = .overFullScreen overlayVC.modalPresentationCapturesStatusBarAppearance = true overlayVC.view.backgroundColor = .clear - + viewModelMac = viewModel - + present(vc: overlayVC) } - - func present(vc: UIViewController) { + + fileprivate func present(vc: UIViewController) { window.rootViewController = vc window.makeKeyAndVisible() - + UIImpactFeedbackGenerator(style: .medium).impactOccurred() didPresentMenuAction?(messageId) vcOverlay = vc } - - func getMenuVC(content: AMenuSection) -> AMenuViewController { + + fileprivate func getMenuVC(content: AMenuSection) -> AMenuViewController { let menuViewController = AMenuViewController(menuContent: content) - + menuViewController.finished = { [weak self] action in guard let self = self else { return } Task { @@ -178,27 +178,27 @@ private extension AdvancedContextMenuManager { action?() } } - + return menuViewController } - - func dismissSync() { + + fileprivate func dismissSync() { Task { await dismiss() } } } // MARK: Delegate - + extension AdvancedContextMenuManager: OverlayViewDelegate { func willDissmis() { guard let newPosition = getPositionOnScreen?(), newPosition != locationOnScreen else { return } - + viewModel?.update(locationOnScreen: newPosition) } - + func didDissmis() { didDismissMenuAction?(messageId) getPositionOnScreen = nil @@ -210,13 +210,14 @@ extension AdvancedContextMenuManager: OverlayViewDelegate { window.isHidden = true } } - + func didAppear() { if let newPosition = getPositionOnScreen?(), - newPosition != locationOnScreen { + newPosition != locationOnScreen + { viewModel?.update(locationOnScreen: newPosition) } - + didAppearMenuAction?(messageId) } } diff --git a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/ContextMenuOverlayView.swift b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/ContextMenuOverlayView.swift index 9b8624de0..54a784027 100644 --- a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/ContextMenuOverlayView.swift +++ b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/ContextMenuOverlayView.swift @@ -1,28 +1,28 @@ // // ContextMenuOverlayView.swift -// +// // // Created by Stanislav Jelezoglo on 23.06.2023. // -import SwiftUI import CommonKit +import SwiftUI struct ContextMenuOverlayView: View { @StateObject private var viewModel: ContextMenuOverlayViewModel - + init(viewModel: ContextMenuOverlayViewModel) { _viewModel = StateObject(wrappedValue: viewModel) } - + var backgroundBlur: Blur { Blur(style: .systemUltraThinMaterialDark, sensetivity: 0.5) } - + var axes: Axis.Set { return viewModel.shouldScroll ? .vertical : [] } - + var menuTransition: AnyTransition { AnyTransition.asymmetric( insertion: .scale(scale: 0, anchor: .top), @@ -31,14 +31,14 @@ struct ContextMenuOverlayView: View { ) ) } - + var body: some View { ZStack { if viewModel.additionalMenuVisible { backgroundBlur .zIndex(0) .ignoresSafeArea() - + if let upperContentView = viewModel.upperContentView { makeUpperOverlayView(upperContentView: upperContentView) .zIndex(2) @@ -68,21 +68,21 @@ struct ContextMenuOverlayView: View { } } -private extension ContextMenuOverlayView { - func makeOverlayView() -> some View { +extension ContextMenuOverlayView { + fileprivate func makeOverlayView() -> some View { makeOverlayScrollToBottom(makeOverlayScrollView()) } - - func makeOverlayScrollView() -> some View { + + fileprivate func makeOverlayScrollView() -> some View { ScrollView(axes, showsIndicators: false) { VStack(spacing: .zero) { makeContentView() - .onTapGesture { } + .onTapGesture {} Spacer() .frame( height: viewModel.menuSize.height - + minBottomOffset - + minContentsSpace + + minBottomOffset + + minContentsSpace ) } .id(1) @@ -90,36 +90,38 @@ private extension ContextMenuOverlayView { .fullScreen() .transition(.opacity) } - - func makeOverlayScrollToBottom(_ content: some View) -> some View { + + fileprivate func makeOverlayScrollToBottom(_ content: some View) -> some View { if #available(iOS 17.0, *) { - return content + return + content .defaultScrollAnchor(.bottom) } - + return ScrollViewReader { value in content .onChange(of: viewModel.scrollToEnd) { scrollToBottom in guard scrollToBottom else { return } - + withAnimation { value.scrollTo(1, anchor: .bottom) } } } } - - func makeContentView() -> some View { + + fileprivate func makeContentView() -> some View { HStack { UIViewWrapper(view: viewModel.contentView) .frame( width: viewModel.contentViewSize.width, height: viewModel.contentViewSize.height ) - .padding(.top, - viewModel.additionalMenuVisible - ? viewModel.contentViewLocation.y - : viewModel.startOffsetForContentView + .padding( + .top, + viewModel.additionalMenuVisible + ? viewModel.contentViewLocation.y + : viewModel.startOffsetForContentView ) .padding(.leading, viewModel.contentViewLocation.x) Spacer() @@ -127,21 +129,22 @@ private extension ContextMenuOverlayView { .fullScreen() .transition(.opacity) } - - func makeMenuOverlayView() -> some View { + + fileprivate func makeMenuOverlayView() -> some View { VStack { makeMenuView() - .onTapGesture { } + .onTapGesture {} Spacer() } .fullScreen() .transition(.opacity) } - - func makeMenuView() -> some View { + + fileprivate func makeMenuView() -> some View { HStack { if viewModel.additionalMenuVisible, - let menuVC = viewModel.menu { + let menuVC = viewModel.menu + { UIViewControllerWrapper(menuVC) .frame(width: menuVC.menuSize.width, height: menuVC.menuSize.height) .cornerRadius(15) @@ -157,35 +160,36 @@ private extension ContextMenuOverlayView { .offset(y: viewModel.menuLocation.y) .ignoresSafeArea() } - - func makeUpperOverlayView(upperContentView: some View) -> some View { + + fileprivate func makeUpperOverlayView(upperContentView: some View) -> some View { VStack { makeUpperContentView(upperContentView: upperContentView) - .onTapGesture { } + .onTapGesture {} Spacer() } .fullScreen() .transition(.opacity) } - - func makeUpperContentView(upperContentView: some View) -> some View { + + fileprivate func makeUpperContentView(upperContentView: some View) -> some View { HStack { upperContentView .frame( width: viewModel.upperContentSize.width, height: viewModel.upperContentSize.height ) - .padding(.top, - viewModel.additionalMenuVisible - ? viewModel.upperContentViewLocation.y - : viewModel.startOffsetForUpperContentView + .padding( + .top, + viewModel.additionalMenuVisible + ? viewModel.upperContentViewLocation.y + : viewModel.startOffsetForUpperContentView ) .padding(.leading, viewModel.upperContentViewLocation.x) Spacer() } .fullScreen() } - + } private let minBottomOffset: CGFloat = 50 diff --git a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/ContextMenuOverlayViewModel.swift b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/ContextMenuOverlayViewModel.swift index 721e2bf6d..236170167 100644 --- a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/ContextMenuOverlayViewModel.swift +++ b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/ContextMenuOverlayViewModel.swift @@ -1,12 +1,12 @@ // // ContextMenuOverlayViewModel.swift -// +// // // Created by Stanislav Jelezoglo on 07.07.2023. // -import SwiftUI import CommonKit +import SwiftUI @MainActor final class ContextMenuOverlayViewModel: ObservableObject { @@ -17,26 +17,26 @@ final class ContextMenuOverlayViewModel: ObservableObject { let upperContentView: AnyView? let upperContentSize: CGSize let animationDuration: TimeInterval - + var upperContentViewLocation: CGPoint = .zero var contentViewLocation: CGPoint = .zero var menuLocation: CGPoint = .zero var startOffsetForContentView: CGFloat = .zero - + var startOffsetForUpperContentView: CGFloat { locationOnScreen.y - (upperContentSize.height + minContentsSpace) } - + var menuSize: CGSize { menu?.menuSize ?? .init(width: 250, height: 300) } - + weak var delegate: OverlayViewDelegate? - + @Published var additionalMenuVisible = false @Published var shouldScroll: Bool = false @Published var scrollToEnd = false - + init( contentView: UIView, contentViewSize: CGSize, @@ -53,116 +53,120 @@ final class ContextMenuOverlayViewModel: ObservableObject { self.upperContentView = upperContentView self.upperContentSize = upperContentSize self.animationDuration = animationDuration - + startOffsetForContentView = locationOnScreen.y contentViewLocation = calculateContentViewLocation() menuLocation = calculateMenuLocation() upperContentViewLocation = calculateUpperContentViewLocation() shouldScroll = shoudScroll() } - + @MainActor func dismiss() async { delegate?.willDissmis() - + await animate(duration: animationDuration) { self.additionalMenuVisible.toggle() } - + delegate?.didDissmis() } - + func update(locationOnScreen: CGPoint) { startOffsetForContentView = locationOnScreen.y } } -private extension ContextMenuOverlayViewModel { - func calculateContentViewLocation() -> CGPoint { +extension ContextMenuOverlayViewModel { + fileprivate func calculateContentViewLocation() -> CGPoint { .init( x: locationOnScreen.x, y: calculateOffsetForContentView() ) } - - func calculateUpperContentViewLocation() -> CGPoint { + + fileprivate func calculateUpperContentViewLocation() -> CGPoint { .init( x: isNeedToMoveFromTrailing() - ? calculateLeadingOffset(for: upperContentSize.width) - : locationOnScreen.x, + ? calculateLeadingOffset(for: upperContentSize.width) + : locationOnScreen.x, y: calculateOffsetForUpperContentView() ) } - - func calculateMenuLocation() -> CGPoint { + + fileprivate func calculateMenuLocation() -> CGPoint { .init( x: isNeedToMoveFromTrailing() - ? calculateLeadingOffset(for: menuSize.width) - : locationOnScreen.x, + ? calculateLeadingOffset(for: menuSize.width) + : locationOnScreen.x, y: calculateMenuTopOffset() ) } - - func calculateMenuTopOffset() -> CGFloat { - let offset = calculateOffsetForContentView() - + contentViewSize.height - + minContentsSpace - + + fileprivate func calculateMenuTopOffset() -> CGFloat { + let offset = + calculateOffsetForContentView() + + contentViewSize.height + + minContentsSpace + if !shoudScroll() { return offset > UIScreen.main.bounds.height - ? UIScreen.main.bounds.height - - menuSize.height - - minBottomOffset - : offset + ? UIScreen.main.bounds.height + - menuSize.height + - minBottomOffset + : offset } - + return UIScreen.main.bounds.height - menuSize.height - minBottomOffset } - - func calculateLeadingOffset(for width: CGFloat) -> CGFloat { + + fileprivate func calculateLeadingOffset(for width: CGFloat) -> CGFloat { (locationOnScreen.x + contentViewSize.width) - width } - - func isNeedToMoveFromTrailing() -> Bool { - let maxSize = menuSize.width > upperContentSize.width - ? menuSize - : upperContentSize - + + fileprivate func isNeedToMoveFromTrailing() -> Bool { + let maxSize = + menuSize.width > upperContentSize.width + ? menuSize + : upperContentSize + guard calculateLeadingOffset(for: maxSize.width) > .zero else { return false } - + let sum = locationOnScreen.x + maxSize.width + minBottomOffset - + return UIScreen.main.bounds.width < sum } - - func calculateOffsetForUpperContentView() -> CGFloat { - let offset = calculateOffsetForContentView() - - (upperContentSize.height + minContentsSpace) - + + fileprivate func calculateOffsetForUpperContentView() -> CGFloat { + let offset = + calculateOffsetForContentView() + - (upperContentSize.height + minContentsSpace) + return offset < .zero - ? minBottomOffset - : offset + ? minBottomOffset + : offset } - - func calculateOffsetForContentView() -> CGFloat { + + fileprivate func calculateOffsetForContentView() -> CGFloat { guard !shoudScroll() else { return minBottomOffset + upperContentSize.height + minContentsSpace } - + if isNeedToMoveFromBottom( for: locationOnScreen.y + contentViewSize.height ) { return getOffsetToMoveFromBottom() } - + if isNeedToMoveFromTop() { return getOffsetToMoveFromTop() } - + return locationOnScreen.y } - - func shoudScroll() -> Bool { - guard contentViewSize.height + + fileprivate func shoudScroll() -> Bool { + guard + contentViewSize.height + menuSize.height + minBottomOffset * 2 + upperContentSize.height @@ -171,31 +175,31 @@ private extension ContextMenuOverlayViewModel { else { return true } - + return false } - - func isNeedToMoveFromTop() -> Bool { + + fileprivate func isNeedToMoveFromTop() -> Bool { locationOnScreen.y - minContentsSpace - upperContentSize.height < minBottomOffset } - - func getOffsetToMoveFromTop() -> CGFloat { + + fileprivate func getOffsetToMoveFromTop() -> CGFloat { minContentsSpace - + upperContentSize.height - + minBottomOffset + + upperContentSize.height + + minBottomOffset } - - func isNeedToMoveFromBottom(for bottomPosition: CGFloat) -> Bool { + + fileprivate func isNeedToMoveFromBottom(for bottomPosition: CGFloat) -> Bool { UIScreen.main.bounds.height - bottomPosition < (menuSize.height + minBottomOffset) } - - func getOffsetToMoveFromBottom() -> CGFloat { + + fileprivate func getOffsetToMoveFromBottom() -> CGFloat { UIScreen.main.bounds.height - - menuSize.height - - contentViewSize.height - - minBottomOffset + - menuSize.height + - contentViewSize.height + - minBottomOffset } - + } private let estimateMenuRowHeight: CGFloat = 50 diff --git a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/MacOSOverlay/ContextMenuOverlayViewMac.swift b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/MacOSOverlay/ContextMenuOverlayViewMac.swift index 1a7637933..06883de6b 100644 --- a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/MacOSOverlay/ContextMenuOverlayViewMac.swift +++ b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/MacOSOverlay/ContextMenuOverlayViewMac.swift @@ -1,20 +1,20 @@ // // ContextMenuOverlayViewMac.swift -// +// // // Created by Stanislav Jelezoglo on 13.07.2023. // -import SwiftUI import CommonKit +import SwiftUI struct ContextMenuOverlayViewMac: View { @StateObject private var viewModel: ContextMenuOverlayViewModelMac - + init(viewModel: ContextMenuOverlayViewModelMac) { _viewModel = StateObject(wrappedValue: viewModel) } - + var body: some View { GeometryReader { geometry in makeStackView(geometry: geometry) @@ -22,25 +22,28 @@ struct ContextMenuOverlayViewMac: View { } } -private extension ContextMenuOverlayViewMac { - func makeStackView(geometry: GeometryProxy) -> some View { +extension ContextMenuOverlayViewMac { + fileprivate func makeStackView(geometry: GeometryProxy) -> some View { ZStack { viewModel.updateLocations(geometry: geometry) - - Button(action: { - Task { - await viewModel.dismiss() - } - }, label: { - if viewModel.additionalMenuVisible { - Color.init(uiColor: .adamant.contextMenuOverlayMacColor) - } else { - Color.clear + + Button( + action: { + Task { + await viewModel.dismiss() + } + }, + label: { + if viewModel.additionalMenuVisible { + Color.init(uiColor: .adamant.contextMenuOverlayMacColor) + } else { + Color.clear + } } - }) - + ) + makeContentOverlayView() - + if viewModel.additionalMenuVisible { if let upperContentView = viewModel.upperContentView { makeUpperOverlayView(upperContentView: upperContentView) @@ -59,19 +62,19 @@ private extension ContextMenuOverlayViewMac { } } - func makeOverlayView() -> some View { + fileprivate func makeOverlayView() -> some View { VStack(spacing: 10) { if viewModel.additionalMenuVisible { makeMenuView() - .onTapGesture { } + .onTapGesture {} } Spacer() } .fullScreen() .transition(.opacity) } - - func makeContentOverlayView() -> some View { + + fileprivate func makeContentOverlayView() -> some View { VStack(spacing: 10) { makeContentView() Spacer() @@ -79,8 +82,8 @@ private extension ContextMenuOverlayViewMac { .fullScreen() .transition(.opacity) } - - func makeContentView() -> some View { + + fileprivate func makeContentView() -> some View { HStack { UIViewWrapper(view: viewModel.contentView) .frame( @@ -94,8 +97,8 @@ private extension ContextMenuOverlayViewMac { .fullScreen() .transition(.opacity) } - - func makeMenuView() -> some View { + + fileprivate func makeMenuView() -> some View { HStack { if let menuVC = viewModel.menu { UIViewControllerWrapper(menuVC) @@ -110,18 +113,18 @@ private extension ContextMenuOverlayViewMac { .fullScreen() .transition(.opacity) } - - func makeUpperOverlayView(upperContentView: some View) -> some View { + + fileprivate func makeUpperOverlayView(upperContentView: some View) -> some View { VStack { makeUpperContentView(upperContentView: upperContentView) - .onTapGesture { } + .onTapGesture {} Spacer() } .fullScreen() .transition(.opacity) } - - func makeUpperContentView(upperContentView: some View) -> some View { + + fileprivate func makeUpperContentView(upperContentView: some View) -> some View { HStack { upperContentView .frame( diff --git a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/MacOSOverlay/ContextMenuOverlayViewModelMac.swift b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/MacOSOverlay/ContextMenuOverlayViewModelMac.swift index 603d80f38..60af225c6 100644 --- a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/MacOSOverlay/ContextMenuOverlayViewModelMac.swift +++ b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/MacOSOverlay/ContextMenuOverlayViewModelMac.swift @@ -1,13 +1,13 @@ // // File.swift -// +// // // Created by Stanislav Jelezoglo on 13.07.2023. // +import CommonKit import Foundation import SwiftUI -import CommonKit @MainActor final class ContextMenuOverlayViewModelMac: ObservableObject { @@ -19,23 +19,23 @@ final class ContextMenuOverlayViewModelMac: ObservableObject { var locationOnScreen: CGPoint var contentLocation: CGPoint let animationDuration: TimeInterval - + @Published var additionalMenuVisible = false - + var menuSize: CGSize { menu?.menuSize ?? .init(width: 250, height: 300) } - + var upperContentViewLocation: CGPoint = .zero var menuLocation: CGPoint = .zero var finalOffsetForUpperContentView: CGFloat = .zero - + weak var delegate: OverlayViewDelegate? - + private var screenSize: CGSize = UIScreen.main.bounds.size // MARK: Init - + init( contentView: UIView, contentViewSize: CGSize, @@ -56,19 +56,19 @@ final class ContextMenuOverlayViewModelMac: ObservableObject { self.delegate = delegate self.contentLocation = contentLocation self.animationDuration = animationDuration - + menuLocation = calculateMenuLocation() upperContentViewLocation = calculateUpperContentViewLocation() } - + @MainActor func dismiss() async { await animate(duration: animationDuration) { self.additionalMenuVisible.toggle() } - + delegate?.didDissmis() } - + func updateLocations(geometry: GeometryProxy) -> EmptyView { screenSize = geometry.size menuLocation = calculateMenuLocation() @@ -77,56 +77,56 @@ final class ContextMenuOverlayViewModelMac: ObservableObject { } } -private extension ContextMenuOverlayViewModelMac { - func calculateUpperContentViewLocation() -> CGPoint { +extension ContextMenuOverlayViewModelMac { + fileprivate func calculateUpperContentViewLocation() -> CGPoint { .init( x: calculateLeadingOffset(for: upperContentSize.width), y: calculateUpperContentTopOffset() ) } - - func calculateMenuLocation() -> CGPoint { + + fileprivate func calculateMenuLocation() -> CGPoint { .init( x: calculateLeadingOffset(for: menuSize.width), y: calculateMenuTopOffset() ) } - - func calculateUpperContentTopOffset() -> CGFloat { + + fileprivate func calculateUpperContentTopOffset() -> CGFloat { guard isNeedToMoveFromBottom() else { return locationOnScreen.y - - upperContentSize.height - - minContentsSpace + - upperContentSize.height + - minContentsSpace } - + let location = screenSize.height - menuSize.height - minBottomOffset - + return location - - upperContentSize.height - - minContentsSpace + - upperContentSize.height + - minContentsSpace } - - func calculateMenuTopOffset() -> CGFloat { + + fileprivate func calculateMenuTopOffset() -> CGFloat { guard isNeedToMoveFromBottom() else { return locationOnScreen.y } - + return screenSize.height - menuSize.height - minBottomOffset } - - func calculateLeadingOffset(for width: CGFloat) -> CGFloat { + + fileprivate func calculateLeadingOffset(for width: CGFloat) -> CGFloat { guard isNeedToMoveFromTrailing() else { return locationOnScreen.x } - + return locationOnScreen.x - width } - - func isNeedToMoveFromTrailing() -> Bool { + + fileprivate func isNeedToMoveFromTrailing() -> Bool { screenSize.width < locationOnScreen.x + upperContentSize.width + minBottomOffset } - func isNeedToMoveFromBottom() -> Bool { + fileprivate func isNeedToMoveFromBottom() -> Bool { screenSize.height < locationOnScreen.y + menuSize.height + minBottomOffset } } diff --git a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/OverlayViewDelegate.swift b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/OverlayViewDelegate.swift index 00eb7c087..6861d5cc0 100644 --- a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/OverlayViewDelegate.swift +++ b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/OverlayViewDelegate.swift @@ -1,6 +1,6 @@ // // OverlayViewDelegate.swift -// +// // // Created by Stanislav Jelezoglo on 01.08.2023. // diff --git a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/ContanierPreviewView.swift b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/ContanierPreviewView.swift index 39a1af4c1..1682016bf 100644 --- a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/ContanierPreviewView.swift +++ b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/ContanierPreviewView.swift @@ -1,23 +1,23 @@ // // ContanierPreviewView.swift -// +// // // Created by Stanislav Jelezoglo on 04.08.2023. // -import UIKit import SnapKit +import UIKit @MainActor final class ContanierPreviewView: UIView { private let contentView: UIView private let animationInDuration: TimeInterval private let _size: CGSize - + override var intrinsicContentSize: CGSize { _size } - + init( contentView: UIView, scale: CGFloat, @@ -30,23 +30,23 @@ final class ContanierPreviewView: UIView { super.init(frame: .zero) self.contentView.clipsToBounds = true self.contentView.transform = .init(scaleX: scale, y: scale) - + backgroundColor = .clear addSubview(self.contentView) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + override func layoutSubviews() { super.layoutSubviews() self.contentView.frame = .init(origin: .zero, size: intrinsicContentSize) } - + override func didMoveToSuperview() { super.didMoveToSuperview() - + UIView.animate(withDuration: animationInDuration) { self.contentView.transform = .identity } diff --git a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/Menu/Components/AMenuRowCell.swift b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/Menu/Components/AMenuRowCell.swift index 921a81415..1d3ceeb54 100644 --- a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/Menu/Components/AMenuRowCell.swift +++ b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/Menu/Components/AMenuRowCell.swift @@ -5,71 +5,71 @@ // Created by Stanislav Jelezoglo on 26.07.2023. // -import UIKit -import SnapKit import CommonKit +import SnapKit +import UIKit -final class AMenuRowCell: UITableViewCell { +final class AMenuRowCell: UITableViewCell { lazy var titleLabel = UILabel() lazy var iconImage = UIImageView(image: nil) lazy var backgroundColorView = UIView() lazy var backgroundDarkeningView = UIView() lazy var lineView = UIView() - + // MARK: Proprieties - + enum RowPosition { case top case bottom case other } - + private var rowBackgroundColor: UIColor? - + // MARK: Init - + required override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupView() } - + required init?(coder: NSCoder) { super.init(coder: coder) setupView() } - + func setupView() { contentView.addSubview(backgroundColorView) contentView.addSubview(backgroundDarkeningView) contentView.addSubview(titleLabel) contentView.addSubview(iconImage) contentView.addSubview(lineView) - + backgroundColorView.snp.makeConstraints { make in make.directionalEdges.equalToSuperview() } - + backgroundDarkeningView.snp.makeConstraints { make in make.directionalEdges.equalToSuperview() } - + titleLabel.snp.makeConstraints { make in make.centerY.equalToSuperview() make.leading.equalToSuperview().inset(20) } - + iconImage.snp.makeConstraints { make in make.centerY.equalToSuperview() make.trailing.equalToSuperview().inset(20) make.width.height.equalTo(25) } - + lineView.snp.makeConstraints { make in make.bottom.leading.trailing.equalToSuperview() make.height.equalTo(0.5) } } - + func configure( with menuItem: AMenuItem, accentColor: UIColor?, @@ -78,11 +78,11 @@ final class AMenuRowCell: UITableViewCell { rowPosition: RowPosition ) { selectionStyle = .none - + titleLabel.text = menuItem.name iconImage.image = menuItem.iconImage backgroundColorView.backgroundColor = backgroundColor - + menuItem.style.configure( titleLabel: titleLabel, icon: iconImage, @@ -90,10 +90,10 @@ final class AMenuRowCell: UITableViewCell { menuAccentColor: accentColor, menuFont: font ) - + rowBackgroundColor = backgroundColorView.backgroundColor backgroundColorView.backgroundColor = rowBackgroundColor - + switch rowPosition { case .top, .other: lineView.backgroundColor = .adamant.contextMenuLineColor @@ -101,16 +101,16 @@ final class AMenuRowCell: UITableViewCell { lineView.backgroundColor = .clear } } - + func select() { guard let rowBackgroundColor = rowBackgroundColor else { return } - + backgroundDarkeningView.backgroundColor = .adamant.contextMenuSelectColor backgroundColorView.backgroundColor = rowBackgroundColor.withAlphaComponent(0.3) } - + func deselect() { backgroundDarkeningView.backgroundColor = .clear backgroundColorView.backgroundColor = rowBackgroundColor diff --git a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/Menu/Components/AMenuViewController.swift b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/Menu/Components/AMenuViewController.swift index ffb413061..676945745 100644 --- a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/Menu/Components/AMenuViewController.swift +++ b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/Menu/Components/AMenuViewController.swift @@ -5,91 +5,91 @@ // Created by Stanislav Jelezoglo on 25.07.2023. // -import UIKit -import SnapKit import CommonKit +import SnapKit +import UIKit @MainActor final class AMenuViewController: UIViewController { - + private lazy var tableView: UITableView = { let tableView = UITableView() - + if #available(iOS 15.0, *) { tableView.sectionHeaderTopPadding = 0 } - + tableView.layer.cornerRadius = 15 tableView.separatorStyle = .none tableView.delegate = self tableView.dataSource = self tableView.isScrollEnabled = false tableView.register(AMenuRowCell.self, forCellReuseIdentifier: String(describing: AMenuRowCell.self)) - + return tableView }() - + // MARK: Proprieties - + let menuContent: AMenuSection var finished: (((() -> Void)?) -> Void)? - + private var done = false private var selectedItem: IndexPath? private let rowHeight = 40 - + private let font = UIFont.systemFont(ofSize: 17) private let dragSensitivity: CGFloat = 250 - + var menuSize: CGSize { let items = menuContent.menuItems.count let height = items * rowHeight return CGSize(width: 250, height: height) } - + // MARK: Init - + init(menuContent: AMenuSection) { self.menuContent = menuContent - + super.init(nibName: nil, bundle: nil) } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - + // MARK: LifeCycle - + override func viewDidLoad() { super.viewDidLoad() view.layer.cornerRadius = 15 - + setupGestures() setupView() } - + private func setupView() { view.addSubview(tableView) - + tableView.snp.makeConstraints { make in make.directionalEdges.equalToSuperview() } } - + private func setupGestures() { let panGestureRecognizer = UIPanGestureRecognizer( target: self, action: #selector(userPanned(_:)) ) view.addGestureRecognizer(panGestureRecognizer) - + let tapGestureRecognizer = UITapGestureRecognizer( target: self, action: #selector(userTapped(_:)) ) view.addGestureRecognizer(tapGestureRecognizer) - + let hoverGestureRecognizer = UIHoverGestureRecognizer( target: self, action: #selector(self.hoverGesture(_:)) @@ -100,24 +100,24 @@ final class AMenuViewController: UIViewController { // MARK: - Private -private extension AMenuViewController { +extension AMenuViewController { @objc private func hoverGesture(_ gestureRecognizer: UIHoverGestureRecognizer) { let touchLocation = gestureRecognizer.location(in: self.view) let indexPath = self.indexPath(forRowAtPoint: touchLocation) - + switch gestureRecognizer.state { case .began: guard let indexPath = indexPath else { return } - + selectRow(at: indexPath) case .changed: guard !done else { selectRow(at: nil) return } - + selectRow(at: indexPath) case .ended: selectRow(at: nil) @@ -125,23 +125,23 @@ private extension AMenuViewController { selectRow(at: nil) } } - - @objc func userTapped(_ gestureRecognizer: UIPanGestureRecognizer) { + + @objc fileprivate func userTapped(_ gestureRecognizer: UIPanGestureRecognizer) { let touchLocation = gestureRecognizer.location(in: view) guard gestureRecognizer.state == .ended, - let indexPath = self.indexPath(forRowAtPoint: touchLocation) + let indexPath = self.indexPath(forRowAtPoint: touchLocation) else { return } - + selectRow(at: indexPath) let menuItem = menuContent.menuItems[indexPath.row] menuItemWasTapped(menuItem) } - - @objc func userPanned(_ gestureRecognizer: UIPanGestureRecognizer) { + + @objc fileprivate func userPanned(_ gestureRecognizer: UIPanGestureRecognizer) { let touchLocation = gestureRecognizer.location(in: view) let indexPath = self.indexPath(forRowAtPoint: touchLocation) - + switch gestureRecognizer.state { case .began: guard let indexPath = indexPath else { @@ -151,43 +151,43 @@ private extension AMenuViewController { } return } - + selectRow(at: indexPath) case .changed: guard !done else { selectRow(at: nil) return } - + selectRow(at: indexPath) case .ended: selectRow(at: nil) - + guard !done, - let indexPath = indexPath, - indexPath.row > -1, - gestureRecognizer.velocity( + let indexPath = indexPath, + indexPath.row > -1, + gestureRecognizer.velocity( in: tableView - ).magnitude < dragSensitivity + ).magnitude < dragSensitivity else { return } - + let menuItem = menuContent.menuItems[indexPath.row] menuItemWasTapped(menuItem) - + default: selectRow(at: nil) } } - - func menuItemWasTapped( + + fileprivate func menuItemWasTapped( _ menuItem: AMenuItem ) { finished?(menuItem.action) } - - func selectRow(at indexPath: IndexPath?) { + + fileprivate func selectRow(at indexPath: IndexPath?) { if let selectedItem = selectedItem, selectedItem != indexPath { if let cell = tableView.cellForRow(at: selectedItem) as? AMenuRowCell { cell.deselect() @@ -201,15 +201,15 @@ private extension AMenuViewController { UIImpactFeedbackGenerator(style: .light).impactOccurred(intensity: 0.8) } } - + if indexPath?.row == -1 { selectedItem = nil } else { selectedItem = indexPath } } - - func indexPath(forRowAtPoint point: CGPoint) -> IndexPath? { + + fileprivate func indexPath(forRowAtPoint point: CGPoint) -> IndexPath? { tableView.indexPathForRow(at: point) } } @@ -222,18 +222,19 @@ extension AMenuViewController: UITableViewDelegate, UITableViewDataSource { ) -> Int { return menuContent.menuItems.count } - + func tableView( _ tableView: UITableView, cellForRowAt indexPath: IndexPath ) -> UITableViewCell { - let cell = tableView.dequeueReusableCell( - withIdentifier: String(describing: AMenuRowCell.self), - for: indexPath - ) as! AMenuRowCell - + let cell = + tableView.dequeueReusableCell( + withIdentifier: String(describing: AMenuRowCell.self), + for: indexPath + ) as! AMenuRowCell + let rowPosition: AMenuRowCell.RowPosition - + if indexPath.row == menuContent.menuItems.count - 1 { rowPosition = .bottom } else if indexPath.row == .zero { @@ -241,9 +242,9 @@ extension AMenuViewController: UITableViewDelegate, UITableViewDataSource { } else { rowPosition = .other } - + let menuItem = menuContent.menuItems[indexPath.row] - + cell.configure( with: menuItem, accentColor: .adamant.textColor, @@ -251,14 +252,14 @@ extension AMenuViewController: UITableViewDelegate, UITableViewDataSource { font: font, rowPosition: rowPosition ) - + return cell } - + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { CGFloat(rowHeight) } - + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let menuItem = menuContent.menuItems[indexPath.row] menuItemWasTapped(menuItem) diff --git a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/Menu/Helpers/CGPoint+Extensions.swift b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/Menu/Helpers/CGPoint+Extensions.swift index 542fec8d2..62e0ece2a 100644 --- a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/Menu/Helpers/CGPoint+Extensions.swift +++ b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/Menu/Helpers/CGPoint+Extensions.swift @@ -1,6 +1,6 @@ // // CGPoint+Extensions.swift -// +// // // Created by Franklyn Weber on 10/03/2021. // @@ -8,17 +8,17 @@ import UIKit extension CGPoint { - + // though CGPoint isn't a vector, Apple uses it in places where a CGVector would be more appropriate public var magnitude: CGFloat { return sqrt(x * x + y * y) } - - public static func +(_ lhs: CGPoint, _ rhs: CGPoint) -> CGPoint { + + public static func + (_ lhs: CGPoint, _ rhs: CGPoint) -> CGPoint { return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y) } - - public static func -(_ lhs: CGPoint, _ rhs: CGPoint) -> CGPoint { + + public static func - (_ lhs: CGPoint, _ rhs: CGPoint) -> CGPoint { return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y) } } diff --git a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/OverlayHostingController.swift b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/OverlayHostingController.swift index 07d8a5853..46e678029 100644 --- a/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/OverlayHostingController.swift +++ b/pckg/AdvancedContextMenuKit/Sources/AdvancedContextMenuKit/Implementation/Views/OverlayHostingController.swift @@ -1,17 +1,17 @@ // // OverlayHostingController.swift -// +// // // Created by Andrey Golubenko on 24.08.2023. // -import SwiftUI import CommonKit +import SwiftUI @MainActor final class OverlayHostingController: UIHostingController { private let dismissAction: @MainActor () -> Void - + override var keyCommands: [UIKeyCommand]? { let commands = [ UIKeyCommand( @@ -23,18 +23,18 @@ final class OverlayHostingController: UIHostingController Void) { self.dismissAction = dismissAction super.init(rootView: rootView) } - + required dynamic init?(coder aDecoder: NSCoder) { self.dismissAction = {} super.init(coder: aDecoder) assertionFailure("init(coder:) has not been implemented") } - + @objc private func dismissOverlay() { dismissAction() } diff --git a/scripts/AdamantCLI/Makefile b/scripts/AdamantCLI/Makefile new file mode 100644 index 000000000..661f59b4f --- /dev/null +++ b/scripts/AdamantCLI/Makefile @@ -0,0 +1,3 @@ +build: + swift build --configuration release --build-path ./build + cp ./build/release/adamant-cli ./ || true \ No newline at end of file diff --git a/scripts/AdamantCLI/Package.resolved b/scripts/AdamantCLI/Package.resolved new file mode 100644 index 000000000..ae1feeefa --- /dev/null +++ b/scripts/AdamantCLI/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "b907355d434cc251a00660fd1d4f5ae6654fa21f4345fee2b5ab3627d8819393", + "pins" : [ + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "41982a3656a71c768319979febd796c6fd111d5c", + "version" : "1.5.0" + } + } + ], + "version" : 3 +} diff --git a/scripts/AdamantCLI/Package.swift b/scripts/AdamantCLI/Package.swift new file mode 100644 index 000000000..26f83f5f1 --- /dev/null +++ b/scripts/AdamantCLI/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "AdamantCLI", + dependencies: [ + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0") + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .executableTarget( + name: "adamant-cli", + dependencies: [ + .product(name: "ArgumentParser", package: "swift-argument-parser") + ] + ) + ] +) diff --git a/scripts/AdamantCLI/Sources/AdamantCLI/AdamantCLI.swift b/scripts/AdamantCLI/Sources/AdamantCLI/AdamantCLI.swift new file mode 100644 index 000000000..81db27d9e --- /dev/null +++ b/scripts/AdamantCLI/Sources/AdamantCLI/AdamantCLI.swift @@ -0,0 +1,19 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book +// +// Swift Argument Parser +// https://swiftpackageindex.com/apple/swift-argument-parser/documentation + +import ArgumentParser + +@main +struct AdamantCLI: AsyncParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Adamant-CLI Toolkit.", + subcommands: [Hooks.self, Format.self, Setup.self, Mocks.self] + ) + + mutating func run() async throws { + + } +} diff --git a/scripts/AdamantCLI/Sources/Format/Format.swift b/scripts/AdamantCLI/Sources/Format/Format.swift new file mode 100644 index 000000000..1c36d6492 --- /dev/null +++ b/scripts/AdamantCLI/Sources/Format/Format.swift @@ -0,0 +1,102 @@ +// +// Format.swift +// AdamantCLI +// +// Created by Brian on 31/03/2025. +// + +import ArgumentParser +import Foundation + +struct Format: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Format Swift files in the project.", + usage: "adamant-cli format [] [ ...]", + discussion: """ + This script formats Swift files in the project using SwiftFormat. + Use 'all' to format all files in the project, excluding ignored folders. + Use 'staged' to format only staged Swift files in git. + """ + ) + + enum CodingKeys: CodingKey { + case type + case files + } + + let ignoredFolders: [String] = [ + "Pods" + ] + + enum RunType: String, ExpressibleByArgument { + case all + case staged + } + + @Argument( + help: + "Specify the type of run: 'all' to format all files, 'staged' to format only staged files." + ) + var type: RunType + + @Argument(help: "List of file paths to format when using 'staged' type.") + var files: [String] = [] + + func run() throws { + let configurationFilename = ".swiftformat" + let fileManager = FileManager.default + let currentDirectory = fileManager.currentDirectoryPath + let swiftFormatConfigPath = (currentDirectory as NSString).appendingPathComponent( + configurationFilename + ) + + guard fileManager.fileExists(atPath: swiftFormatConfigPath) else { + throw ValidationError( + "🚨 Error: .swiftformat configuration file not found in the current directory. Ensure you are running this script from the root of the project." + ) + } + + switch type { + case .all: + let directories = try fileManager.contentsOfDirectory(atPath: currentDirectory) + .filter { fileName in + var isDirectory: ObjCBool = false + let fullPath = (currentDirectory as NSString).appendingPathComponent(fileName) + return fileManager.fileExists(atPath: fullPath, isDirectory: &isDirectory) + && isDirectory.boolValue && !ignoredFolders.contains(fileName) + } + + let process = Process() + process.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun") + process.arguments = ["swift", "format", "--recursive", "--configuration", configurationFilename] + directories.flatMap { ["-i", $0] } + + try process.run() + process.waitUntilExit() + + if process.terminationStatus == 0 { + print("✅ Swift files formatted successfully.") + } else { + print("🚨 Failed to format Swift files. Exit code: \(process.terminationStatus)") + } + + case .staged: + guard !files.isEmpty else { + print("✅ No files specified for formatting.") + return + } + + let process = Process() + process.executableURL = URL(fileURLWithPath: "/usr/bin/xcrun") + process.arguments = ["swift", "format", "--configuration", configurationFilename] + files.flatMap { ["-i", $0] } + + try process.run() + process.waitUntilExit() + + if process.terminationStatus == 0 { + print("✅ Specified Swift files formatted successfully.") + } else { + print("🚨 Failed to format specified Swift files. Exit code: \(process.terminationStatus)") + } + } + } +} diff --git a/scripts/AdamantCLI/Sources/Hooks/Hooks.swift b/scripts/AdamantCLI/Sources/Hooks/Hooks.swift new file mode 100644 index 000000000..9930dba49 --- /dev/null +++ b/scripts/AdamantCLI/Sources/Hooks/Hooks.swift @@ -0,0 +1,90 @@ +// +// Hooks.swift +// AdamantCLI +// +// Created by Brian on 31/03/2025. +// + +import ArgumentParser +import Foundation + +public struct Hooks: ParsableCommand { + public static let configuration = CommandConfiguration( + abstract: "Git hook installer for Adamant-CLI Toolkit.", + usage: "adamant-cli hooks []", + discussion: """ + Use this command to install git hooks for the Adamant-CLI Toolkit. + Available hook types: + - formatter: Installs the pre-commit formatter hook. + - message: Installs the commit message validation hook. + Example: + adamant-cli hooks formatter + """ + ) + + enum Hook: String, ExpressibleByArgument { + case message + case formatter + + var sourceFilename: String { + switch self { + case .message: "commit-msg" + case .formatter: "pre-commit-formatter" + } + } + + var targetFilename: String { + switch self { + case .message: "commit-msg" + case .formatter: "pre-commit" + } + } + + var sourceFolder: String { ".githooks" } + var targetFolder: String { ".git/hooks" } + } + + @Argument(help: "Git hook type. Options are 'message' or 'formatter'. Default is 'formatter'.") + var hook: Hook + + public init() {} + + public mutating func run() throws { + try install(hook) + } + + private func install(_ hook: Hook) throws { + // Get the current working directory of the binary + let currentDirectory = FileManager.default.currentDirectoryPath + let githooksPath = "\(currentDirectory)" + + guard FileManager.default.fileExists(atPath: githooksPath) else { + throw ValidationError( + """ + 🚨 Invalid current directory. The command must be run from the directory where the .xcodeproj file is located, + which is typically the root of the project directory. + """ + ) + } + + // Define the source and destination paths + let sourcePath = "\(currentDirectory)/\(hook.sourceFolder)/\(hook.sourceFilename)" + let destinationPath = "\(currentDirectory)/\(hook.targetFolder)/\(hook.targetFilename)" + + do { + // Remove the destination file if it already exists + if FileManager.default.fileExists(atPath: destinationPath) { + try FileManager.default.removeItem(atPath: destinationPath) + } + + // Copy the file from source to destination + try FileManager.default.copyItem(atPath: sourcePath, toPath: destinationPath) + // Make the copied file executable + try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: destinationPath) + print("✅ Successfully copied \(sourcePath) to \(destinationPath)") + } catch { + print("🚨 Error: \(error.localizedDescription) at path: \(sourcePath)") + throw ValidationError(error.localizedDescription) + } + } +} diff --git a/scripts/AdamantCLI/Sources/Mocks/Mocks.swift b/scripts/AdamantCLI/Sources/Mocks/Mocks.swift new file mode 100644 index 000000000..232f4b334 --- /dev/null +++ b/scripts/AdamantCLI/Sources/Mocks/Mocks.swift @@ -0,0 +1,67 @@ +// +// Mocks.swift +// AdamantCLI +// +// Created by Brian on 01/04/2025. +// + +import ArgumentParser +import Foundation + +struct Mocks: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Generate mock files using Sourcery.", + usage: "adamant-cli mocks", + discussion: """ + This command generates mock files for your project using Sourcery. + Ensure Sourcery is installed and properly configured before running this command. + """ + ) + + func run() throws { + try checkSourceryInstallation() + try executeProcess( + launchPath: "/opt/homebrew/bin/sourcery", + arguments: ["--config", ".sourcery.yml"], + successMessage: "✅ Mocks are generated successfully.", + failureMessage: "Failed to generate mocks using Sourcery." + ) + } + + private func checkSourceryInstallation() throws { + try executeProcess( + launchPath: "/bin/bash", + arguments: ["-c", "command -v sourcery"], + successMessage: nil, + failureMessage: "🚨 Sourcery is not installed. Please install Sourcery first using the setup command." + ) + } + + private func executeProcess( + launchPath: String, + arguments: [String], + successMessage: String?, + failureMessage: String + ) throws { + let process = Process() + process.launchPath = launchPath + process.arguments = arguments + + let pipe = Pipe() + process.standardOutput = pipe + process.standardError = pipe + + process.launch() + process.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + if process.terminationStatus != 0 { + if let output = String(data: data, encoding: .utf8) { + print(output) + } + throw ValidationError(failureMessage) + } else if let successMessage = successMessage { + print(successMessage) + } + } +} diff --git a/scripts/AdamantCLI/Sources/Setup/Setup.swift b/scripts/AdamantCLI/Sources/Setup/Setup.swift new file mode 100644 index 000000000..e6c3750de --- /dev/null +++ b/scripts/AdamantCLI/Sources/Setup/Setup.swift @@ -0,0 +1,66 @@ +// +// Setup.swift +// AdamantCLI +// +// Created by Brian on 01/04/2025. +// + +import ArgumentParser +import Foundation + +struct Setup: ParsableCommand { + static let configuration = CommandConfiguration( + abstract: "Setup script for Adamant-CLI Toolkit.", + usage: "adamant-cli setup", + discussion: """ + Use this command to set up the Adamant-CLI Toolkit by installing necessary dependencies such as Sourcery and CocoaPods. + Ensure Homebrew is installed before running this command. + """ + ) + + func run() throws { + let brewInstallCommand = """ + if ! command -v brew &> /dev/null; then + echo "Homebrew is not installed. Please install Homebrew first." + exit 1 + fi + + echo "Installing Sourcery..." + brew install sourcery + + echo "Installing CocoaPods..." + brew install cocoapods + + echo "Installing xcbeautify..." + brew install xcbeautify + + echo 'import Foundation + enum AdamantSecret { + static let appIdentifierPrefix: String = "random.string" + static let keychainValuePassword: String = "random.string.two" + static let oldKeychainPass: String = "random.string.three" + }' > CommonKit/Sources/CommonKit/AdamantSecret.swift + """ + + let process = Process() + process.launchPath = "/bin/bash" + process.arguments = ["-c", brewInstallCommand] + + let pipe = Pipe() + process.standardOutput = pipe + process.standardError = pipe + + process.launch() + process.waitUntilExit() + + let data = pipe.fileHandleForReading.readDataToEndOfFile() + if process.terminationStatus != 0 { + if let output = String(data: data, encoding: .utf8) { + print(output) + } + throw ValidationError("Failed to install Sourcery or CocoaPods.") + } else { + print("✅ Installation successful.") + } + } +} diff --git a/scripts/init_hooks.sh b/scripts/init_hooks.sh deleted file mode 100644 index 0df418b77..000000000 --- a/scripts/init_hooks.sh +++ /dev/null @@ -1 +0,0 @@ -cp -R ./.githooks/commit-msg ./.git/hooks \ No newline at end of file