diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..063e3bfe6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +Adamant/Assets/adamant-core.js linguist-vendored diff --git a/Adamant.xcodeproj/project.pbxproj b/Adamant.xcodeproj/project.pbxproj index 50fa373c1..fd5688bdc 100644 --- a/Adamant.xcodeproj/project.pbxproj +++ b/Adamant.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 4411402421C8B290703B13EB /* Pods_Adamant.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C107D5CB65B4D728B9D97C0F /* Pods_Adamant.framework */; }; + 642361BE20E3869D0061559E /* eth_l18n.strings in Resources */ = {isa = PBXBuildFile; fileRef = 642361BD20E3869D0061559E /* eth_l18n.strings */; }; 643ED0B12109F4BD005A9FDA /* NativeAdamantCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643ED0B02109F4BD005A9FDA /* NativeAdamantCore.swift */; }; 644EC34D20EFA60900F40C73 /* AdamantApi+Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EC34C20EFA60900F40C73 /* AdamantApi+Delegates.swift */; }; 644EC34F20EFA77A00F40C73 /* Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 644EC34E20EFA77A00F40C73 /* Delegate.swift */; }; @@ -22,8 +23,16 @@ 645E7B062111DF3A006CC9FD /* Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 645E7B052111DF3A006CC9FD /* Crypto.swift */; }; 649E9A152111B3C200686B01 /* Mnemonic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649E9A142111B3C200686B01 /* Mnemonic.swift */; }; 64A223D620F760BB005157CB /* Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A223D520F760BB005157CB /* Localization.swift */; }; + 64A223D820F7A08E005157CB /* LskApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A223D720F7A08E005157CB /* LskApiService.swift */; }; + 64A223DA20F7A14B005157CB /* AdamantLskApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64A223D920F7A14B005157CB /* AdamantLskApiService.swift */; }; + 64BD2B7520E2814B00E2CD36 /* EthTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64BD2B7420E2814B00E2CD36 /* EthTransaction.swift */; }; + 64BD2B7720E2820300E2CD36 /* TransactionDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64BD2B7620E2820300E2CD36 /* TransactionDetails.swift */; }; 64D059FF20D3116B003AD655 /* NodesListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D059FE20D3116A003AD655 /* NodesListViewController.swift */; }; 64E8305020F5FEEF006FA590 /* VotesAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E8304F20F5FEEF006FA590 /* VotesAsset.swift */; }; + 64EE46B220FE0C8D00194DDA /* LskTransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EE46B120FE0C8D00194DDA /* LskTransactionsViewController.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 */; }; 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 */; }; @@ -60,9 +69,8 @@ E91947AC20001A9A001362F8 /* ApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E91947AB20001A9A001362F8 /* ApiService.swift */; }; E91947B020002393001362F8 /* AdamantApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E91947AF20002393001362F8 /* AdamantApiService.swift */; }; E91947B22000246A001362F8 /* AdamantError.swift in Sources */ = {isa = PBXBuildFile; fileRef = E91947B12000246A001362F8 /* AdamantError.swift */; }; - E91947B420002809001362F8 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = E91947B320002809001362F8 /* Account.swift */; }; + E91947B420002809001362F8 /* AdamantAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = E91947B320002809001362F8 /* AdamantAccount.swift */; }; E91E5BF220DAF05500B06B3C /* EurekaNodeRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E91E5BF120DAF05500B06B3C /* EurekaNodeRow.swift */; }; - E91E5BF420DAF3AB00B06B3C /* NodeCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = E91E5BF320DAF3AB00B06B3C /* NodeCell.xib */; }; E9204B5020C94C4A00F3B9AB /* Date+humanizedString.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9204B4F20C94C4A00F3B9AB /* Date+humanizedString.swift */; }; E9204B5220C9762400F3B9AB /* MessageStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9204B5120C9762300F3B9AB /* MessageStatus.swift */; }; E921534E20EE1E8700C0843F /* EurekaAlertLabelRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E921534C20EE1E8700C0843F /* EurekaAlertLabelRow.swift */; }; @@ -71,12 +79,21 @@ 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 */; }; - E9220E0321983156009C9642 /* adamant-core.js in Resources */ = {isa = PBXBuildFile; fileRef = E9220E0121983155009C9642 /* adamant-core.js */; }; - E9220E0421983156009C9642 /* JSAdamantCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9220E0221983155009C9642 /* JSAdamantCore.swift */; }; - E9220E08219879B9009C9642 /* NativeCoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9220E07219879B9009C9642 /* NativeCoreTests.swift */; }; + E9220E0D21988EEA009C9642 /* ChatModels.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = E95F85B1200954D00070534A /* ChatModels.xcdatamodeld */; }; + E9220E1121988F81009C9642 /* JSAdamantCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9220E0221983155009C9642 /* JSAdamantCore.swift */; }; + E9220E1221988F81009C9642 /* adamant-core.js in Resources */ = {isa = PBXBuildFile; fileRef = E9220E0121983155009C9642 /* adamant-core.js */; }; + E9220E1321988F81009C9642 /* JSAdamantCoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85762007E8EC0070534A /* JSAdamantCoreTests.swift */; }; + E9220E1421988F81009C9642 /* NativeCoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9220E07219879B9009C9642 /* NativeCoreTests.swift */; }; + E9220E1521988FCE009C9642 /* JSModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F856A200789450070534A /* JSModels.swift */; }; + E923222621135F9000A7E5AF /* EthAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = E923222521135F9000A7E5AF /* EthAccount.swift */; }; + E9240BF5215D686500187B09 /* AdmWalletService+RichMessageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9240BF4215D686500187B09 /* AdmWalletService+RichMessageProvider.swift */; }; + E9240BF9215D813A00187B09 /* CustomCellDeleage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9240BF8215D813A00187B09 /* CustomCellDeleage.swift */; }; E9256F5F2034C21100DE86E9 /* String+localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9256F5E2034C21100DE86E9 /* String+localized.swift */; }; E9256F6D20357B1700DE86E9 /* LogoFullHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = E9256F6C20357B1700DE86E9 /* LogoFullHeader.xib */; }; 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 */; }; + E926E034213EC454005E536B /* FullscreenAlertView.xib in Resources */ = {isa = PBXBuildFile; fileRef = E926E033213EC454005E536B /* FullscreenAlertView.xib */; }; E927171E20C04614002BB9A6 /* UIColor+hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = E927171D20C04613002BB9A6 /* UIColor+hex.swift */; }; E9393FA82055C92700EE6F30 /* Decimal+adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9393FA72055C92700EE6F30 /* Decimal+adamant.swift */; }; E9393FAA2055D03300EE6F30 /* AdamantMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9393FA92055D03300EE6F30 /* AdamantMessage.swift */; }; @@ -87,6 +104,19 @@ E93EB09F20DA3FA4001F9601 /* NodesEditorRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E93EB09E20DA3FA4001F9601 /* NodesEditorRoutes.swift */; }; E93EB0A320DA4CCA001F9601 /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = E93EB0A220DA4CCA001F9601 /* Node.swift */; }; E93EFE13200D1156000BB482 /* ChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E93EFE12200D1156000BB482 /* ChatViewController.swift */; }; + E940086B2114A70600CD2D67 /* LskAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = E940086A2114A70600CD2D67 /* LskAccount.swift */; }; + E940086E2114AA2E00CD2D67 /* WalletService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E940086D2114AA2E00CD2D67 /* WalletService.swift */; }; + E94008702114EA6800CD2D67 /* AdamantBalanceFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = E940086F2114EA6800CD2D67 /* AdamantBalanceFormat.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 */; }; + E94008802114EE2000CD2D67 /* AdmWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E940087F2114EE2000CD2D67 /* AdmWallet.swift */; }; + E94008832114EE4700CD2D67 /* LskWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E94008822114EE4700CD2D67 /* LskWallet.swift */; }; + E94008852114EE7500CD2D67 /* LskWalletService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E94008842114EE7500CD2D67 /* LskWalletService.swift */; }; + E94008872114F05B00CD2D67 /* AddressValidationResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = E94008862114F05B00CD2D67 /* AddressValidationResult.swift */; }; + E94008892114F0F700CD2D67 /* AdmWalletService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E94008882114F0F700CD2D67 /* AdmWalletService.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 */; }; @@ -95,10 +125,8 @@ E948E0482024F02700975D6B /* VersionFooter.xib in Resources */ = {isa = PBXBuildFile; fileRef = E948E0472024F02700975D6B /* VersionFooter.xib */; }; E948E04C2027679300975D6B /* AdamantFormattingTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = E948E04B2027679300975D6B /* AdamantFormattingTools.swift */; }; E94E7B01205D3F090042B639 /* ChatListViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E94E7B00205D3F090042B639 /* ChatListViewController.xib */; }; - E94E7B06205D48B20042B639 /* TransactionsRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E94E7B05205D48B20042B639 /* TransactionsRoutes.swift */; }; E94E7B08205D4CB80042B639 /* SharedRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E94E7B07205D4CB80042B639 /* SharedRoutes.swift */; }; - E94E7B0C205D5E4A0042B639 /* TransactionsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E94E7B0B205D5E4A0042B639 /* TransactionsViewController.xib */; }; - E94E7B0E205D5EA80042B639 /* TransactionDetailsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E94E7B0D205D5EA80042B639 /* TransactionDetailsViewController.xib */; }; + 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 */; }; @@ -106,17 +134,14 @@ E95CB456205D77B500A7218E /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E95CB458205D77B500A7218E /* Localizable.strings */; }; E95CB45E205D7F9600A7218E /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = E95CB460205D7F9600A7218E /* Localizable.stringsdict */; }; E95F85692006AB9D0070534A /* NormalizedTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85682006AB9D0070534A /* NormalizedTransaction.swift */; }; - E95F856B200789450070534A /* JSModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F856A200789450070534A /* JSModels.swift */; }; E95F856F2007B61D0070534A /* GetPublicKeyResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F856E2007B61D0070534A /* GetPublicKeyResponse.swift */; }; E95F85712007D98D0070534A /* CurrencyFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85702007D98D0070534A /* CurrencyFormatterTests.swift */; }; E95F85752007E4790070534A /* HexAndBytesUtilitiesTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85742007E4790070534A /* HexAndBytesUtilitiesTest.swift */; }; - E95F85772007E8EC0070534A /* JSAdamantCoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85762007E8EC0070534A /* JSAdamantCoreTests.swift */; }; E95F857A2007F0260070534A /* ServerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85792007F0260070534A /* ServerResponse.swift */; }; E95F85802008C8D70070534A /* ChatsRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F857E2008C8D60070534A /* ChatsRoutes.swift */; }; E95F85852008CB3A0070534A /* ChatListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85842008CB3A0070534A /* ChatListViewController.swift */; }; E95F85872008FDBF0070534A /* ChatAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85862008FDBF0070534A /* ChatAsset.swift */; }; E95F8589200900B10070534A /* ChatType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F8588200900B10070534A /* ChatType.swift */; }; - E95F85B3200954D00070534A /* ChatModels.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = E95F85B1200954D00070534A /* ChatModels.xcdatamodeld */; }; E95F85B7200A4D8F0070534A /* TestTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85B6200A4D8F0070534A /* TestTools.swift */; }; E95F85BA200A4DC90070534A /* TransactionSend.json in Resources */ = {isa = PBXBuildFile; fileRef = E95F85B9200A4DC90070534A /* TransactionSend.json */; }; E95F85BC200A4E670070534A /* ParsingModelsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95F85BB200A4E670070534A /* ParsingModelsTests.swift */; }; @@ -130,11 +155,14 @@ E965A53220B82C850041A3EA /* StateType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E965A53120B82C850041A3EA /* StateType.swift */; }; E965A53420B833A00041A3EA /* StateAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = E965A53320B833A00041A3EA /* StateAsset.swift */; }; E965A53620B8370C0041A3EA /* TransactionAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = E965A53520B8370C0041A3EA /* TransactionAsset.swift */; }; + 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 */; }; + E974D168215D033C003AD7E8 /* ChatCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E974D167215D033C003AD7E8 /* ChatCell.swift */; }; E983AE2120E655C500497E1A /* AccountHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E983AE2020E655C500497E1A /* AccountHeaderView.swift */; }; - E983AE2320E6568C00497E1A /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E983AE2220E6568C00497E1A /* Wallet.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 */; }; @@ -142,10 +170,19 @@ E98FC34420F920BD00032D65 /* UIFont+adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98FC34320F920BD00032D65 /* UIFont+adamant.swift */; }; E98FC34620F9210100032D65 /* Date+adamant.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98FC34520F9210100032D65 /* Date+adamant.swift */; }; E98FC34820F921EA00032D65 /* DelegateVote.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98FC34720F921EA00032D65 /* DelegateVote.swift */; }; + E993301E212EF39700CD5200 /* EthTransferViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E993301D212EF39700CD5200 /* EthTransferViewController.swift */; }; + E993302021354B1800CD5200 /* AdmWalletRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E993301F21354B1800CD5200 /* AdmWalletRoutes.swift */; }; + E993302221354BC300CD5200 /* EthWalletRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E993302121354BC300CD5200 /* EthWalletRoutes.swift */; }; + E993302421369B8A00CD5200 /* RichMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E993302321369B8A00CD5200 /* RichMessage.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 */; }; + E9981892212088DE0018C84C /* NodeCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = E91E5BF320DAF3AB00B06B3C /* NodeCell.xib */; }; + 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 */; }; E9A03FD620DBC8E2007653A1 /* AdamantApi+Peers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9A03FD520DBC8E2007653A1 /* AdamantApi+Peers.swift */; }; @@ -155,10 +192,21 @@ 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 */; }; + E9AA8BFC212C169200F9249F /* EthWalletService+Transfers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9AA8BFB212C169200F9249F /* EthWalletService+Transfers.swift */; }; + E9AA8C02212C5BF500F9249F /* AdmWalletService+Send.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9AA8C01212C5BF500F9249F /* AdmWalletService+Send.swift */; }; + E9AD78812158EED300742061 /* RichMessageTransaction+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9AD787F2158EED300742061 /* RichMessageTransaction+CoreDataClass.swift */; }; + E9AD78822158EED300742061 /* RichMessageTransaction+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9AD78802158EED300742061 /* RichMessageTransaction+CoreDataProperties.swift */; }; + E9B1AA572121ACC000080A2A /* AdmWalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9B1AA562121ACBF00080A2A /* AdmWalletViewController.swift */; }; + E9B1AA592122D59600080A2A /* WalletsRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9B1AA582122D59600080A2A /* WalletsRoutes.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 */; }; E9C51EEF20139DC600385EB7 /* TransactionIdResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9C51EEE20139DC600385EB7 /* TransactionIdResponse.swift */; }; E9C51EF12013F18000385EB7 /* NewChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9C51EF02013F18000385EB7 /* NewChatViewController.swift */; }; @@ -167,6 +215,10 @@ E9CAE8D62018AC5300345E76 /* AdamantApi+Transactions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9CAE8D52018AC5300345E76 /* AdamantApi+Transactions.swift */; }; E9CAE8D82018ACA700345E76 /* AdamantApi+Transfers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9CAE8D72018ACA700345E76 /* AdamantApi+Transfers.swift */; }; E9CAE8DA2018ACD300345E76 /* AdamantApi+Chats.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9CAE8D92018ACD300345E76 /* AdamantApi+Chats.swift */; }; + E9D1BE1A211DA25300E86B72 /* UIView+constraints.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D1BE19211DA25300E86B72 /* UIView+constraints.swift */; }; + E9D1BE1C211DABE100E86B72 /* WalletPagingItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9D1BE1B211DABE100E86B72 /* WalletPagingItem.swift */; }; + E9DFB71C21624C9200CF8C7C /* AdmTransactionDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9DFB71B21624C9200CF8C7C /* AdmTransactionDetailsViewController.swift */; }; + E9DFB720216619F400CF8C7C /* BaseTransaction+TransactionDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9DFB71F216619F400CF8C7C /* BaseTransaction+TransactionDetails.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 */; }; @@ -179,12 +231,11 @@ E9E7CDB72003994E00DFC4DB /* AdamantUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDB62003994E00DFC4DB /* AdamantUtilities.swift */; }; E9E7CDBE2003AEFB00DFC4DB /* CellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDBD2003AEFB00DFC4DB /* CellFactory.swift */; }; E9E7CDC02003AF6D00DFC4DB /* AdamantCellFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDBF2003AF6D00DFC4DB /* AdamantCellFactory.swift */; }; - E9E7CDC22003F5A400DFC4DB /* TransactionsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDC12003F5A400DFC4DB /* TransactionsViewController.swift */; }; + E9E7CDC22003F5A400DFC4DB /* TransactionsListViewControllerBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDC12003F5A400DFC4DB /* TransactionsListViewControllerBase.swift */; }; E9E7CDC72003F6D200DFC4DB /* TransactionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDC52003F6D200DFC4DB /* TransactionTableViewCell.swift */; }; E9E7CDC82003F6D200DFC4DB /* TransactionTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = E9E7CDC62003F6D200DFC4DB /* TransactionTableViewCell.xib */; }; E9E7CDCA20040CC200DFC4DB /* Transaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDC920040CC200DFC4DB /* Transaction.swift */; }; E9E7CDCC20040FDC00DFC4DB /* TransactionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E7CDCB20040FDC00DFC4DB /* TransactionType.swift */; }; - E9EC34142005178500C0E546 /* TransactionDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9EC34132005178500C0E546 /* TransactionDetailsViewController.swift */; }; E9EC3415200524CA00C0E546 /* Exo+2_100_normal.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E91947A3200010DD001362F8 /* Exo+2_100_normal.ttf */; }; E9EC3416200524CA00C0E546 /* Exo+2_300_normal.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E91947A4200010DE001362F8 /* Exo+2_300_normal.ttf */; }; E9EC3417200524CA00C0E546 /* Exo+2_400_italic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E919479F200010DC001362F8 /* Exo+2_400_italic.ttf */; }; @@ -196,11 +247,16 @@ E9EC341D200524CA00C0E546 /* Roboto_400_normal.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E91947A6200010DF001362F8 /* Roboto_400_normal.ttf */; }; E9EC341E200524CA00C0E546 /* Roboto_500_normal.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E91947A5200010DE001362F8 /* Roboto_500_normal.ttf */; }; E9EC341F200524CA00C0E546 /* Roboto_700_normal.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E91947A7200010DF001362F8 /* Roboto_700_normal.ttf */; }; - E9EC342120052ABB00C0E546 /* TransferViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9EC342020052ABB00C0E546 /* TransferViewController.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 */; }; + E9FEECA421413659007DD7C8 /* RichMessageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FEECA321413659007DD7C8 /* RichMessageProvider.swift */; }; + E9FEECA62143C300007DD7C8 /* EthWalletService+RichMessageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FEECA52143C300007DD7C8 /* EthWalletService+RichMessageProvider.swift */; }; + E9FEECA92143C371007DD7C8 /* TransferCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FEECA72143C371007DD7C8 /* TransferCollectionViewCell.swift */; }; + E9FEECAA2143C371007DD7C8 /* TransferCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = E9FEECA82143C371007DD7C8 /* TransferCollectionViewCell.xib */; }; + E9FEECAC2143CEED007DD7C8 /* TransferMessageSizeCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9FEECAB2143CEED007DD7C8 /* TransferMessageSizeCalculator.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -214,6 +270,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 642361BD20E3869D0061559E /* eth_l18n.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = eth_l18n.strings; sourceTree = ""; }; 643ED0B02109F4BD005A9FDA /* NativeAdamantCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeAdamantCore.swift; sourceTree = ""; }; 644EC34C20EFA60900F40C73 /* AdamantApi+Delegates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantApi+Delegates.swift"; sourceTree = ""; }; 644EC34E20EFA77A00F40C73 /* Delegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Delegate.swift; sourceTree = ""; }; @@ -228,8 +285,16 @@ 645E7B052111DF3A006CC9FD /* Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Crypto.swift; sourceTree = ""; }; 649E9A142111B3C200686B01 /* Mnemonic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mnemonic.swift; sourceTree = ""; }; 64A223D520F760BB005157CB /* Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Localization.swift; sourceTree = ""; }; + 64A223D720F7A08E005157CB /* LskApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskApiService.swift; sourceTree = ""; }; + 64A223D920F7A14B005157CB /* AdamantLskApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantLskApiService.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 = ""; }; 64D059FE20D3116A003AD655 /* NodesListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodesListViewController.swift; sourceTree = ""; }; 64E8304F20F5FEEF006FA590 /* VotesAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotesAsset.swift; sourceTree = ""; }; + 64EE46B120FE0C8D00194DDA /* LskTransactionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskTransactionsViewController.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 = ""; }; 871009461457F6B7B47D99C5 /* Pods-Adamant.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Adamant.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Adamant/Pods-Adamant.debug.xcconfig"; sourceTree = ""; }; 903AC078A8A55175A05E42D4 /* Pods-Adamant.testing.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Adamant.testing.xcconfig"; path = "Pods/Target Support Files/Pods-Adamant/Pods-Adamant.testing.xcconfig"; sourceTree = ""; }; C107D5CB65B4D728B9D97C0F /* Pods_Adamant.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Adamant.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -288,7 +353,7 @@ E91947AB20001A9A001362F8 /* ApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiService.swift; sourceTree = ""; }; E91947AF20002393001362F8 /* AdamantApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantApiService.swift; sourceTree = ""; }; E91947B12000246A001362F8 /* AdamantError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantError.swift; sourceTree = ""; }; - E91947B320002809001362F8 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = ""; }; + E91947B320002809001362F8 /* AdamantAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantAccount.swift; sourceTree = ""; }; E91E5BF120DAF05500B06B3C /* EurekaNodeRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EurekaNodeRow.swift; sourceTree = ""; }; E91E5BF320DAF3AB00B06B3C /* NodeCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NodeCell.xib; sourceTree = ""; }; E9204B4F20C94C4A00F3B9AB /* Date+humanizedString.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+humanizedString.swift"; sourceTree = ""; }; @@ -302,9 +367,15 @@ 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 = ""; }; + E9240BF8215D813A00187B09 /* CustomCellDeleage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCellDeleage.swift; sourceTree = ""; }; E9256F5E2034C21100DE86E9 /* String+localized.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+localized.swift"; sourceTree = ""; }; E9256F6C20357B1700DE86E9 /* LogoFullHeader.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = LogoFullHeader.xib; 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 = ""; }; + E926E033213EC454005E536B /* FullscreenAlertView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FullscreenAlertView.xib; sourceTree = ""; }; E927171D20C04613002BB9A6 /* UIColor+hex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+hex.swift"; sourceTree = ""; }; E9393FA72055C92700EE6F30 /* Decimal+adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decimal+adamant.swift"; sourceTree = ""; }; E9393FA92055D03300EE6F30 /* AdamantMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantMessage.swift; sourceTree = ""; }; @@ -315,6 +386,19 @@ E93EB09E20DA3FA4001F9601 /* NodesEditorRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodesEditorRoutes.swift; sourceTree = ""; }; E93EB0A220DA4CCA001F9601 /* Node.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Node.swift; sourceTree = ""; }; E93EFE12200D1156000BB482 /* ChatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatViewController.swift; sourceTree = ""; }; + E940086A2114A70600CD2D67 /* LskAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskAccount.swift; sourceTree = ""; }; + E940086D2114AA2E00CD2D67 /* WalletService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletService.swift; sourceTree = ""; }; + E940086F2114EA6800CD2D67 /* AdamantBalanceFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantBalanceFormat.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 = ""; }; + E940087F2114EE2000CD2D67 /* AdmWallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AdmWallet.swift; path = Adamant/Wallets/Adamant/AdmWallet.swift; sourceTree = SOURCE_ROOT; }; + E94008822114EE4700CD2D67 /* LskWallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskWallet.swift; sourceTree = ""; }; + E94008842114EE7500CD2D67 /* LskWalletService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LskWalletService.swift; sourceTree = ""; }; + E94008862114F05B00CD2D67 /* AddressValidationResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressValidationResult.swift; sourceTree = ""; }; + E94008882114F0F700CD2D67 /* AdmWalletService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AdmWalletService.swift; path = Adamant/Wallets/Adamant/AdmWalletService.swift; sourceTree = SOURCE_ROOT; }; + 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 = ""; }; @@ -323,10 +407,8 @@ E948E0472024F02700975D6B /* VersionFooter.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = VersionFooter.xib; sourceTree = ""; }; E948E04B2027679300975D6B /* AdamantFormattingTools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantFormattingTools.swift; sourceTree = ""; }; E94E7B00205D3F090042B639 /* ChatListViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ChatListViewController.xib; sourceTree = ""; }; - E94E7B05205D48B20042B639 /* TransactionsRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionsRoutes.swift; sourceTree = ""; }; E94E7B07205D4CB80042B639 /* SharedRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedRoutes.swift; sourceTree = ""; }; - E94E7B0B205D5E4A0042B639 /* TransactionsViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TransactionsViewController.xib; sourceTree = ""; }; - E94E7B0D205D5EA80042B639 /* TransactionDetailsViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TransactionDetailsViewController.xib; 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 = ""; }; @@ -361,11 +443,14 @@ E965A53120B82C850041A3EA /* StateType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateType.swift; sourceTree = ""; }; E965A53320B833A00041A3EA /* StateAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StateAsset.swift; sourceTree = ""; }; E965A53520B8370C0041A3EA /* TransactionAsset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionAsset.swift; 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 = ""; }; + E974D167215D033C003AD7E8 /* ChatCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatCell.swift; sourceTree = ""; }; E983AE2020E655C500497E1A /* AccountHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountHeaderView.swift; sourceTree = ""; }; - E983AE2220E6568C00497E1A /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.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 = ""; }; @@ -373,10 +458,18 @@ E98FC34320F920BD00032D65 /* UIFont+adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+adamant.swift"; sourceTree = ""; }; E98FC34520F9210100032D65 /* Date+adamant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+adamant.swift"; sourceTree = ""; }; E98FC34720F921EA00032D65 /* DelegateVote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DelegateVote.swift; sourceTree = ""; }; + E993301D212EF39700CD5200 /* EthTransferViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthTransferViewController.swift; sourceTree = ""; }; + E993301F21354B1800CD5200 /* AdmWalletRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdmWalletRoutes.swift; sourceTree = ""; }; + E993302121354BC300CD5200 /* EthWalletRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthWalletRoutes.swift; sourceTree = ""; }; + E993302321369B8A00CD5200 /* RichMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RichMessage.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 = ""; }; + 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 = ""; }; E9A03FD520DBC8E2007653A1 /* AdamantApi+Peers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantApi+Peers.swift"; sourceTree = ""; }; @@ -386,10 +479,21 @@ 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 = ""; }; + E9AA8BFB212C169200F9249F /* EthWalletService+Transfers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EthWalletService+Transfers.swift"; sourceTree = ""; }; + E9AA8C01212C5BF500F9249F /* AdmWalletService+Send.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdmWalletService+Send.swift"; sourceTree = ""; }; + E9AD787F2158EED300742061 /* RichMessageTransaction+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RichMessageTransaction+CoreDataClass.swift"; sourceTree = ""; }; + E9AD78802158EED300742061 /* RichMessageTransaction+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RichMessageTransaction+CoreDataProperties.swift"; sourceTree = ""; }; + E9B1AA562121ACBF00080A2A /* AdmWalletViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdmWalletViewController.swift; sourceTree = ""; }; + E9B1AA582122D59600080A2A /* WalletsRoutes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletsRoutes.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 = ""; }; E9C51ECE200E2D1100385EB7 /* FeeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeeTests.swift; sourceTree = ""; }; E9C51EEE20139DC600385EB7 /* TransactionIdResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionIdResponse.swift; sourceTree = ""; }; E9C51EF02013F18000385EB7 /* NewChatViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewChatViewController.swift; sourceTree = ""; }; @@ -398,6 +502,11 @@ E9CAE8D52018AC5300345E76 /* AdamantApi+Transactions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantApi+Transactions.swift"; sourceTree = ""; }; E9CAE8D72018ACA700345E76 /* AdamantApi+Transfers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantApi+Transfers.swift"; sourceTree = ""; }; E9CAE8D92018ACD300345E76 /* AdamantApi+Chats.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdamantApi+Chats.swift"; sourceTree = ""; }; + E9D1BE19211DA25300E86B72 /* UIView+constraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+constraints.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 = ""; }; + E9DFB71D21651FBD00CF8C7C /* JSAdamantCore+Native.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "JSAdamantCore+Native.swift"; sourceTree = ""; }; + E9DFB71F216619F400CF8C7C /* BaseTransaction+TransactionDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseTransaction+TransactionDetails.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 = ""; }; @@ -410,19 +519,23 @@ E9E7CDB62003994E00DFC4DB /* AdamantUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdamantUtilities.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 /* TransactionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionsViewController.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 = ""; }; E9E7CDC62003F6D200DFC4DB /* TransactionTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TransactionTableViewCell.xib; sourceTree = ""; }; E9E7CDC920040CC200DFC4DB /* Transaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transaction.swift; sourceTree = ""; }; E9E7CDCB20040FDC00DFC4DB /* TransactionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionType.swift; sourceTree = ""; }; - E9EC34132005178500C0E546 /* TransactionDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionDetailsViewController.swift; sourceTree = ""; }; - E9EC342020052ABB00C0E546 /* TransferViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferViewController.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 = ""; }; + E9FEECA321413659007DD7C8 /* RichMessageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RichMessageProvider.swift; sourceTree = ""; }; + E9FEECA52143C300007DD7C8 /* EthWalletService+RichMessageProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EthWalletService+RichMessageProvider.swift"; sourceTree = ""; }; + E9FEECA72143C371007DD7C8 /* TransferCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferCollectionViewCell.swift; sourceTree = ""; }; + E9FEECA82143C371007DD7C8 /* TransferCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TransferCollectionViewCell.xib; sourceTree = ""; }; + E9FEECAB2143CEED007DD7C8 /* TransferMessageSizeCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferMessageSizeCalculator.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -466,6 +579,14 @@ path = Delegates; sourceTree = ""; }; + 64D05A0020D515CA003AD655 /* TokensApiService */ = { + isa = PBXGroup; + children = ( + 64A223D920F7A14B005157CB /* AdamantLskApiService.swift */, + ); + path = TokensApiService; + sourceTree = ""; + }; E59396A8E0053F21F768E69B /* Pods */ = { isa = PBXGroup; children = ( @@ -484,6 +605,7 @@ 54F4E3CDA748B7F48C085503 /* Frameworks */, E59396A8E0053F21F768E69B /* Pods */, E913C8EF1FFFA51D001A83F7 /* Products */, + E9220E0C21988D9A009C9642 /* Recovered References */, ); sourceTree = ""; }; @@ -506,6 +628,7 @@ E91947B72000326B001362F8 /* ServerResponses */, E913C9041FFFA8FE001A83F7 /* ServiceProtocols */, E913C9061FFFA92E001A83F7 /* Services */, + E940086C2114A8FD00CD2D67 /* Wallets */, E9E7CDB82003AA8E00DFC4DB /* SharedViews */, E919479920000FFD001362F8 /* Stories */, E913C9111FFFAB05001A83F7 /* Assets */, @@ -534,6 +657,8 @@ E93D7ABD2052CEE1005D19DC /* NotificationsService.swift */, E9A174B22057EC47003667CD /* BackgroundFetchService.swift */, E9215972206119FB0000CA5C /* ReachabilityMonitor.swift */, + 64A223D720F7A08E005157CB /* LskApiService.swift */, + E9FEECA321413659007DD7C8 /* RichMessageProvider.swift */, ); path = ServiceProtocols; sourceTree = ""; @@ -541,6 +666,7 @@ E913C9061FFFA92E001A83F7 /* Services */ = { isa = PBXGroup; children = ( + 64D05A0020D515CA003AD655 /* TokensApiService */, E9CAE8D02018AA5000345E76 /* ApiService */, E9B3D39F201FA2090019EB36 /* DataProviders */, E9E7CD922002740500DFC4DB /* AdamantAccountService.swift */, @@ -562,7 +688,7 @@ E913C9091FFFA95A001A83F7 /* Models */ = { isa = PBXGroup; children = ( - E91947B320002809001362F8 /* Account.swift */, + E91947B320002809001362F8 /* AdamantAccount.swift */, E9393FA92055D03300EE6F30 /* AdamantMessage.swift */, E95F85862008FDBF0070534A /* ChatAsset.swift */, E95F8588200900B10070534A /* ChatType.swift */, @@ -574,10 +700,14 @@ E9E7CDC920040CC200DFC4DB /* Transaction.swift */, E965A53520B8370C0041A3EA /* TransactionAsset.swift */, E9E7CDCB20040FDC00DFC4DB /* TransactionType.swift */, - E983AE2220E6568C00497E1A /* Wallet.swift */, E9A03FD320DBC824007653A1 /* NodeVersion.swift */, 644EC34E20EFA77A00F40C73 /* Delegate.swift */, 64E8304F20F5FEEF006FA590 /* VotesAsset.swift */, + 64BD2B7420E2814B00E2CD36 /* EthTransaction.swift */, + E923222521135F9000A7E5AF /* EthAccount.swift */, + E940086A2114A70600CD2D67 /* LskAccount.swift */, + E993302321369B8A00CD5200 /* RichMessage.swift */, + E971591921681D6900A5F904 /* TransactionStatus.swift */, ); path = Models; sourceTree = ""; @@ -587,20 +717,24 @@ children = ( E91947B12000246A001362F8 /* AdamantError.swift */, E9061B96207501E40011F104 /* AdamantUserInfoKey.swift */, + E94008862114F05B00CD2D67 /* AddressValidationResult.swift */, + E940086F2114EA6800CD2D67 /* AdamantBalanceFormat.swift */, + E940088E2119A9E800CD2D67 /* BigInt+Decimal.swift */, E9FAE5D9203DBFEF008D3A6B /* Comparable+clamped.swift */, E98FC34520F9210100032D65 /* Date+adamant.swift */, E9204B4F20C94C4A00F3B9AB /* Date+humanizedString.swift */, E9393FA72055C92700EE6F30 /* Decimal+adamant.swift */, E98FC34720F921EA00032D65 /* DelegateVote.swift */, - E95F856A200789450070534A /* JSModels.swift */, E913C90C1FFFA99B001A83F7 /* Keypair.swift */, 64A223D520F760BB005157CB /* Localization.swift */, E9147B5E20500E9300145913 /* MyLittlePinpad+adamant.swift */, + E940088A2114F63000CD2D67 /* NSRegularExpression+adamant.swift */, E9147B6220505C7500145913 /* QRCodeReader+adamant.swift */, E9256F5E2034C21100DE86E9 /* String+localized.swift */, E98FC34120F9209900032D65 /* UIColor+adamant.swift */, E927171D20C04613002BB9A6 /* UIColor+hex.swift */, E98FC34320F920BD00032D65 /* UIFont+adamant.swift */, + E9D1BE19211DA25300E86B72 /* UIView+constraints.swift */, ); path = Helpers; sourceTree = ""; @@ -613,6 +747,7 @@ E913C8F81FFFA51D001A83F7 /* Assets.xcassets */, E9A174B820587B83003667CD /* notification.mp3 */, E9256F752039A9A200DE86E9 /* LaunchScreen.storyboard */, + 642361BD20E3869D0061559E /* eth_l18n.strings */, ); path = Assets; sourceTree = ""; @@ -627,7 +762,6 @@ E93EB09D20DA3F3A001F9601 /* NodesEditor */, E982F69820235AF000566AC7 /* Settings */, E9FAE5DB203ECD41008D3A6B /* Shared */, - E94E7B04205D48950042B639 /* Transactions */, ); path = Stories; sourceTree = ""; @@ -658,6 +792,7 @@ E9220E0021983145009C9642 /* Core */ = { isa = PBXGroup; children = ( + E95F856A200789450070534A /* JSModels.swift */, E9220E0221983155009C9642 /* JSAdamantCore.swift */, E9220E0121983155009C9642 /* adamant-core.js */, E95F85762007E8EC0070534A /* JSAdamantCoreTests.swift */, @@ -666,6 +801,14 @@ path = Core; sourceTree = ""; }; + E9220E0C21988D9A009C9642 /* Recovered References */ = { + isa = PBXGroup; + children = ( + E9DFB71D21651FBD00CF8C7C /* JSAdamantCore+Native.swift */, + ); + name = "Recovered References"; + sourceTree = ""; + }; E93EB09D20DA3F3A001F9601 /* NodesEditor */ = { isa = PBXGroup; children = ( @@ -678,18 +821,72 @@ path = NodesEditor; sourceTree = ""; }; - E94E7B04205D48950042B639 /* Transactions */ = { + E940086C2114A8FD00CD2D67 /* Wallets */ = { isa = PBXGroup; children = ( - E94E7B05205D48B20042B639 /* TransactionsRoutes.swift */, - E9E7CDC12003F5A400DFC4DB /* TransactionsViewController.swift */, - E94E7B0B205D5E4A0042B639 /* TransactionsViewController.xib */, - E9EC34132005178500C0E546 /* TransactionDetailsViewController.swift */, - E94E7B0D205D5EA80042B639 /* TransactionDetailsViewController.xib */, + E94008902119D22400CD2D67 /* Adamant */, + E94008792114ECF100CD2D67 /* Ethereum */, + E94008812114EE3900CD2D67 /* Lisk */, + E94008712114EACF00CD2D67 /* WalletAccount.swift */, + E940086D2114AA2E00CD2D67 /* WalletService.swift */, + E9B1AA582122D59600080A2A /* WalletsRoutes.swift */, + E99818932120892F0018C84C /* WalletViewControllerBase.swift */, + E9981897212096ED0018C84C /* WalletViewControllerBase.xib */, + E9EC342020052ABB00C0E546 /* TransferViewControllerBase.swift */, + E99330252136B0E500CD5200 /* TransferViewControllerBase+QR.swift */, + E926E02D213EAABF005E536B /* TransferViewControllerBase+Alert.swift */, + E9E7CDC12003F5A400DFC4DB /* TransactionsListViewControllerBase.swift */, + E94E7B0B205D5E4A0042B639 /* TransactionsListViewControllerBase.xib */, + 64BD2B7620E2820300E2CD36 /* TransactionDetails.swift */, + 64FA53D020E24941006783C9 /* TransactionDetailsViewControllerBase.swift */, E9E7CDC52003F6D200DFC4DB /* TransactionTableViewCell.swift */, E9E7CDC62003F6D200DFC4DB /* TransactionTableViewCell.xib */, ); - path = Transactions; + path = Wallets; + sourceTree = ""; + }; + E94008792114ECF100CD2D67 /* Ethereum */ = { + isa = PBXGroup; + children = ( + E993302121354BC300CD5200 /* EthWalletRoutes.swift */, + E940087C2114EDEE00CD2D67 /* EthWallet.swift */, + E940087A2114ED0600CD2D67 /* EthWalletService.swift */, + E9AA8BF9212C166600F9249F /* EthWalletService+Send.swift */, + E9AA8BFB212C169200F9249F /* EthWalletService+Transfers.swift */, + E9FEECA52143C300007DD7C8 /* EthWalletService+RichMessageProvider.swift */, + E971591B2168209800A5F904 /* EthWalletService+RichMessageProviderWithStatusCheck.swift */, + E9981895212095CA0018C84C /* EthWalletViewController.swift */, + E993301D212EF39700CD5200 /* EthTransferViewController.swift */, + 64FA53CC20E1300A006783C9 /* EthTransactionsViewController.swift */, + E96E86B721679C120061F80A /* EthTransactionDetailsViewController.swift */, + ); + path = Ethereum; + sourceTree = ""; + }; + E94008812114EE3900CD2D67 /* Lisk */ = { + isa = PBXGroup; + children = ( + E94008822114EE4700CD2D67 /* LskWallet.swift */, + E94008842114EE7500CD2D67 /* LskWalletService.swift */, + 64EE46B120FE0C8D00194DDA /* LskTransactionsViewController.swift */, + ); + path = Lisk; + sourceTree = ""; + }; + E94008902119D22400CD2D67 /* Adamant */ = { + isa = PBXGroup; + children = ( + E993301F21354B1800CD5200 /* AdmWalletRoutes.swift */, + E940087F2114EE2000CD2D67 /* AdmWallet.swift */, + E94008882114F0F700CD2D67 /* AdmWalletService.swift */, + E9AA8C01212C5BF500F9249F /* AdmWalletService+Send.swift */, + E9240BF4215D686500187B09 /* AdmWalletService+RichMessageProvider.swift */, + E9B1AA562121ACBF00080A2A /* AdmWalletViewController.swift */, + E9B1AA5A21283E0F00080A2A /* AdmTransferViewController.swift */, + 64F085D820E2D7600006DE68 /* AdmTransactionsViewController.swift */, + E9DFB71B21624C9200CF8C7C /* AdmTransactionDetailsViewController.swift */, + ); + path = Adamant; sourceTree = ""; }; E950651F20404997008352E5 /* Utilities */ = { @@ -724,8 +921,11 @@ E93EFE12200D1156000BB482 /* ChatViewController.swift */, E9150B7F2066861D0065A985 /* ChatViewController+MessageKit.swift */, E9C51EF02013F18000385EB7 /* NewChatViewController.swift */, + E9AA8BF72129F13000F9249F /* ComplexTransferViewController.swift */, E95F85C5200A9B070070534A /* ChatTableViewCell.swift */, E95F85C6200A9B070070534A /* ChatTableViewCell.xib */, + E974D167215D033C003AD7E8 /* ChatCell.swift */, + E9240BF8215D813A00187B09 /* CustomCellDeleage.swift */, ); path = Chats; sourceTree = ""; @@ -733,8 +933,11 @@ E95F859220094B8E0070534A /* CoreData */ = { isa = PBXGroup; children = ( + E9AD787F2158EED300742061 /* RichMessageTransaction+CoreDataClass.swift */, + E9AD78802158EED300742061 /* RichMessageTransaction+CoreDataProperties.swift */, E9150B8F2066DA210065A985 /* BaseTransaction+CoreDataClass.swift */, E9150B902066DA210065A985 /* BaseTransaction+CoreDataProperties.swift */, + E9DFB71F216619F400CF8C7C /* BaseTransaction+TransactionDetails.swift */, E9150B912066DA210065A985 /* Chatroom+CoreDataClass.swift */, E9150B922066DA210065A985 /* Chatroom+CoreDataProperties.swift */, E9150B952066DA210065A985 /* ChatTransaction+CoreDataClass.swift */, @@ -841,12 +1044,12 @@ children = ( E9E7CDB02002B97B00DFC4DB /* AccountRoutes.swift */, E983AE2820E65F3200497E1A /* AccountViewController.swift */, - E9EC342020052ABB00C0E546 /* TransferViewController.swift */, E983AE2020E655C500497E1A /* AccountHeaderView.swift */, E941CCDA20E786D700C96220 /* AccountHeader.xib */, E983AE2C20E6720D00497E1A /* AccountFooter.xib */, E941CCDC20E7B70200C96220 /* WalletCollectionViewCell.swift */, E941CCDD20E7B70200C96220 /* WalletCollectionViewCell.xib */, + E9D1BE1B211DABE100E86B72 /* WalletPagingItem.swift */, ); path = Account; sourceTree = ""; @@ -860,6 +1063,13 @@ E948E0472024F02700975D6B /* VersionFooter.xib */, E921534C20EE1E8700C0843F /* EurekaAlertLabelRow.swift */, E921534D20EE1E8700C0843F /* AlertLabelCell.xib */, + E926E031213EC43B005E536B /* FullscreenAlertView.swift */, + E926E033213EC454005E536B /* FullscreenAlertView.xib */, + E9FEECA72143C371007DD7C8 /* TransferCollectionViewCell.swift */, + E9FEECA82143C371007DD7C8 /* TransferCollectionViewCell.xib */, + E9FEECAB2143CEED007DD7C8 /* TransferMessageSizeCalculator.swift */, + E9B4E1A7210F079E007E77FC /* DoubleDetailsTableViewCell.swift */, + E9B4E1A9210F08BE007E77FC /* DoubleDetailsTableViewCell.xib */, ); path = SharedViews; sourceTree = ""; @@ -942,11 +1152,12 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 0930; + LastUpgradeCheck = 0940; ORGANIZATIONNAME = Adamant; TargetAttributes = { E913C8ED1FFFA51D001A83F7 = { CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1000; ProvisioningStyle = Manual; SystemCapabilities = { com.apple.BackgroundModes = { @@ -959,6 +1170,7 @@ }; E9EC344320066D4A00C0E546 = { CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1000; ProvisioningStyle = Automatic; TestTargetID = E913C8ED1FFFA51D001A83F7; }; @@ -991,6 +1203,7 @@ files = ( E9256F762039A9A200DE86E9 /* LaunchScreen.storyboard in Resources */, E95CB451205D77B200A7218E /* InfoPlist.strings in Resources */, + E9981898212096ED0018C84C /* WalletViewControllerBase.xib in Resources */, E983AE2D20E6720D00497E1A /* AccountFooter.xib in Resources */, E9EC341B200524CA00C0E546 /* Roboto_300_normal.ttf in Resources */, E95F85C8200A9B070070534A /* ChatTableViewCell.xib in Resources */, @@ -998,10 +1211,13 @@ E9EC341F200524CA00C0E546 /* Roboto_700_normal.ttf in Resources */, 644EC35C20EFB8E900F40C73 /* AdamantDelegateCell.xib in Resources */, E9942B89203D9ECA00C163AF /* QrCell.xib in Resources */, + E9FEECAA2143C371007DD7C8 /* TransferCollectionViewCell.xib in Resources */, E921597D2065031D0000CA5C /* ButtonsStripe.xib in Resources */, E90A4945204C6204009F6A65 /* PassphraseCell.xib in Resources */, E941CCDF20E7B70200C96220 /* WalletCollectionViewCell.xib in Resources */, + 642361BE20E3869D0061559E /* eth_l18n.strings in Resources */, E9E7CDC82003F6D200DFC4DB /* TransactionTableViewCell.xib in Resources */, + E926E034213EC454005E536B /* FullscreenAlertView.xib in Resources */, E9EC3417200524CA00C0E546 /* Exo+2_400_italic.ttf in Resources */, E94E7B01205D3F090042B639 /* ChatListViewController.xib in Resources */, E941CCDB20E786D800C96220 /* AccountHeader.xib in Resources */, @@ -1010,13 +1226,13 @@ E9256F6D20357B1700DE86E9 /* LogoFullHeader.xib in Resources */, E9EC3416200524CA00C0E546 /* Exo+2_300_normal.ttf in Resources */, E9EC341C200524CA00C0E546 /* Roboto_400_italic.ttf in Resources */, - E94E7B0C205D5E4A0042B639 /* TransactionsViewController.xib in Resources */, + E94E7B0C205D5E4A0042B639 /* TransactionsListViewControllerBase.xib in Resources */, E9EC3415200524CA00C0E546 /* Exo+2_100_normal.ttf in Resources */, E9EC341A200524CA00C0E546 /* Exo+2_700_normal.ttf in Resources */, E95CB456205D77B500A7218E /* Localizable.strings in Resources */, E95CB45E205D7F9600A7218E /* Localizable.stringsdict in Resources */, - E94E7B0E205D5EA80042B639 /* TransactionDetailsViewController.xib in Resources */, - E91E5BF420DAF3AB00B06B3C /* NodeCell.xib in Resources */, + E9981892212088DE0018C84C /* NodeCell.xib in Resources */, + E9B4E1AA210F1803007E77FC /* DoubleDetailsTableViewCell.xib in Resources */, E9EC341D200524CA00C0E546 /* Roboto_400_normal.ttf in Resources */, E9A174B920587B84003667CD /* notification.mp3 in Resources */, E9EC3418200524CA00C0E546 /* Exo+2_400_normal.ttf in Resources */, @@ -1035,7 +1251,7 @@ E95F85C2200A53E90070534A /* NormalizedTransaction.json in Resources */, E95F85BE200A503A0070534A /* TransactionChat.json in Resources */, E95F85C4200A540B0070534A /* Chat.json in Resources */, - E9220E0321983156009C9642 /* adamant-core.js in Resources */, + E9220E1221988F81009C9642 /* adamant-core.js in Resources */, E95F85BA200A4DC90070534A /* TransactionSend.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1051,44 +1267,70 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Adamant/Pods-Adamant-frameworks.sh", "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework", + "${BUILT_PRODUCTS_DIR}/BigInt/BigInt.framework", "${BUILT_PRODUCTS_DIR}/ByteBackpacker/ByteBackpacker.framework", "${BUILT_PRODUCTS_DIR}/CryptoSwift/CryptoSwift.framework", "${BUILT_PRODUCTS_DIR}/DateToolsSwift/DateToolsSwift.framework", "${BUILT_PRODUCTS_DIR}/EFQRCode/EFQRCode.framework", + "${BUILT_PRODUCTS_DIR}/Ed25519/Ed25519.framework", "${BUILT_PRODUCTS_DIR}/Eureka/Eureka.framework", "${BUILT_PRODUCTS_DIR}/FTIndicator/FTIndicator.framework", "${BUILT_PRODUCTS_DIR}/FreakingSimpleRoundImageView/FreakingSimpleRoundImageView.framework", "${BUILT_PRODUCTS_DIR}/Haring/Haring.framework", "${BUILT_PRODUCTS_DIR}/KeychainAccess/KeychainAccess.framework", + "${BUILT_PRODUCTS_DIR}/Lisk/Lisk.framework", + "${BUILT_PRODUCTS_DIR}/MessageInputBar/MessageInputBar.framework", "${BUILT_PRODUCTS_DIR}/MessageKit/MessageKit.framework", "${BUILT_PRODUCTS_DIR}/MyLittlePinpad/MyLittlePinpad.framework", "${BUILT_PRODUCTS_DIR}/PMAlertController/PMAlertController.framework", + "${BUILT_PRODUCTS_DIR}/Parchment/Parchment.framework", + "${BUILT_PRODUCTS_DIR}/ProcedureKit/ProcedureKit.framework", + "${BUILT_PRODUCTS_DIR}/PromiseKit/PromiseKit.framework", "${BUILT_PRODUCTS_DIR}/QRCodeReader.swift/QRCodeReader.framework", "${BUILT_PRODUCTS_DIR}/RNCryptor/RNCryptor.framework", "${BUILT_PRODUCTS_DIR}/ReachabilitySwift/Reachability.framework", + "${BUILT_PRODUCTS_DIR}/Result/Result.framework", + "${BUILT_PRODUCTS_DIR}/SipHash/SipHash.framework", "${BUILT_PRODUCTS_DIR}/Swinject/Swinject.framework", + "${BUILT_PRODUCTS_DIR}/libCEd25519/CEd25519.framework", "${BUILT_PRODUCTS_DIR}/libsodium/libsodium.framework", + "${BUILT_PRODUCTS_DIR}/scrypt/scrypt.framework", + "${BUILT_PRODUCTS_DIR}/secp256k1_ios/secp256k1_ios.framework", + "${BUILT_PRODUCTS_DIR}/web3swift/web3swift.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/BigInt.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ByteBackpacker.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CryptoSwift.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DateToolsSwift.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/EFQRCode.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Ed25519.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Eureka.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FTIndicator.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FreakingSimpleRoundImageView.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Haring.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/KeychainAccess.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Lisk.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MessageInputBar.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MessageKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/MyLittlePinpad.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PMAlertController.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Parchment.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ProcedureKit.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PromiseKit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/QRCodeReader.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RNCryptor.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Result.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SipHash.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Swinject.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CEd25519.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libsodium.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/scrypt.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/secp256k1_ios.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/web3swift.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -1154,7 +1396,13 @@ E9CAE8D22018AA7700345E76 /* AdamantApi+Accounts.swift in Sources */, E987024920C2B1F700E393F4 /* AdamantChatsProvider+fakeMessages.swift in Sources */, E9942B84203CBFCE00C163AF /* AdamantQRTools.swift in Sources */, + E9AD78812158EED300742061 /* RichMessageTransaction+CoreDataClass.swift in Sources */, + E94008872114F05B00CD2D67 /* AddressValidationResult.swift in Sources */, + E974D168215D033C003AD7E8 /* ChatCell.swift in Sources */, + 64A223D820F7A08E005157CB /* LskApiService.swift in Sources */, + E94008702114EA6800CD2D67 /* AdamantBalanceFormat.swift in Sources */, E9E7CD8F20026CD300DFC4DB /* AdamantDialogService.swift in Sources */, + E993301E212EF39700CD5200 /* EthTransferViewController.swift in Sources */, E9CAE8DA2018ACD300345E76 /* AdamantApi+Chats.swift in Sources */, E91947B020002393001362F8 /* AdamantApiService.swift in Sources */, E921597B206503000000CA5C /* ButtonsStripeView.swift in Sources */, @@ -1167,34 +1415,43 @@ E9150B9D2066DA210065A985 /* Chatroom+CoreDataClass.swift in Sources */, E9942B80203C058C00C163AF /* QRGeneratorViewController.swift in Sources */, E921597520611A6A0000CA5C /* AdamantReachability.swift in Sources */, + 64A223DA20F7A14B005157CB /* AdamantLskApiService.swift in Sources */, E9150BA22066DA210065A985 /* ChatTransaction+CoreDataProperties.swift in Sources */, 6455E9F321075D8000B2E94C /* AdamantAddressBookService.swift in Sources */, E9204B5020C94C4A00F3B9AB /* Date+humanizedString.swift in Sources */, E9E7CD9120026FA100DFC4DB /* SwinjectDependencies.swift in Sources */, + 64BD2B7520E2814B00E2CD36 /* EthTransaction.swift in Sources */, E9E7CD8B20026B0600DFC4DB /* AccountService.swift in Sources */, + 64F085D920E2D7600006DE68 /* AdmTransactionsViewController.swift in Sources */, + E9D1BE1C211DABE100E86B72 /* WalletPagingItem.swift in Sources */, E9150B992066DA210065A985 /* TransferTransaction+CoreDataClass.swift in Sources */, + E940086E2114AA2E00CD2D67 /* WalletService.swift in Sources */, E9150BA02066DA210065A985 /* MessageTransaction+CoreDataProperties.swift in Sources */, + E9AD78822158EED300742061 /* RichMessageTransaction+CoreDataProperties.swift in Sources */, E9B3D39A201F90570019EB36 /* AccountsProvider.swift in Sources */, E950652320404C84008352E5 /* AdamantUriTools.swift in Sources */, E95F85C7200A9B070070534A /* ChatTableViewCell.swift in Sources */, 6455E9F121075D3600B2E94C /* AddressBookService.swift in Sources */, E983AE2A20E65F3200497E1A /* AccountViewController.swift in Sources */, E9E7CD932002740500DFC4DB /* AdamantAccountService.swift in Sources */, + 64FA53CD20E1300B006783C9 /* EthTransactionsViewController.swift in Sources */, E9147B612050599000145913 /* LoginViewController+QR.swift in Sources */, E9147B6F205088DE00145913 /* LoginViewController+Pinpad.swift in Sources */, E9FAE5E2203ED1AE008D3A6B /* ShareQrViewController.swift in Sources */, - E94E7B06205D48B20042B639 /* TransactionsRoutes.swift in Sources */, E983AE2120E655C500497E1A /* AccountHeaderView.swift in Sources */, 64E8305020F5FEEF006FA590 /* VotesAsset.swift in Sources */, + E971591C2168209800A5F904 /* EthWalletService+RichMessageProviderWithStatusCheck.swift in Sources */, 644EC35220EFA9A300F40C73 /* DelegateRoutes.swift in Sources */, + E926E032213EC43B005E536B /* FullscreenAlertView.swift in Sources */, 644EC35B20EFB8E900F40C73 /* AdamantDelegateCell.swift in Sources */, - E95F85B3200954D00070534A /* ChatModels.xcdatamodeld in Sources */, E90A494B204D9EB8009F6A65 /* AdamantAuthentication.swift in Sources */, E9215973206119FB0000CA5C /* ReachabilityMonitor.swift in Sources */, - E91947B420002809001362F8 /* Account.swift in Sources */, - E9E7CDC22003F5A400DFC4DB /* TransactionsViewController.swift in Sources */, + E91947B420002809001362F8 /* AdamantAccount.swift in Sources */, + E9E7CDC22003F5A400DFC4DB /* TransactionsListViewControllerBase.swift in Sources */, E905D39F204C281400DDB504 /* LoginViewController.swift in Sources */, E9150B9C2066DA210065A985 /* BaseTransaction+CoreDataProperties.swift in Sources */, + E9B1AA592122D59600080A2A /* WalletsRoutes.swift in Sources */, + E971591A21681D6900A5F904 /* TransactionStatus.swift in Sources */, E9E7CDB52002BA6900DFC4DB /* SwinjectedRouter.swift in Sources */, E95F85872008FDBF0070534A /* ChatAsset.swift in Sources */, E9A03FD220DBC0F2007653A1 /* NodeEditorViewController.swift in Sources */, @@ -1203,22 +1460,32 @@ E95F856F2007B61D0070534A /* GetPublicKeyResponse.swift in Sources */, 644EC34D20EFA60900F40C73 /* AdamantApi+Delegates.swift in Sources */, E9E7CDAF2002B8A100DFC4DB /* Router.swift in Sources */, + E940088F2119A9E800CD2D67 /* BigInt+Decimal.swift in Sources */, E9E7CDC72003F6D200DFC4DB /* TransactionTableViewCell.swift in Sources */, + E940088B2114F63000CD2D67 /* NSRegularExpression+adamant.swift in Sources */, E9B3D3A9202082450019EB36 /* AdamantTransfersProvider.swift in Sources */, E9A174B72057F1B3003667CD /* AdamantChatsProvider+backgroundFetch.swift in Sources */, E9E7CDB32002B9FB00DFC4DB /* LoginRoutes.swift in Sources */, E941CCDE20E7B70200C96220 /* WalletCollectionViewCell.swift in Sources */, E9150B9E2066DA210065A985 /* Chatroom+CoreDataProperties.swift in Sources */, + E9AA8BFA212C166600F9249F /* EthWalletService+Send.swift in Sources */, + E9220E0D21988EEA009C9642 /* ChatModels.xcdatamodeld in Sources */, E91947B22000246A001362F8 /* AdamantError.swift in Sources */, E95F85802008C8D70070534A /* ChatsRoutes.swift in Sources */, E9942B87203D9E5100C163AF /* EurekaQRRow.swift in Sources */, E93EFE13200D1156000BB482 /* ChatViewController.swift in Sources */, + E9AA8C02212C5BF500F9249F /* AdmWalletService+Send.swift in Sources */, + E9FEECAC2143CEED007DD7C8 /* TransferMessageSizeCalculator.swift in Sources */, E927171E20C04614002BB9A6 /* UIColor+hex.swift in Sources */, + E99818942120892F0018C84C /* WalletViewControllerBase.swift in Sources */, E9B3D39E201F99F40019EB36 /* DataProvider.swift in Sources */, 643ED0B12109F4BD005A9FDA /* NativeAdamantCore.swift in Sources */, E93EB0A320DA4CCA001F9601 /* Node.swift in Sources */, 645E7B062111DF3A006CC9FD /* Crypto.swift in Sources */, E9E7CDC02003AF6D00DFC4DB /* AdamantCellFactory.swift in Sources */, + E9D1BE1A211DA25300E86B72 /* UIView+constraints.swift in Sources */, + E9DFB71C21624C9200CF8C7C /* AdmTransactionDetailsViewController.swift in Sources */, + E94008722114EACF00CD2D67 /* WalletAccount.swift in Sources */, E93B0D742028B21400126346 /* ChatsProvider.swift in Sources */, E9150BA12066DA210065A985 /* ChatTransaction+CoreDataClass.swift in Sources */, E9CAE8D42018AC1800345E76 /* AdamantApi+Keys.swift in Sources */, @@ -1226,7 +1493,9 @@ E9722066201F42BB004F2AAD /* CoreDataStack.swift in Sources */, E913C8F21FFFA51D001A83F7 /* AppDelegate.swift in Sources */, E905D39B2048A9BD00DDB504 /* KeychainStore.swift in Sources */, + E9DFB720216619F400CF8C7C /* BaseTransaction+TransactionDetails.swift in Sources */, E9E7CDCA20040CC200DFC4DB /* Transaction.swift in Sources */, + E9AA8BFC212C169200F9249F /* EthWalletService+Transfers.swift in Sources */, 649E9A152111B3C200686B01 /* Mnemonic.swift in Sources */, E95F85692006AB9D0070534A /* NormalizedTransaction.swift in Sources */, E9150B9B2066DA210065A985 /* BaseTransaction+CoreDataClass.swift in Sources */, @@ -1235,56 +1504,80 @@ E9150B802066861D0065A985 /* ChatViewController+MessageKit.swift in Sources */, E90055F920ECD86800D0CB2D /* SecurityViewController+StayIn.swift in Sources */, E9204B5220C9762400F3B9AB /* MessageStatus.swift in Sources */, + E993302421369B8A00CD5200 /* RichMessage.swift in Sources */, + E940087B2114ED0600CD2D67 /* EthWalletService.swift in Sources */, E948E03B20235E2300975D6B /* SettingsRoutes.swift in Sources */, E9CAE8D82018ACA700345E76 /* AdamantApi+Transfers.swift in Sources */, E9E7CDCC20040FDC00DFC4DB /* TransactionType.swift in Sources */, E95F85852008CB3A0070534A /* ChatListViewController.swift in Sources */, + E9FEECA62143C300007DD7C8 /* EthWalletService+RichMessageProvider.swift in Sources */, E91947AC20001A9A001362F8 /* ApiService.swift in Sources */, + E993302221354BC300CD5200 /* EthWalletRoutes.swift in Sources */, E9150B9F2066DA210065A985 /* MessageTransaction+CoreDataClass.swift in Sources */, E90055F520EBF5DA00D0CB2D /* AboutViewController.swift in Sources */, E965A53020B594120041A3EA /* AdamantApi+States.swift in Sources */, 64D059FF20D3116B003AD655 /* NodesListViewController.swift in Sources */, E91E5BF220DAF05500B06B3C /* EurekaNodeRow.swift in Sources */, + E99330262136B0E500CD5200 /* TransferViewControllerBase+QR.swift in Sources */, E9150B982066DA210065A985 /* CoreDataAccount+CoreDataProperties.swift in Sources */, + E9B1AA5B21283E0F00080A2A /* AdmTransferViewController.swift in Sources */, + E940086B2114A70600CD2D67 /* LskAccount.swift in Sources */, E9B3D3A1201FA26B0019EB36 /* AdamantAccountsProvider.swift in Sources */, E9FAE5DA203DBFEF008D3A6B /* Comparable+clamped.swift in Sources */, + E94008802114EE2000CD2D67 /* AdmWallet.swift in Sources */, E9A03FDA20DC0B14007653A1 /* NodesSource.swift in Sources */, E9150B972066DA210065A985 /* CoreDataAccount+CoreDataClass.swift in Sources */, + E926E02E213EAABF005E536B /* TransferViewControllerBase+Alert.swift in Sources */, + E9B1AA572121ACC000080A2A /* AdmWalletViewController.swift in Sources */, E95F857A2007F0260070534A /* ServerResponse.swift in Sources */, + E9240BF5215D686500187B09 /* AdmWalletService+RichMessageProvider.swift in Sources */, E9A174B32057EC47003667CD /* BackgroundFetchService.swift in Sources */, E9E7CDBE2003AEFB00DFC4DB /* CellFactory.swift in Sources */, E965A53220B82C850041A3EA /* StateType.swift in Sources */, + E923222621135F9000A7E5AF /* EthAccount.swift in Sources */, E965A53620B8370C0041A3EA /* TransactionAsset.swift in Sources */, E9061B97207501E40011F104 /* AdamantUserInfoKey.swift in Sources */, E9CAE8D62018AC5300345E76 /* AdamantApi+Transactions.swift in Sources */, E93D7ABE2052CEE1005D19DC /* NotificationsService.swift in Sources */, + E940087D2114EDEE00CD2D67 /* EthWallet.swift in Sources */, E972206B201F44CA004F2AAD /* TransfersProvider.swift in Sources */, + E9FEECA421413659007DD7C8 /* RichMessageProvider.swift in Sources */, E9A03FD620DBC8E2007653A1 /* AdamantApi+Peers.swift in Sources */, 644EC34F20EFA77A00F40C73 /* Delegate.swift in Sources */, E9722068201F42CC004F2AAD /* InMemoryCoreDataStack.swift in Sources */, E98FC34420F920BD00032D65 /* UIFont+adamant.swift in Sources */, 644EC35720EFAAB700F40C73 /* DelegatesListViewController.swift in Sources */, E9C51EF12013F18000385EB7 /* NewChatViewController.swift in Sources */, + E9B4E1A8210F079E007E77FC /* DoubleDetailsTableViewCell.swift in Sources */, + E9FEECA92143C371007DD7C8 /* TransferCollectionViewCell.swift in Sources */, E9502740202E257E002C1098 /* RepeaterService.swift in Sources */, E93D7AC02052CF63005D19DC /* AdamantNotificationService.swift in Sources */, E93B0D762028B28E00126346 /* AdamantChatsProvider.swift in Sources */, - E983AE2320E6568C00497E1A /* Wallet.swift in Sources */, + E993302021354B1800CD5200 /* AdmWalletRoutes.swift in Sources */, + 64FA53D120E24942006783C9 /* TransactionDetailsViewControllerBase.swift in Sources */, + 64BD2B7720E2820300E2CD36 /* TransactionDetails.swift in Sources */, + E94008852114EE7500CD2D67 /* LskWalletService.swift in Sources */, + E96E86B821679C120061F80A /* EthTransactionDetailsViewController.swift in Sources */, E90A4943204C5ED6009F6A65 /* EurekaPassphraseRow.swift in Sources */, E965A53420B833A00041A3EA /* StateAsset.swift in Sources */, E9393FA82055C92700EE6F30 /* Decimal+adamant.swift in Sources */, E95F8589200900B10070534A /* ChatType.swift in Sources */, E913C9081FFFA943001A83F7 /* AdamantCore.swift in Sources */, - E9EC342120052ABB00C0E546 /* TransferViewController.swift in Sources */, + E9EC342120052ABB00C0E546 /* TransferViewControllerBase.swift in Sources */, E921534E20EE1E8700C0843F /* EurekaAlertLabelRow.swift in Sources */, E9A03FD420DBC824007653A1 /* NodeVersion.swift in Sources */, + E94008892114F0F700CD2D67 /* AdmWalletService.swift in Sources */, + 64EE46B220FE0C8D00194DDA /* LskTransactionsViewController.swift in Sources */, + E94008832114EE4700CD2D67 /* LskWallet.swift in Sources */, E93EB09F20DA3FA4001F9601 /* NodesEditorRoutes.swift in Sources */, - E9EC34142005178500C0E546 /* TransactionDetailsViewController.swift in Sources */, E98FC34220F9209900032D65 /* UIColor+adamant.swift in Sources */, E948E04C2027679300975D6B /* AdamantFormattingTools.swift in Sources */, E9E7CDB12002B97B00DFC4DB /* AccountRoutes.swift in Sources */, - E95F856B200789450070534A /* JSModels.swift in Sources */, + E9240BF9215D813A00187B09 /* CustomCellDeleage.swift in Sources */, + E9AA8BF82129F13000F9249F /* ComplexTransferViewController.swift in Sources */, E9A174B52057EDCE003667CD /* AdamantTransfersProvider+backgroundFetch.swift in Sources */, E9147B5F20500E9300145913 /* MyLittlePinpad+adamant.swift in Sources */, + E9981896212095CA0018C84C /* EthWalletViewController.swift in Sources */, E98FC34620F9210100032D65 /* Date+adamant.swift in Sources */, E905D39D204C13B900DDB504 /* SecuredStore.swift in Sources */, E98FC34820F921EA00032D65 /* DelegateVote.swift in Sources */, @@ -1298,15 +1591,16 @@ files = ( E9C51ECF200E2D1100385EB7 /* FeeTests.swift in Sources */, E9EC344720066D4A00C0E546 /* AddressValidationTests.swift in Sources */, + E9220E1321988F81009C9642 /* JSAdamantCoreTests.swift in Sources */, E94883E7203F07CD00F6E1B0 /* PassphraseValidation.swift in Sources */, E95F85B7200A4D8F0070534A /* TestTools.swift in Sources */, E95F85BC200A4E670070534A /* ParsingModelsTests.swift in Sources */, + E9220E1521988FCE009C9642 /* JSModels.swift in Sources */, E95F85752007E4790070534A /* HexAndBytesUtilitiesTest.swift in Sources */, - E9220E0421983156009C9642 /* JSAdamantCore.swift in Sources */, - E95F85772007E8EC0070534A /* JSAdamantCoreTests.swift in Sources */, + E9220E1121988F81009C9642 /* JSAdamantCore.swift in Sources */, E95F85712007D98D0070534A /* CurrencyFormatterTests.swift in Sources */, E950652120404BF0008352E5 /* AdamantUriBuilding.swift in Sources */, - E9220E08219879B9009C9642 /* NativeCoreTests.swift in Sources */, + E9220E1421988F81009C9642 /* NativeCoreTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1412,6 +1706,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; }; name = Debug; }; @@ -1465,6 +1760,7 @@ PRODUCT_NAME = ADAMANT; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.2; VALIDATE_PRODUCT = YES; }; name = Release; @@ -1485,7 +1781,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = "e4233bbf-3705-44fe-95b0-e77475672c60"; PROVISIONING_PROFILE_SPECIFIER = "ADAMANT Debug Dev"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = 1; }; name = Debug; @@ -1508,7 +1804,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE = "bedd1b75-2f23-4a85-a0b2-14c424fcff42"; PROVISIONING_PROFILE_SPECIFIER = "ADAMANT Messenger Dev"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = 1; }; name = Release; @@ -1525,7 +1821,7 @@ PRODUCT_BUNDLE_IDENTIFIER = im.adamant.AdamantTests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Adamant.app/Adamant"; }; @@ -1543,7 +1839,7 @@ PRODUCT_BUNDLE_IDENTIFIER = im.adamant.AdamantTests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Adamant.app/Adamant"; }; diff --git a/Adamant.xcodeproj/xcshareddata/xcschemes/Adamant.Dev.BackgroundFetch.xcscheme b/Adamant.xcodeproj/xcshareddata/xcschemes/Adamant.Dev.BackgroundFetch.xcscheme index ef6bec5a9..1f9e9c6e2 100644 --- a/Adamant.xcodeproj/xcshareddata/xcschemes/Adamant.Dev.BackgroundFetch.xcscheme +++ b/Adamant.xcodeproj/xcshareddata/xcschemes/Adamant.Dev.BackgroundFetch.xcscheme @@ -1,6 +1,6 @@ Bool { + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // MARK: 1. Initiating Swinject container = Container() container.registerAdamantServices() @@ -213,16 +223,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func applicationDidEnterBackground(_ application: UIApplication) { repeater.pauseAll() - - // MARK: Save KVS data - backgroundTaskID = UIApplication.shared.beginBackgroundTask { [unowned self] in - UIApplication.shared.endBackgroundTask(self.backgroundTaskID) - self.backgroundTaskID = UIBackgroundTaskInvalid - } - addressBookService.saveIfNeeded() - UIApplication.shared.endBackgroundTask(backgroundTaskID) - self.backgroundTaskID = UIBackgroundTaskInvalid } // MARK: Notifications @@ -331,7 +332,7 @@ extension AppDelegate { container.registerAdamantBackgroundFetchServices() guard let notificationsService = container.resolve(NotificationsService.self) else { - UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplicationBackgroundFetchIntervalNever) + UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalNever) completionHandler(.failed) return } diff --git a/Adamant/Assets/Assets.xcassets/Icons/adamant_token.imageset/Contents.json b/Adamant/Assets/Assets.xcassets/Icons/adamant_token.imageset/Contents.json new file mode 100644 index 000000000..e5abf1e02 --- /dev/null +++ b/Adamant/Assets/Assets.xcassets/Icons/adamant_token.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "adamant_token.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "adamant_token@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "adamant_token@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Adamant/Assets/Assets.xcassets/Icons/adamant_token.imageset/adamant_token.png b/Adamant/Assets/Assets.xcassets/Icons/adamant_token.imageset/adamant_token.png new file mode 100644 index 000000000..fe77f8deb Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/Icons/adamant_token.imageset/adamant_token.png differ diff --git a/Adamant/Assets/Assets.xcassets/Icons/adamant_token.imageset/adamant_token@2x.png b/Adamant/Assets/Assets.xcassets/Icons/adamant_token.imageset/adamant_token@2x.png new file mode 100644 index 000000000..10ba2277b Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/Icons/adamant_token.imageset/adamant_token@2x.png differ diff --git a/Adamant/Assets/Assets.xcassets/Icons/adamant_token.imageset/adamant_token@3x.png b/Adamant/Assets/Assets.xcassets/Icons/adamant_token.imageset/adamant_token@3x.png new file mode 100644 index 000000000..c2e677c66 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/Icons/adamant_token.imageset/adamant_token@3x.png differ diff --git a/Adamant/Assets/Assets.xcassets/Icons/attachment.imageset/Contents.json b/Adamant/Assets/Assets.xcassets/Icons/attachment.imageset/Contents.json new file mode 100644 index 000000000..dc0cd0535 --- /dev/null +++ b/Adamant/Assets/Assets.xcassets/Icons/attachment.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "attachment.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "attachment@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "attachment@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Adamant/Assets/Assets.xcassets/Icons/attachment.imageset/attachment.png b/Adamant/Assets/Assets.xcassets/Icons/attachment.imageset/attachment.png new file mode 100644 index 000000000..6a43d5685 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/Icons/attachment.imageset/attachment.png differ diff --git a/Adamant/Assets/Assets.xcassets/Icons/attachment.imageset/attachment@2x.png b/Adamant/Assets/Assets.xcassets/Icons/attachment.imageset/attachment@2x.png new file mode 100644 index 000000000..1dc44dbff Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/Icons/attachment.imageset/attachment@2x.png differ diff --git a/Adamant/Assets/Assets.xcassets/Icons/attachment.imageset/attachment@3x.png b/Adamant/Assets/Assets.xcassets/Icons/attachment.imageset/attachment@3x.png new file mode 100644 index 000000000..ff9d96d71 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/Icons/attachment.imageset/attachment@3x.png differ diff --git a/Adamant/Assets/Assets.xcassets/Icons/eth_token.imageset/Contents.json b/Adamant/Assets/Assets.xcassets/Icons/eth_token.imageset/Contents.json new file mode 100644 index 000000000..7d0b87990 --- /dev/null +++ b/Adamant/Assets/Assets.xcassets/Icons/eth_token.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "eth_token.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "eth_token@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "eth_token@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Adamant/Assets/Assets.xcassets/Icons/eth_token.imageset/eth_token.png b/Adamant/Assets/Assets.xcassets/Icons/eth_token.imageset/eth_token.png new file mode 100644 index 000000000..9c4cb95c0 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/Icons/eth_token.imageset/eth_token.png differ diff --git a/Adamant/Assets/Assets.xcassets/Icons/eth_token.imageset/eth_token@2x.png b/Adamant/Assets/Assets.xcassets/Icons/eth_token.imageset/eth_token@2x.png new file mode 100644 index 000000000..dc3da9c25 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/Icons/eth_token.imageset/eth_token@2x.png differ diff --git a/Adamant/Assets/Assets.xcassets/Icons/eth_token.imageset/eth_token@3x.png b/Adamant/Assets/Assets.xcassets/Icons/eth_token.imageset/eth_token@3x.png new file mode 100644 index 000000000..140dd4a36 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/Icons/eth_token.imageset/eth_token@3x.png differ diff --git a/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/Contents.json b/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/Contents.json new file mode 100644 index 000000000..850b37580 --- /dev/null +++ b/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "wallet_lsk.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "wallet_lsk@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "wallet_lsk@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/wallet_lsk.png b/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/wallet_lsk.png new file mode 100644 index 000000000..908fda752 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/wallet_lsk.png differ diff --git a/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/wallet_lsk@2x.png b/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/wallet_lsk@2x.png new file mode 100644 index 000000000..c6a0035e6 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/wallet_lsk@2x.png differ diff --git a/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/wallet_lsk@3x.png b/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/wallet_lsk@3x.png new file mode 100644 index 000000000..8044e1a49 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/Icons/wallet_lsk.imageset/wallet_lsk@3x.png differ diff --git a/Adamant/Assets/Assets.xcassets/Swipe actions/Contents.json b/Adamant/Assets/Assets.xcassets/Swipe actions/Contents.json new file mode 100644 index 000000000..da4a164c9 --- /dev/null +++ b/Adamant/Assets/Assets.xcassets/Swipe actions/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Adamant/Assets/Assets.xcassets/Swipe actions/swipe_mark-as-read.imageset/Contents.json b/Adamant/Assets/Assets.xcassets/Swipe actions/swipe_mark-as-read.imageset/Contents.json new file mode 100644 index 000000000..9e863bfc9 --- /dev/null +++ b/Adamant/Assets/Assets.xcassets/Swipe actions/swipe_mark-as-read.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "swipe_mark-as-read.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "swipe_mark-as-read@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "swipe_mark-as-read@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/Adamant/Assets/Assets.xcassets/Swipe actions/swipe_mark-as-read.imageset/swipe_mark-as-read.png b/Adamant/Assets/Assets.xcassets/Swipe actions/swipe_mark-as-read.imageset/swipe_mark-as-read.png new file mode 100644 index 000000000..f62c626d1 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/Swipe actions/swipe_mark-as-read.imageset/swipe_mark-as-read.png differ diff --git a/Adamant/Assets/Assets.xcassets/Swipe actions/swipe_mark-as-read.imageset/swipe_mark-as-read@2x.png b/Adamant/Assets/Assets.xcassets/Swipe actions/swipe_mark-as-read.imageset/swipe_mark-as-read@2x.png new file mode 100644 index 000000000..ac0df8d42 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/Swipe actions/swipe_mark-as-read.imageset/swipe_mark-as-read@2x.png differ diff --git a/Adamant/Assets/Assets.xcassets/Swipe actions/swipe_mark-as-read.imageset/swipe_mark-as-read@3x.png b/Adamant/Assets/Assets.xcassets/Swipe actions/swipe_mark-as-read.imageset/swipe_mark-as-read@3x.png new file mode 100644 index 000000000..4522ff586 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/Swipe actions/swipe_mark-as-read.imageset/swipe_mark-as-read@3x.png differ diff --git a/Adamant/Assets/Assets.xcassets/Swipe actions/swipe_more.imageset/Contents.json b/Adamant/Assets/Assets.xcassets/Swipe actions/swipe_more.imageset/Contents.json new file mode 100644 index 000000000..426aac47d --- /dev/null +++ b/Adamant/Assets/Assets.xcassets/Swipe actions/swipe_more.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "swipe_more.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "swipe_more@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "swipe_more@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/Adamant/Assets/Assets.xcassets/Swipe actions/swipe_more.imageset/swipe_more.png b/Adamant/Assets/Assets.xcassets/Swipe actions/swipe_more.imageset/swipe_more.png new file mode 100644 index 000000000..01ac017b7 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/Swipe actions/swipe_more.imageset/swipe_more.png differ diff --git a/Adamant/Assets/Assets.xcassets/Swipe actions/swipe_more.imageset/swipe_more@2x.png b/Adamant/Assets/Assets.xcassets/Swipe actions/swipe_more.imageset/swipe_more@2x.png new file mode 100644 index 000000000..e51d1d9a0 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/Swipe actions/swipe_more.imageset/swipe_more@2x.png differ diff --git a/Adamant/Assets/Assets.xcassets/Swipe actions/swipe_more.imageset/swipe_more@3x.png b/Adamant/Assets/Assets.xcassets/Swipe actions/swipe_more.imageset/swipe_more@3x.png new file mode 100644 index 000000000..0c74fd0f1 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/Swipe actions/swipe_more.imageset/swipe_more@3x.png differ diff --git a/Adamant/Assets/Assets.xcassets/TransactionStatus/Contents.json b/Adamant/Assets/Assets.xcassets/TransactionStatus/Contents.json new file mode 100644 index 000000000..da4a164c9 --- /dev/null +++ b/Adamant/Assets/Assets.xcassets/TransactionStatus/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Adamant/Assets/Assets.xcassets/TransactionStatus/status_failed.imageset/Contents.json b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_failed.imageset/Contents.json new file mode 100644 index 000000000..126384d40 --- /dev/null +++ b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_failed.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Failed.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Failed@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "Failed@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/Adamant/Assets/Assets.xcassets/TransactionStatus/status_failed.imageset/Failed.png b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_failed.imageset/Failed.png new file mode 100644 index 000000000..8e9145f04 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_failed.imageset/Failed.png differ diff --git a/Adamant/Assets/Assets.xcassets/TransactionStatus/status_failed.imageset/Failed@2x.png b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_failed.imageset/Failed@2x.png new file mode 100644 index 000000000..596d027ee Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_failed.imageset/Failed@2x.png differ diff --git a/Adamant/Assets/Assets.xcassets/TransactionStatus/status_failed.imageset/Failed@3x.png b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_failed.imageset/Failed@3x.png new file mode 100644 index 000000000..d25c4d980 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_failed.imageset/Failed@3x.png differ diff --git a/Adamant/Assets/Assets.xcassets/TransactionStatus/status_pending.imageset/Contents.json b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_pending.imageset/Contents.json new file mode 100644 index 000000000..863714380 --- /dev/null +++ b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_pending.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Pending.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Pending@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "Pending@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/Adamant/Assets/Assets.xcassets/TransactionStatus/status_pending.imageset/Pending.png b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_pending.imageset/Pending.png new file mode 100644 index 000000000..975b35ce1 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_pending.imageset/Pending.png differ diff --git a/Adamant/Assets/Assets.xcassets/TransactionStatus/status_pending.imageset/Pending@2x.png b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_pending.imageset/Pending@2x.png new file mode 100644 index 000000000..6a6797f76 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_pending.imageset/Pending@2x.png differ diff --git a/Adamant/Assets/Assets.xcassets/TransactionStatus/status_pending.imageset/Pending@3x.png b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_pending.imageset/Pending@3x.png new file mode 100644 index 000000000..04f6b1ff4 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_pending.imageset/Pending@3x.png differ diff --git a/Adamant/Assets/Assets.xcassets/TransactionStatus/status_success.imageset/Contents.json b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_success.imageset/Contents.json new file mode 100644 index 000000000..6d01193ce --- /dev/null +++ b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_success.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Success.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Success@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "Success@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/Adamant/Assets/Assets.xcassets/TransactionStatus/status_success.imageset/Success.png b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_success.imageset/Success.png new file mode 100644 index 000000000..a5302f693 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_success.imageset/Success.png differ diff --git a/Adamant/Assets/Assets.xcassets/TransactionStatus/status_success.imageset/Success@2x.png b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_success.imageset/Success@2x.png new file mode 100644 index 000000000..552bad04a Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_success.imageset/Success@2x.png differ diff --git a/Adamant/Assets/Assets.xcassets/TransactionStatus/status_success.imageset/Success@3x.png b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_success.imageset/Success@3x.png new file mode 100644 index 000000000..bcd5a2b96 Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_success.imageset/Success@3x.png differ diff --git a/Adamant/Assets/Assets.xcassets/TransactionStatus/status_updating.imageset/Contents.json b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_updating.imageset/Contents.json new file mode 100644 index 000000000..3349827f8 --- /dev/null +++ b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_updating.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "Updating.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Updating@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "Updating@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/Adamant/Assets/Assets.xcassets/TransactionStatus/status_updating.imageset/Updating.png b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_updating.imageset/Updating.png new file mode 100644 index 000000000..40278c8ca Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_updating.imageset/Updating.png differ diff --git a/Adamant/Assets/Assets.xcassets/TransactionStatus/status_updating.imageset/Updating@2x.png b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_updating.imageset/Updating@2x.png new file mode 100644 index 000000000..7def3f4fe Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_updating.imageset/Updating@2x.png differ diff --git a/Adamant/Assets/Assets.xcassets/TransactionStatus/status_updating.imageset/Updating@3x.png b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_updating.imageset/Updating@3x.png new file mode 100644 index 000000000..6b9a6821e Binary files /dev/null and b/Adamant/Assets/Assets.xcassets/TransactionStatus/status_updating.imageset/Updating@3x.png differ diff --git a/Adamant/Assets/eth_l18n.strings b/Adamant/Assets/eth_l18n.strings new file mode 100644 index 000000000..4b2b3d9f3 --- /dev/null +++ b/Adamant/Assets/eth_l18n.strings @@ -0,0 +1,16 @@ +/* + eth_l18n.strings + Adamant + + Created by Anton Boyarkin on 27/06/2018. + Copyright © 2018 Adamant. All rights reserved. +*/ + +/* TransactionList: 'Transactions not found' message. */ +"TransactionListScene.Error.NotFound" = "Transactions not found"; + +/* TransactionList: 'Transactions not found' localize error message from API. */ +"No transactions found" = "Transactions not found"; + +/* Transaction details: 'Requesting Data' progress message. */ +"TransactionDetailsScene.RequestingData" = "Requesting Data.."; diff --git a/Adamant/Assets/l18n/de.lproj/Localizable.strings b/Adamant/Assets/l18n/de.lproj/Localizable.strings index 49e35255c..8694f0d0e 100755 --- a/Adamant/Assets/l18n/de.lproj/Localizable.strings +++ b/Adamant/Assets/l18n/de.lproj/Localizable.strings @@ -76,6 +76,12 @@ /* AccountsProvider: Address not valid error, %@ for address */ "AccountsProvider.Error.AddressNotValidFormat" = "Ungültige Adresse: %@"; +/* AccountService: Alert title. Changes in version 1.2 */ +"AccountService.update.v12.title" = "Version 1.2 update: Cryptowallets"; + +/* AccountService: Alert message. Changes in version 1.2, notify user that he needs to relogin to initiate eth & lsk wallets */ +"AccountService.update.v12.message" = "You need to relogin to initiate Ethereum and Lisk wallets"; + /* Login: user typed in invalid passphrase */ "AccountServiceError.InvalidPassphrase" = "Falsche Passphrase"; @@ -112,6 +118,9 @@ /* Account tab: 'Send tokens' button */ "AccountTab.Row.SendTokens" = "Tokens senden"; +/* Account tab: 'Address' row */ +"AccountTab.Row.Address" = "Addresse"; + /* Account tab: 'Get free tokens' button */ "AccountTab.Row.FreeTokens" = "Kostenlose ADM Tokens"; @@ -127,11 +136,14 @@ /* Account tab: Application section title */ "AccountTab.Section.Application" = "Application"; -/* Account tab: Adamant wallet section */ -"AccountTab.Sections.adamant_wallet" = "ADAMANT Wallet"; +/* Account tab: Adamant wallet */ +"AccountTab.Wallets.adamant_wallet" = "Adamant Wallet"; -/* Account tab: Ethereum wallet section */ -"AccountTab.Sections.ethereum_wallet" = "Ethereum Wallet"; +/* Account tab: Ethereum wallet */ +"AccountTab.Wallets.ethereum_wallet" = "Ethereum Wallet"; + +/* Account tab: Lisk wallet */ +"AccountTab.Wallets.lisk_wallet" = "Lisk Wallet"; /* Account page: scene title */ "AccountTab.Title" = "Konto"; @@ -143,7 +155,7 @@ "ADAMANT" = "ADAMANT"; /* AddressBookService: Not enought money to save address into blockchain */ -"AddressBookService.Error.NotEnoughtMoney" = "Not enought tokens to store data in Blockchain"; +"AddressBookService.Error.NotEnoughtMoney" = "Nicht genug Tokens, um Daten auf der Blockchain zu speichern"; /* Application: Failed to send deviceToken to ANS error format. %@ for error description */ "Application.deviceTokenErrorFormat" = "Anmeldefehler in ANS: %@"; @@ -157,9 +169,6 @@ /* Serious internal error: Error parsing response */ "ApiService.InternalError.ParsingFailed" = "Parsing fehlgeschlagen. Bericht senden"; -/* Unknown internal error */ -"ApiService.InternalError.UnknownError" = "Unbekannter Fehler. Bericht senden"; - /* Eureka forms Cancel button */ "Cancel" = "Abbrechen"; @@ -218,10 +227,10 @@ "ChatScene.tapForDetails" = "Tippen Sie für Details"; /* Chat: Body for actions menu */ -"ChatScene.Actions.Body" = "New name for %@"; +"ChatScene.Actions.Body" = "Neuer Name für %@"; /* Chat: 'Rename' action in actions menu */ -"ChatScene.Actions.Rename" = "Rename"; +"ChatScene.Actions.Rename" = "Umbenennen"; /* Chat: 'Name' field in actions menu */ "ChatScene.Actions.NamePlaceholder" = "Name"; @@ -310,6 +319,9 @@ /* Shared error: User not logged */ "Error.UserNotLogged" = "Sie sind nicht eingeloggt"; +/* Shared unknown error */ +"Error.UnknownError" = "Unbekannter Fehler. Bericht senden"; + /* Login: Notify user, that he disabled camera in settings, and need to authorize application. */ "LoginScene.Error.AuthorizeCamera" = "Sie müssen die Kamera freigeben, damit ADAMANT die Gerätekamera verwenden kann"; @@ -553,6 +565,18 @@ /* TransactionList: scene title */ "TransactionListScene.Title" = "Transaktionen"; +/* Transaction status: updating in progress */ +"TransactionStatus.Updating" = "Updating..."; + +/* Transaction status: transaction is pending */ +"TransactionStatus.Pending" = "Pending"; + +/* Transaction status: success */ +"TransactionStatus.Success" = "Success"; + +/* Transaction status: transaction failed */ +"TransactionStatus.Failed" = "Failed"; + /* Transaction details: scene title */ "TransactionDetailsScene.Title" = "Details"; @@ -562,6 +586,9 @@ /* Transaction details: Block id row. */ "TransactionDetailsScene.Row.Block" = "Block"; +/* Transaction details: Transaction delivery status. */ +"TransactionDetailsScene.Row.Status" = "Transaction status"; + /* Transaction details: confirmations row. */ "TransactionDetailsScene.Row.Confirmations" = "Bestätigungen"; @@ -589,6 +616,9 @@ /* Export transaction: 'Share transaction URL' button */ "TransactionDetailsScene.Share.URL" = "URL"; +/* Transaction details: 'Your address' flag. */ +"TransactionDetailsScene.YourAddress" = "Sie"; + /* TransfersProvider: Transaction not found error. %@ for transaction's ID */ "TransfersProvider.Error.TransactionNotFoundFormat" = "Transaktion mit der ID %@ nicht gefunden"; diff --git a/Adamant/Assets/l18n/en.lproj/Localizable.strings b/Adamant/Assets/l18n/en.lproj/Localizable.strings index 1692a8cb8..e3a15db40 100755 --- a/Adamant/Assets/l18n/en.lproj/Localizable.strings +++ b/Adamant/Assets/l18n/en.lproj/Localizable.strings @@ -76,6 +76,12 @@ /* AccountsProvider: Address not valid error, %@ for address */ "AccountsProvider.Error.AddressNotValidFormat" = "Invalid address: %@"; +/* AccountService: Alert title. Changes in version 1.2 */ +"AccountService.update.v12.title" = "Version 1.2 update: Cryptowallets"; + +/* AccountService: Alert message. Changes in version 1.2, notify user that he needs to relogin to initiate eth & lsk wallets */ +"AccountService.update.v12.message" = "You need to relogin to initiate Ethereum and Lisk wallets"; + /* Login: user typed in invalid passphrase */ "AccountServiceError.InvalidPassphrase" = "Wrong passphrase"; @@ -109,6 +115,9 @@ /* Account tab: 'Send tokens' button */ "AccountTab.Row.SendTokens" = "Send Tokens"; +/* Account tab: 'Address' row */ +"AccountTab.Row.Address" = "Address"; + /* Account tab: 'Get free tokens' button */ "AccountTab.Row.FreeTokens" = "Free ADM tokens"; @@ -124,11 +133,14 @@ /* Account tab: Application section title */ "AccountTab.Section.Application" = "Application"; -/* Account tab: Adamant wallet section */ -"AccountTab.Sections.adamant_wallet" = "ADAMANT Wallet"; +/* Account tab: Adamant wallet */ +"AccountTab.Wallets.adamant_wallet" = "Adamant Wallet"; -/* Account tab: Ethereum wallet section */ -"AccountTab.Sections.ethereum_wallet" = "Ethereum Wallet"; +/* Account tab: Ethereum wallet */ +"AccountTab.Wallets.ethereum_wallet" = "Ethereum Wallet"; + +/* Account tab: Lisk wallet */ +"AccountTab.Wallets.lisk_wallet" = "Lisk Wallet"; /* Account page: scene title */ "AccountTab.Title" = "Account"; @@ -140,7 +152,7 @@ "ADAMANT" = "ADAMANT"; /* AddressBookService: Not enought money to save address into blockchain */ -"AddressBookService.Error.NotEnoughtMoney" = "Nicht genug Tokens, um Daten auf der Blockchain zu speichern"; +"AddressBookService.Error.NotEnoughtMoney" = "Not enought tokens to store data in Blockchain"; /* Application: Failed to send deviceToken to ANS error format. %@ for error description */ "Application.deviceTokenErrorFormat" = "Failed to register in ANS: %@"; @@ -154,9 +166,6 @@ /* Serious internal error: Error parsing response */ "ApiService.InternalError.ParsingFailed" = "Parsing failed. Report a bug"; -/* Unknown internal error */ -"ApiService.InternalError.UnknownError" = "Unknown error. Report a bug"; - /* Eureka forms Cancel button */ "Cancel" = "Cancel"; @@ -215,10 +224,10 @@ "ChatScene.MessageStatus.Pending" = "Pending"; /* Chat: Body for actions menu */ -"ChatScene.Actions.Body" = "Neuer Name für %@"; +"ChatScene.Actions.Body" = "New name for %@"; /* Chat: 'Rename' action in actions menu */ -"ChatScene.Actions.Rename" = "Umbenennen"; +"ChatScene.Actions.Rename" = "Rename"; /* Chat: 'Name' field in actions menu */ "ChatScene.Actions.NamePlaceholder" = "Name"; @@ -307,6 +316,9 @@ /* Shared error: User not logged */ "Error.UserNotLogged" = "User is not logged in"; +/* Shared unknown error */ +"Error.UnknownError" = "Unknown error. Report a bug"; + /* Login: Notify user, that he disabled camera in settings, and need to authorize application. */ "LoginScene.Error.AuthorizeCamera" = "You need to authorize ADAMANT to use device's camera"; @@ -595,6 +607,24 @@ /* TransactionList: Start Chat button */ "TransactionListScene.StartChat" = "Start Chat"; +/* TransactionList: 'Transactions not found' message. */ +"TransactionListScene.Error.NotFound" = "Transactions not found"; + +/* TransactionList: 'Transactions not found' localize error message from API. */ +"No transactions found" = "Transactions not found"; + +/* Transaction status: updating in progress */ +"TransactionStatus.Updating" = "Updating..."; + +/* Transaction status: transaction is pending */ +"TransactionStatus.Pending" = "Pending"; + +/* Transaction status: success */ +"TransactionStatus.Success" = "Success"; + +/* Transaction status: transaction failed */ +"TransactionStatus.Failed" = "Failed"; + /* Transaction details: scene title */ "TransactionDetailsScene.Title" = "Details"; @@ -604,6 +634,9 @@ /* Transaction details: Block id row. */ "TransactionDetailsScene.Row.Block" = "Block"; +/* Transaction details: Transaction delivery status. */ +"TransactionDetailsScene.Row.Status" = "Transaction status"; + /* Transaction details: confirmations row. */ "TransactionDetailsScene.Row.Confirmations" = "Confirmations"; @@ -631,6 +664,12 @@ /* Export transaction: 'Share transaction URL' button */ "TransactionDetailsScene.Share.URL" = "URL"; +/* Transaction details: 'Requesting Data' progress message. */ +"TransactionDetailsScene.RequestingData" = "Requesting Data.."; + +/* Transaction details: 'Your address' flag. */ +"TransactionDetailsScene.YourAddress" = "You"; + /* TransfersProvider: Transaction not found error. %@ for transaction's ID */ "TransfersProvider.Error.TransactionNotFoundFormat" = "Transaction with id %@ not found"; @@ -673,6 +712,9 @@ /* Transfer: transfer fee */ "TransferScene.Row.TransactionFee" = "Fee"; +/* Transfer: comment for transfer in chat */ +"TransferScene.Row.Comments" = "Comments"; + /* Transfer: 'Transfer info' section */ "TransferScene.Section.TransferInfo" = "Transfer Info"; @@ -682,6 +724,9 @@ /* Transfer: Confirm transfer alert: Send tokens button */ "TransferScene.Send" = "Send"; +/* Transfer: Confirm transfer alert: 'Can't Undo' message */ +"TransferScene.CantUndo" = "You can't undo this action."; + /* 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'. */ "TransferScene.SendConfirmFormat" = "Send %1$@ to %2$@?"; diff --git a/Adamant/Assets/l18n/ru.lproj/Localizable.strings b/Adamant/Assets/l18n/ru.lproj/Localizable.strings index 35f71f9d4..94b148aca 100644 --- a/Adamant/Assets/l18n/ru.lproj/Localizable.strings +++ b/Adamant/Assets/l18n/ru.lproj/Localizable.strings @@ -76,6 +76,12 @@ /* AccountsProvider: Address not valid error, %@ for address */ "AccountsProvider.Error.AddressNotValidFormat" = "Неправильный адрес: %@"; +/* AccountService: Alert title. Changes in version 1.2 */ +"AccountService.update.v12.title" = "Новое в версии 1.2: Криптокошельки"; + +/* AccountService: Alert message. Changes in version 1.2, notify user that he needs to relogin to initiate eth & lsk wallets */ +"AccountService.update.v12.message" = "Необходимо снова зайти в приложение чтобы создать Ethereum и Lisk кошельки"; + /* Login: user typed in invalid passphrase */ "AccountServiceError.InvalidPassphrase" = "Неправильный пароль"; @@ -109,6 +115,9 @@ /* Account tab: 'Send tokens' button */ "AccountTab.Row.SendTokens" = "Отправить токены"; +/* Account tab: 'Address' row */ +"AccountTab.Row.Address" = "Адрес"; + /* Account tab: 'Get free tokens' button */ "AccountTab.Row.FreeTokens" = "Бесплатные ADM-токены"; @@ -124,11 +133,14 @@ /* Account tab: Application section title */ "AccountTab.Section.Application" = "Приложение"; -/* Account tab: Adamant wallet section */ -"AccountTab.Sections.adamant_wallet" = "ADAMANT кошелёк"; +/* Account tab: Adamant wallet */ +"AccountTab.Wallets.adamant_wallet" = "Adamant кошелёк"; -/* Account tab: Ethereum wallet section */ -"AccountTab.Sections.ethereum_wallet" = "Ethereum кошелёк"; +/* Account tab: Ethereum wallet */ +"AccountTab.Wallets.ethereum_wallet" = "Ethereum кошелёк"; + +/* Account tab: Lisk wallet */ +"AccountTab.Wallets.lisk_wallet" = "Lisk кошелёк"; /* Account tab: Delegates section title */ "AccountTab.Section.Delegates" = "Делегаты"; @@ -154,9 +166,6 @@ /* Serious internal error: Error parsing response */ "ApiService.InternalError.ParsingFailed" = "Не удалось разобрать ответ узла блокчена. Сообщите разработчикам"; -/* Unknown internal error */ -"ApiService.InternalError.UnknownError" = "Неизвестная ошибка"; - /* Eureka forms Cancel button */ "Cancel" = "Отмена"; @@ -307,6 +316,9 @@ /* Shared error: User not logged */ "Error.UserNotLogged" = "Не выполнен вход"; +/* Shared unknown error */ +"Error.UnknownError" = "Неизвестная ошибка"; + /* Login: Notify user, that he disabled camera in settings, and need to authorize application. */ "LoginScene.Error.AuthorizeCamera" = "Для чтения QR-кодов необходимо разрешить доступ к камере"; @@ -595,6 +607,24 @@ /* TransactionList: Start Chat button */ "TransactionListScene.StartChat" = "Начать чат"; +/* TransactionList: 'Transactions not found' message. */ +"TransactionListScene.Error.NotFound" = "Транзакции не найдены"; + +/* TransactionList: 'Transactions not found' localize error message from API. */ +"No transactions found" = "Транзакции не найдены"; + +/* Transaction status: updating in progress */ +"TransactionStatus.Updating" = "Обновление..."; + +/* Transaction status: transaction is pending */ +"TransactionStatus.Pending" = "Ожидается"; + +/* Transaction status: success */ +"TransactionStatus.Success" = "Успешно"; + +/* Transaction status: transaction failed */ +"TransactionStatus.Failed" = "Ошибка"; + /* Transaction details: scene title */ "TransactionDetailsScene.Title" = "Подробности"; @@ -604,6 +634,9 @@ /* Transaction details: Block id row. */ "TransactionDetailsScene.Row.Block" = "Блок"; +/* Transaction details: Transaction delivery status. */ +"TransactionDetailsScene.Row.Status" = "Состояние"; + /* Transaction details: confirmations row. */ "TransactionDetailsScene.Row.Confirmations" = "Подтверждения"; @@ -631,6 +664,12 @@ /* Export transaction: 'Share transaction URL' button */ "TransactionDetailsScene.Share.URL" = "URL"; +/* Transaction details: 'Requesting Data' progress message. */ +"TransactionDetailsScene.RequestingData" = "Запрашиваем данные.."; + +/* Transaction details: 'Your address' flag. */ +"TransactionDetailsScene.YourAddress" = "Вы"; + /* TransfersProvider: Transaction not found error. %@ for transaction's ID */ "TransfersProvider.Error.TransactionNotFoundFormat" = "Транзакция с идентификатором %@ не найдена"; @@ -673,6 +712,9 @@ /* Transfer: transfer fee */ "TransferScene.Row.TransactionFee" = "Комиссия"; +/* Transfer: comment for transfer in chat */ +"TransferScene.Row.Comments" = "Комментарии"; + /* Transfer: 'Transfer info' section */ "TransferScene.Section.TransferInfo" = "Перевод"; @@ -682,6 +724,9 @@ /* Transfer: Confirm transfer alert: Send tokens button */ "TransferScene.Send" = "Отправить"; +/* Transfer: Confirm transfer alert: 'Can't Undo' message */ +"TransferScene.CantUndo" = "Вы не сможете отменить это действие."; + /* 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'. */ "TransferScene.SendConfirmFormat" = "Отправить %1$@ получателю %2$@?"; diff --git a/Adamant/CoreData/BaseTransaction+CoreDataClass.swift b/Adamant/CoreData/BaseTransaction+CoreDataClass.swift index b9503623c..0de737909 100644 --- a/Adamant/CoreData/BaseTransaction+CoreDataClass.swift +++ b/Adamant/CoreData/BaseTransaction+CoreDataClass.swift @@ -12,5 +12,7 @@ import CoreData @objc(BaseTransaction) public class BaseTransaction: NSManagedObject { - + var transactionStatus: TransactionStatus? { + return nil + } } diff --git a/Adamant/CoreData/BaseTransaction+TransactionDetails.swift b/Adamant/CoreData/BaseTransaction+TransactionDetails.swift new file mode 100644 index 000000000..25c01bbf5 --- /dev/null +++ b/Adamant/CoreData/BaseTransaction+TransactionDetails.swift @@ -0,0 +1,42 @@ +// +// BaseTransaction+TransactionDetails.swift +// Adamant +// +// Created by Anokhov Pavel on 04.10.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation + +extension BaseTransaction: TransactionDetails { + var id: String { return transactionId ?? "" } + var senderAddress: String { return senderId ?? "" } + var recipientAddress: String { return self.recipientId ?? "" } + var blockValue: String? { return self.blockId } + var confirmationsValue: String? { return String(confirmations) } + var dateValue: Date? { return date as Date? } + + var amountValue: Decimal { + if let amount = self.amount { + return amount.decimalValue + } else { + return 0 + } + } + + var feeValue: Decimal { + if let fee = self.fee { + return fee.decimalValue + } else { + return 0 + } + } + + var block: UInt { + if let raw = blockId, let id = UInt(raw) { + return id + } else { + return 0 + } + } +} diff --git a/Adamant/CoreData/ChatModels.xcdatamodeld/ChatModels.xcdatamodel/contents b/Adamant/CoreData/ChatModels.xcdatamodeld/ChatModels.xcdatamodel/contents index b0166528a..318bdb5e3 100644 --- a/Adamant/CoreData/ChatModels.xcdatamodeld/ChatModels.xcdatamodel/contents +++ b/Adamant/CoreData/ChatModels.xcdatamodeld/ChatModels.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -24,6 +24,7 @@ + @@ -40,19 +41,24 @@ - + + + + + - + - + + \ No newline at end of file diff --git a/Adamant/CoreData/ChatTransaction+CoreDataClass.swift b/Adamant/CoreData/ChatTransaction+CoreDataClass.swift index b7496af12..502a3e4da 100644 --- a/Adamant/CoreData/ChatTransaction+CoreDataClass.swift +++ b/Adamant/CoreData/ChatTransaction+CoreDataClass.swift @@ -16,4 +16,8 @@ public class ChatTransaction: BaseTransaction { get { return MessageStatus(rawValue: self.status) ?? .failed } set { self.status = newValue.rawValue } } + + func serializedMessage() -> String? { + fatalError("You must implement serializedMessage in ChatTransaction classes") + } } diff --git a/Adamant/CoreData/ChatTransaction+CoreDataProperties.swift b/Adamant/CoreData/ChatTransaction+CoreDataProperties.swift index 1ff8c3f94..8b7a56808 100644 --- a/Adamant/CoreData/ChatTransaction+CoreDataProperties.swift +++ b/Adamant/CoreData/ChatTransaction+CoreDataProperties.swift @@ -2,7 +2,7 @@ // ChatTransaction+CoreDataProperties.swift // Adamant // -// Created by Anokhov Pavel on 07.06.2018. +// Created by Anokhov Pavel on 24.09.2018. // Copyright © 2018 Adamant. All rights reserved. // // @@ -20,6 +20,7 @@ extension ChatTransaction { @NSManaged public var isUnread: Bool @NSManaged public var silentNotification: Bool @NSManaged public var status: Int16 + @NSManaged public var isConfirmed: Bool @NSManaged public var chatroom: Chatroom? @NSManaged public var lastIn: Chatroom? diff --git a/Adamant/CoreData/CoreDataAccount+CoreDataProperties.swift b/Adamant/CoreData/CoreDataAccount+CoreDataProperties.swift index 068a0adad..52afbaed9 100644 --- a/Adamant/CoreData/CoreDataAccount+CoreDataProperties.swift +++ b/Adamant/CoreData/CoreDataAccount+CoreDataProperties.swift @@ -21,7 +21,7 @@ extension CoreDataAccount { @NSManaged public var avatar: String? @NSManaged public var name: String? @NSManaged public var publicKey: String? - @NSManaged public var isSystem: Bool + @NSManaged public var isSystem: Bool @NSManaged public var chatroom: Chatroom? @NSManaged public var transfers: NSSet? diff --git a/Adamant/CoreData/MessageTransaction+CoreDataClass.swift b/Adamant/CoreData/MessageTransaction+CoreDataClass.swift index 2ea4ea537..bd6712a26 100644 --- a/Adamant/CoreData/MessageTransaction+CoreDataClass.swift +++ b/Adamant/CoreData/MessageTransaction+CoreDataClass.swift @@ -13,4 +13,8 @@ import CoreData @objc(MessageTransaction) public class MessageTransaction: ChatTransaction { static let entityName = "MessageTransaction" + + override func serializedMessage() -> String? { + return message + } } diff --git a/Adamant/CoreData/MessageTransaction+CoreDataProperties.swift b/Adamant/CoreData/MessageTransaction+CoreDataProperties.swift index 7558288d2..31252778a 100644 --- a/Adamant/CoreData/MessageTransaction+CoreDataProperties.swift +++ b/Adamant/CoreData/MessageTransaction+CoreDataProperties.swift @@ -2,7 +2,7 @@ // MessageTransaction+CoreDataProperties.swift // Adamant // -// Created by Anokhov Pavel on 07.06.2018. +// Created by Anokhov Pavel on 24.09.2018. // Copyright © 2018 Adamant. All rights reserved. // // @@ -17,7 +17,6 @@ extension MessageTransaction { return NSFetchRequest(entityName: "MessageTransaction") } - @NSManaged public var isConfirmed: Bool @NSManaged public var isMarkdown: Bool @NSManaged public var message: String? diff --git a/Adamant/CoreData/RichMessageTransaction+CoreDataClass.swift b/Adamant/CoreData/RichMessageTransaction+CoreDataClass.swift new file mode 100644 index 000000000..ba824d5de --- /dev/null +++ b/Adamant/CoreData/RichMessageTransaction+CoreDataClass.swift @@ -0,0 +1,45 @@ +// +// RichMessageTransaction+CoreDataClass.swift +// Adamant +// +// Created by Anokhov Pavel on 24.09.2018. +// Copyright © 2018 Adamant. All rights reserved. +// +// + +import Foundation +import CoreData +import MessageKit + +@objc(RichMessageTransaction) +public class RichMessageTransaction: ChatTransaction { + static let entityName = "RichMessageTransaction" + + override func serializedMessage() -> String? { + if let richContent = richContent, let data = try? JSONEncoder().encode(richContent), let raw = String(data: data, encoding: String.Encoding.utf8) { + return raw + } else { + return nil + } + } + + override var transactionStatus: TransactionStatus? { + get { + if let raw = transferStatusRaw { + return TransactionStatus(rawValue: raw.int16Value) + } else { + return nil + } + } + set { + if let raw = newValue { + transferStatusRaw = raw.rawValue as NSNumber + } else { + transferStatusRaw = nil + } + } + } + + // Hack? Yes. So? + public var kind: MessageKind = .text("?") +} diff --git a/Adamant/CoreData/RichMessageTransaction+CoreDataProperties.swift b/Adamant/CoreData/RichMessageTransaction+CoreDataProperties.swift new file mode 100644 index 000000000..42b0c54ea --- /dev/null +++ b/Adamant/CoreData/RichMessageTransaction+CoreDataProperties.swift @@ -0,0 +1,24 @@ +// +// RichMessageTransaction+CoreDataProperties.swift +// Adamant +// +// Created by Anokhov Pavel on 06.10.2018. +// Copyright © 2018 Adamant. All rights reserved. +// +// + +import Foundation +import CoreData + + +extension RichMessageTransaction { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "RichMessageTransaction") + } + + @NSManaged public var richContent: [String:String]? + @NSManaged public var richType: String? + @NSManaged public var transferStatusRaw: NSNumber? + +} diff --git a/Adamant/Helpers/AdamantBalanceFormat.swift b/Adamant/Helpers/AdamantBalanceFormat.swift new file mode 100644 index 000000000..67f0161ba --- /dev/null +++ b/Adamant/Helpers/AdamantBalanceFormat.swift @@ -0,0 +1,98 @@ +// +// AdamantBalanceFormat.swift +// Adamant +// +// Created by Anokhov Pavel on 03.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation + +// MARK: - Formatters + +/// - full: 8 digits after the decimal point +/// - compact: 4 digits after the decimal point +/// - short: 2 digits after the decimal point +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 + + + // MARK: Formatters + + static func currencyFormatter(for format: AdamantBalanceFormat, currencySymbol symbol: String?) -> NumberFormatter { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.roundingMode = .floor + + let positiveFormat: String + + switch format { + case .full: positiveFormat = "#.########" + case .compact: positiveFormat = "#.####" + case .short: positiveFormat = "#.##" + } + + if let symbol = symbol { + formatter.positiveFormat = "\(positiveFormat) \(symbol)" + } else { + formatter.positiveFormat = positiveFormat + } + + return formatter + } + + static var currencyFormatterFull: NumberFormatter = { + return currencyFormatter(for: .full, currencySymbol: nil) + }() + + static var currencyFormatterCompact: NumberFormatter = { + return currencyFormatter(for: .compact, currencySymbol: nil) + }() + + static var currencyFormatterShort: NumberFormatter = { + return currencyFormatter(for: .short, currencySymbol: nil) + }() + + + // MARK: Methods + + var defaultFormatter: NumberFormatter { + switch self { + case .full: return AdamantBalanceFormat.currencyFormatterFull + case .compact: return AdamantBalanceFormat.currencyFormatterCompact + case .short: return AdamantBalanceFormat.currencyFormatterShort + } + } + + func format(_ value: Decimal, withCurrencySymbol symbol: String? = nil) -> String { + if let symbol = symbol { + return "\(defaultFormatter.string(fromDecimal: value)!) \(symbol)" + } else { + return defaultFormatter.string(fromDecimal: value)! + } + } + + func format(_ value: Double, withCurrencySymbol symbol: String? = nil) -> String { + if let symbol = symbol { + return "\(defaultFormatter.string(from: NSNumber(floatLiteral: value))!) \(symbol)" + } else { + return defaultFormatter.string(from: NSNumber(floatLiteral: value))! + } + } +} + + +// MARK: - Helper +extension NumberFormatter { + func string(fromDecimal decimal: Decimal) -> String? { + return string(from: decimal as NSNumber) + } +} diff --git a/Adamant/Helpers/AddressValidationResult.swift b/Adamant/Helpers/AddressValidationResult.swift new file mode 100644 index 000000000..6ac5ec4b8 --- /dev/null +++ b/Adamant/Helpers/AddressValidationResult.swift @@ -0,0 +1,15 @@ +// +// AddressValidationResult.swift +// Adamant +// +// Created by Anokhov Pavel on 03.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation + +enum AddressValidationResult { + case valid + case system + case invalid +} diff --git a/Adamant/Helpers/BigInt+Decimal.swift b/Adamant/Helpers/BigInt+Decimal.swift new file mode 100644 index 000000000..67a268afb --- /dev/null +++ b/Adamant/Helpers/BigInt+Decimal.swift @@ -0,0 +1,34 @@ +// +// BigInt+Decimal.swift +// Adamant +// +// Created by Anokhov Pavel on 07.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import BigInt + +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 { + return decim + } + } +} + +extension BigUInt { + func asDecimal(exponent: Int) -> Decimal { + let decim = Decimal(floatLiteral: Double(self)) + + if exponent != 0 { + return Decimal(sign: .plus, exponent: exponent, significand: decim) + } else { + return decim + } + } +} diff --git a/Adamant/Helpers/Date+humanizedString.swift b/Adamant/Helpers/Date+humanizedString.swift index 23fb9f87f..d09078d12 100644 --- a/Adamant/Helpers/Date+humanizedString.swift +++ b/Adamant/Helpers/Date+humanizedString.swift @@ -11,7 +11,7 @@ import DateToolsSwift extension Date { /// Returns readable date with time. - func humanizedDateTime() -> String { + func humanizedDateTime(withWeekday: Bool = true) -> String { if yearsAgo < 1 { let dateString: String if isToday { @@ -23,7 +23,7 @@ extension Date { it will display something like '6 hours ago' */ dateString = NSLocalizedString("Yesterday", tableName: "DateTools", bundle: Bundle.dateToolsBundle(), value: "", comment: "") - } else if 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 dateString = Date.formatterDayMonth.string(from: self) diff --git a/Adamant/Helpers/Decimal+adamant.swift b/Adamant/Helpers/Decimal+adamant.swift index 855b52f93..061d38556 100644 --- a/Adamant/Helpers/Decimal+adamant.swift +++ b/Adamant/Helpers/Decimal+adamant.swift @@ -16,4 +16,8 @@ extension Decimal { func shiftedToAdamant() -> Decimal { return Decimal(sign: self.isSignMinus ? .minus : .plus, exponent: -AdamantUtilities.currencyExponent, significand: self) } + + var doubleValue: Double { + return (self as NSNumber).doubleValue + } } diff --git a/Adamant/Helpers/NSRegularExpression+adamant.swift b/Adamant/Helpers/NSRegularExpression+adamant.swift new file mode 100644 index 000000000..8479c0994 --- /dev/null +++ b/Adamant/Helpers/NSRegularExpression+adamant.swift @@ -0,0 +1,15 @@ +// +// NSRegularExpression+adamant.swift +// Adamant +// +// Created by Anokhov Pavel on 03.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation + +extension NSRegularExpression { + func perfectMatch(with string: String) -> Bool { + return matches(in: string, options: [], range: NSRange(location: 0, length: string.count)).count == 1 + } +} diff --git a/Adamant/Helpers/String+localized.swift b/Adamant/Helpers/String+localized.swift index 127129e48..fc3f88d6f 100644 --- a/Adamant/Helpers/String+localized.swift +++ b/Adamant/Helpers/String+localized.swift @@ -47,6 +47,8 @@ extension String { static let networkError = NSLocalizedString("Error.NoNetwork", comment: "Shared error: Network problems. In most cases - no connection") static let accountNotFound = NSLocalizedString("Error.AccountNotFoundFormat", comment: "Shared error: Account not found error. Using %@ for address.") + static let unknownError = NSLocalizedString("Error.UnknownError", comment: "Shared unknown error") + static func internalError(message: String) -> String { return String.localizedStringWithFormat(NSLocalizedString("Error.InternalErrorFormat", comment: "Shared error: Internal error format, %@ for message"), message) } diff --git a/Adamant/Helpers/UIView+constraints.swift b/Adamant/Helpers/UIView+constraints.swift new file mode 100644 index 000000000..7687ce386 --- /dev/null +++ b/Adamant/Helpers/UIView+constraints.swift @@ -0,0 +1,111 @@ +// +// UIView+constraints.swift +// Adamant +// +// Created by Anokhov Pavel on 10.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit + +extension UIView { + func constrainCentered(_ subview: UIView) { + subview.translatesAutoresizingMaskIntoConstraints = false + + let verticalContraint = NSLayoutConstraint( + item: subview, + attribute: .centerY, + relatedBy: .equal, + toItem: self, + attribute: .centerY, + multiplier: 1.0, + constant: 0) + + let horizontalContraint = NSLayoutConstraint( + item: subview, + attribute: .centerX, + relatedBy: .equal, + toItem: self, + attribute: .centerX, + multiplier: 1.0, + constant: 0) + + let heightContraint = NSLayoutConstraint( + item: subview, + attribute: .height, + relatedBy: .equal, + toItem: nil, + attribute: .notAnAttribute, + multiplier: 1.0, + constant: subview.frame.height) + + let widthContraint = NSLayoutConstraint( + item: subview, + attribute: .width, + relatedBy: .equal, + toItem: nil, + attribute: .notAnAttribute, + multiplier: 1.0, + constant: subview.frame.width) + + addConstraints([ + horizontalContraint, + verticalContraint, + heightContraint, + widthContraint]) + } + + func constrainToEdges(_ subview: UIView, relativeToSafeArea: Bool = false) { + subview.translatesAutoresizingMaskIntoConstraints = false + + let topContraint: NSLayoutConstraint + let bottomConstraint: NSLayoutConstraint + + if relativeToSafeArea, #available(iOS 11, *) { + topContraint = subview.topAnchor.constraint(equalToSystemSpacingBelow: safeAreaLayoutGuide.topAnchor, multiplier: 1.0) + bottomConstraint = safeAreaLayoutGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: subview.bottomAnchor, multiplier: 1.0) + } else { + topContraint = NSLayoutConstraint( + item: subview, + attribute: .top, + relatedBy: .equal, + toItem: self, + attribute: .top, + multiplier: 1.0, + constant: 0) + + bottomConstraint = NSLayoutConstraint( + item: subview, + attribute: .bottom, + relatedBy: .equal, + toItem: self, + attribute: .bottom, + multiplier: 1.0, + constant: 0) + } + + let leadingContraint = NSLayoutConstraint( + item: subview, + attribute: .leading, + relatedBy: .equal, + toItem: self, + attribute: .leading, + multiplier: 1.0, + constant: 0) + + let trailingContraint = NSLayoutConstraint( + item: subview, + attribute: .trailing, + relatedBy: .equal, + toItem: self, + attribute: .trailing, + multiplier: 1.0, + constant: 0) + + addConstraints([ + topContraint, + bottomConstraint, + leadingContraint, + trailingContraint]) + } +} diff --git a/Adamant/Info.plist b/Adamant/Info.plist index 606aa8bc9..592072a21 100644 --- a/Adamant/Info.plist +++ b/Adamant/Info.plist @@ -17,11 +17,16 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0.0 + 1.1.1 CFBundleVersion - 43 + 47 LSRequiresIPhoneOS + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSCameraUsageDescription The camera needed to scan QR codes for addresses and passphrases NSFaceIDUsageDescription diff --git a/Adamant/Models/Account.swift b/Adamant/Models/AdamantAccount.swift similarity index 95% rename from Adamant/Models/Account.swift rename to Adamant/Models/AdamantAccount.swift index 551e53b91..f1b53e100 100644 --- a/Adamant/Models/Account.swift +++ b/Adamant/Models/AdamantAccount.swift @@ -8,7 +8,7 @@ import Foundation -struct Account { +struct AdamantAccount { let address: String var unconfirmedBalance: Decimal var balance: Decimal @@ -20,7 +20,7 @@ struct Account { let uMultisignatures: [String]? } -extension Account: Decodable { +extension AdamantAccount: Decodable { enum CodingKeys: String, CodingKey { case address case unconfirmedBalance @@ -51,7 +51,7 @@ extension Account: Decodable { } } -extension Account: WrappableModel { +extension AdamantAccount: WrappableModel { static let ModelKey = "account" } diff --git a/Adamant/Models/AdamantMessage.swift b/Adamant/Models/AdamantMessage.swift index e4e298b01..159e9929c 100644 --- a/Adamant/Models/AdamantMessage.swift +++ b/Adamant/Models/AdamantMessage.swift @@ -15,15 +15,39 @@ import Foundation enum AdamantMessage { case text(String) case markdownText(String) + case richMessage(payload: RichMessage) } + +// 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 Decimal(ceil(Double(message.count) / 255.0)) * AdamantMessage.textFee + 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 + } +} + + +// MARK: - ChatType +extension AdamantMessage { + var chatType: ChatType { + switch self { + case .text, .markdownText: + return .message + + case .richMessage: + return .richMessage } } } diff --git a/Adamant/Models/EthAccount.swift b/Adamant/Models/EthAccount.swift new file mode 100644 index 000000000..f35d699ba --- /dev/null +++ b/Adamant/Models/EthAccount.swift @@ -0,0 +1,18 @@ +// +// EthAccount.swift +// Adamant +// +// Created by Anokhov Pavel on 02.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import web3swift +import BigInt + +struct EthAccount { + let wallet: BIP32Keystore + let address: String? + var balance: BigUInt? + var balanceString: String? +} diff --git a/Adamant/Models/EthTransaction.swift b/Adamant/Models/EthTransaction.swift new file mode 100644 index 000000000..4f73d99e2 --- /dev/null +++ b/Adamant/Models/EthTransaction.swift @@ -0,0 +1,196 @@ +// +// EthTransaction.swift +// Adamant +// +// Created by Anton Boyarkin on 26/06/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import web3swift +import BigInt + +struct EthResponse { + let status: Int + let message: String + let result: [EthTransaction] +} + +// MARK: - Decodable +extension EthResponse: Decodable { + enum CodingKeys: String, CodingKey { + case status + 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)) ?? [] + } +} + + +// MARK: - Eth Transaction + +struct EthTransaction { + let date: Date? + let hash: String + let value: Decimal + let from: String + let to: String + let gasUsed: Decimal + let gasPrice: Decimal + let confirmations: String? + let isError: Bool + let receiptStatus: TransactionReceipt.TXStatus + let blockNumber: String? + + var isOutgoing: Bool = false +} + + +// MARK: Decodable +extension EthTransaction: Decodable { + enum CodingKeys: String, CodingKey { + case timeStamp + case hash + case value + case from + case to + case gasUsed + case gasPrice + case confirmations + case isError + 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) + + // Status + if let statusRaw = try? container.decode(String.self, forKey: .receiptStatus) { + if statusRaw == "1" { + self.receiptStatus = .ok + } else { + self.receiptStatus = .failed + } + } 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) + } else { + self.gasPrice = 0 + } + } +} + + +// MARK: - TransactionDetails +extension EthTransaction: TransactionDetails { + var id: String { return hash } + var senderAddress: String { return from } + var recipientAddress: String { return to } + var dateValue: Date? { return date } + var amountValue: Decimal { return value } + var confirmationsValue: String? { return confirmations } + var blockValue: String? { return blockNumber} + + var feeValue: Decimal { + return gasPrice * gasUsed + } + + var transactionStatus: TransactionStatus? { + return receiptStatus.asTransactionStatus() + } +} + +// MARK: - From EthereumTransaction +extension EthereumTransaction { + func asEthTransaction(date: Date?, gasUsed: BigUInt, blockNumber: String?, confirmations: String?, receiptStatus: TransactionReceipt.TXStatus, isOutgoing: Bool) -> EthTransaction { + return EthTransaction(date: date, + hash: txhash ?? "", + value: value.asDecimal(exponent: EthWalletService.currencyExponent), + from: sender?.address ?? "", + to: to.address, + gasUsed: gasUsed.asDecimal(exponent: 0), + gasPrice: gasPrice.asDecimal(exponent: EthWalletService.currencyExponent), + confirmations: confirmations, + isError: receiptStatus != .failed, + receiptStatus: receiptStatus, + blockNumber: blockNumber, + isOutgoing: isOutgoing) + } +} + + +// MARK: Sample JSON +/* + { + "blockNumber":"3455267", + "timeStamp":"1529241530", + "hash":"0x9e2092aa9a278ebdd5cc4e37d626533ec1a480397c101add069817c0934cfa76", + "nonce":"561145", + "blockHash":"0xf828955a0911da4a2c207f96b8bffabac804eab7888ec88149ab9867db19b7dd", + "transactionIndex":"16", + "from":"0x687422eea2cb73b5d3e242ba5456b782919afc85", + "to":"0x700bc74dd49044446bcb6a25ae5e725d14538825", + "value":"1000000000000000000", + "gas":"314150", + "gasPrice":"5000000000", + "isError":"0", + "txreceipt_status":"1", + "input":"0x", + "contractAddress":"", + "cumulativeGasUsed":"381927", + "gasUsed":"21000", + "confirmations":"32316" + } + + */ diff --git a/Adamant/Models/LskAccount.swift b/Adamant/Models/LskAccount.swift new file mode 100644 index 000000000..3fb4f5a27 --- /dev/null +++ b/Adamant/Models/LskAccount.swift @@ -0,0 +1,18 @@ +// +// LskAccount.swift +// Adamant +// +// Created by Anokhov Pavel on 03.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import BigInt +import Ed25519 + +struct LskAccount { + let keys: KeyPair + let address: String + var balance: BigUInt? + var balanceString: String? +} diff --git a/Adamant/Models/RichMessage.swift b/Adamant/Models/RichMessage.swift new file mode 100644 index 000000000..2eb53ff0e --- /dev/null +++ b/Adamant/Models/RichMessage.swift @@ -0,0 +1,140 @@ +// +// RichMessage.swift +// Adamant +// +// Created by Anokhov Pavel on 29.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation + +// MARK: - RichMessage + +protocol RichMessage: Codable { + var type: String { get } + + func content() -> [String:String] + func serialized() -> String +} + +extension RichMessage { + func serialized() -> String { + if let data = try? JSONEncoder().encode(self), let raw = String(data: data, encoding: String.Encoding.utf8) { + return raw + } else { + return "" + } + } +} + +struct RichContentKeys { + static let type = "type" + + private init() {} +} + + +// MARK: - RichMessageTransfer + +struct RichMessageTransfer: RichMessage { + let type: String + let amount: String + let hash: String + let comments: String + + func content() -> [String:String] { + return [ + CodingKeys.type.stringValue: type, + CodingKeys.amount.stringValue: amount, + CodingKeys.hash.stringValue: hash, + CodingKeys.comments.stringValue: comments + ] + } + + init(type: String, amount: Decimal, hash: String, comments: String) { + self.type = type + self.amount = RichMessageTransfer.serialize(balance: amount) + self.hash = hash + self.comments = comments + } + + init(type: String, amount: String, hash: String, comments: String) { + self.type = type + self.amount = amount + self.hash = hash + self.comments = comments + } + + 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 amount = content[CodingKeys.amount.stringValue] { + self.amount = amount + } else { + self.amount = "0" + } + + if let comments = content[CodingKeys.comments.stringValue] { + self.comments = comments + } else { + self.comments = "" + } + } +} + +extension RichContentKeys { + struct transfer { + static let amount = "amount" + static let hash = "hash" + static let comments = "comments" + + private init() {} + } +} + +extension RichMessageTransfer { + enum CodingKeys: String, CodingKey { + case type, amount, hash, comments + } + + 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 { + 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) + self.amount = try container.decode(String.self, forKey: .amount) + } +} + +extension RichMessageTransfer { + static var formatter: NumberFormatter = { + let f = NumberFormatter() + f.numberStyle = .decimal + f.roundingMode = .floor + f.decimalSeparator = "." + f.minimumFractionDigits = 0 + f.maximumFractionDigits = 18 + return f + }() + + static func serialize(balance: Decimal) -> String { + return formatter.string(fromDecimal: balance) ?? "0" + } +} diff --git a/Adamant/Models/TransactionStatus.swift b/Adamant/Models/TransactionStatus.swift new file mode 100644 index 000000000..3882b2630 --- /dev/null +++ b/Adamant/Models/TransactionStatus.swift @@ -0,0 +1,30 @@ +// +// TransactionStatus.swift +// Adamant +// +// Created by Anokhov Pavel on 06.10.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation + +enum TransactionStatus: Int16 { + case notInitiated + case updating + case pending + case success + case failed + + var localized: String { + switch self { + case .notInitiated, .updating: + return NSLocalizedString("TransactionStatus.Updating", comment: "Transaction status: updating in progress") + case .pending: + return NSLocalizedString("TransactionStatus.Pending", comment: "Transaction status: transaction is pending") + case .success: + return NSLocalizedString("TransactionStatus.Success", comment: "Transaction status: success") + case .failed: + return NSLocalizedString("TransactionStatus.Failed", comment: "Transaction status: transaction failed") + } + } +} diff --git a/Adamant/Models/Wallet.swift b/Adamant/Models/Wallet.swift deleted file mode 100644 index e629247a6..000000000 --- a/Adamant/Models/Wallet.swift +++ /dev/null @@ -1,105 +0,0 @@ -// -// Wallet.swift -// Adamant -// -// Created by Anokhov Pavel on 29.06.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import UIKit - -enum Wallet { - case adamant(balance: Decimal) - case ethereum - - var enabled: Bool { - switch self { - case .adamant: return true - case .ethereum: return false - } - } -} - - -// MARK: - Resources -extension Wallet { - var currencyLogo: UIImage { - switch self { - case .adamant: return #imageLiteral(resourceName: "wallet_adm") - case .ethereum: return #imageLiteral(resourceName: "wallet_eth") - } - } - - var currencySymbol: String { - switch self { - case .adamant: return "ADM" - case .ethereum: return "ETH" - } - } -} - -// MARK: - Formatter -extension Wallet { - - // MARK: Formatters - - /// Number formatters - /// - full: 8 decimal digits - /// - compact: 4 decimal digits - /// - short: 2 decimal digits - enum NumberFormat { - case full, compact, short - - var formatter: NumberFormatter { - switch self { - case .short: return Wallet.currencyFormatterShort - case .compact: return Wallet.currencyFormatterCompact - case .full: return Wallet.currencyFormatterFull - } - } - } - - static var currencyFormatterFull: NumberFormatter = { - let formatter = NumberFormatter() - formatter.numberStyle = .decimal - formatter.roundingMode = .floor - formatter.positiveFormat = "#.########" - return formatter - }() - - static var currencyFormatterCompact: NumberFormatter = { - let formatter = NumberFormatter() - formatter.numberStyle = .decimal - formatter.roundingMode = .floor - formatter.positiveFormat = "#.####" - return formatter - }() - - static var currencyFormatterShort: NumberFormatter = { - let formatter = NumberFormatter() - formatter.numberStyle = .decimal - formatter.roundingMode = .floor - formatter.positiveFormat = "#.##" - return formatter - }() - - - // MARK: Methods - - func format(numberFormat: NumberFormat, includeCurrencySymbol: Bool) -> String { - let balance: String - switch self { - case .adamant(let b): - balance = numberFormat.formatter.string(from: b as NSNumber)! - - case .ethereum: - balance = "" - } - - if includeCurrencySymbol { - return "\(balance) \(currencySymbol)" - } else { - return balance - } - } -} diff --git a/Adamant/ServiceProtocols/AccountService.swift b/Adamant/ServiceProtocols/AccountService.swift index 13d0c7ba7..9cc434827 100644 --- a/Adamant/ServiceProtocols/AccountService.swift +++ b/Adamant/ServiceProtocols/AccountService.swift @@ -11,11 +11,14 @@ 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 has successfully logged in. See AdamantUserInfoKey.AccountService - static let userLoggedIn = Notification.Name("adamant.accountService.userHasLoggedIn") + /// 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") @@ -26,16 +29,35 @@ extension Notification.Name { /// - Adamant.AccountService.newStayInState with new state static let stayInChanged = Notification.Name("adamant.accountService.stayInChanged") + + /// Raised when wallets collection updated + /// + /// UserInfo: + /// - Adamant.AccountService.updatedWallet: wallet object + /// - Adamant.AccountService.updatedWalletIndex: wallet index in AccountService.wallets collection + static let walletUpdated = Notification.Name("adamant.accountService.walletUpdated") + private init() {} } } +// MARK: - Localization +extension String.adamantLocalized { + struct accountService { + static let updateAlertTitleV12 = NSLocalizedString("AccountService.update.v12.title", comment: "AccountService: Alert title. Changes in version 1.2") + static let updateAlertMessageV12 = NSLocalizedString("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") + } +} + + /// - loggedAccountAddress: Newly logged account's address extension AdamantUserInfoKey { struct AccountService { static let loggedAccountAddress = "adamant.accountService.loggedin.address" static let newStayInState = "adamant.accountService.stayIn" + static let updatedWallet = "adamant.accountService.updatedWallet" + static let updatedWalletIndex = "adamant.accountService.updatedWalletIndex" private init() {} } @@ -52,7 +74,7 @@ enum AccountServiceState { } enum AccountServiceResult { - case success(account: Account) + case success(account: AdamantAccount, alert: (title: String, message: String)?) case failure(AccountServiceError) } @@ -124,10 +146,14 @@ protocol AccountService: class { // MARK: State var state: AccountServiceState { get } - var account: Account? { get } + var account: AdamantAccount? { get } var keypair: Keypair? { get } + // MARK: Wallets + var wallets: [WalletService] { get } + + // MARK: Account functions /// Update logged account info diff --git a/Adamant/ServiceProtocols/AddressBookService.swift b/Adamant/ServiceProtocols/AddressBookService.swift index c0474bc34..d025eb1c8 100644 --- a/Adamant/ServiceProtocols/AddressBookService.swift +++ b/Adamant/ServiceProtocols/AddressBookService.swift @@ -11,7 +11,7 @@ import Foundation // MARK: - Notifications extension Notification.Name { - struct AddressBookService { + struct AdamantAddressBookService { /// Raised when user rename accounts in chat static let addressBookUpdated = Notification.Name("adamant.addressBookService.updated") @@ -19,6 +19,22 @@ extension Notification.Name { } } +enum AddressBookChange { + case newName(address: String, name: String) + case updated(address: String, name: String) + case removed(address: String) +} + +extension AdamantUserInfoKey { + struct AddressBook { + + /// Array of AddressBookChangeType + static let changes = "adamant.addressBook.changes" + + private init() {} + } +} + // MARK: - Result and Errors diff --git a/Adamant/ServiceProtocols/ApiService.swift b/Adamant/ServiceProtocols/ApiService.swift index 5cbe239b9..a119686f2 100644 --- a/Adamant/ServiceProtocols/ApiService.swift +++ b/Adamant/ServiceProtocols/ApiService.swift @@ -126,10 +126,10 @@ protocol ApiService: class { // MARK: - Accounts - func newAccount(byPublicKey publicKey: String, completion: @escaping (ApiServiceResult) -> Void) - 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 newAccount(byPublicKey publicKey: String, completion: @escaping (ApiServiceResult) -> Void) + 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) // MARK: - Keys @@ -145,7 +145,7 @@ protocol ApiService: class { // MARK: - Funds - func transferFunds(sender: String, recipient: String, amount: Decimal, keypair: Keypair, completion: @escaping (ApiServiceResult) -> Void) + func transferFunds(sender: String, recipient: String, amount: Decimal, keypair: Keypair, completion: @escaping (ApiServiceResult) -> Void) // MARK: - States @@ -166,7 +166,7 @@ protocol ApiService: class { /// Send text message /// - completion: Contains processed transactionId, if success, or AdamantError, if fails. func sendMessage(senderId: String, recipientId: String, keypair: Keypair, message: String, type: ChatType, nonce: String, completion: @escaping (ApiServiceResult) -> Void) - + // MARK: - Delegates /// Get delegates diff --git a/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift b/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift index 3e019144b..9737079ee 100644 --- a/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift +++ b/Adamant/ServiceProtocols/DataProviders/ChatsProvider.swift @@ -172,10 +172,10 @@ protocol ChatsProvider: DataProvider { // MARK: - Sending messages func sendMessage(_ message: AdamantMessage, recipientId: String, completion: @escaping (ChatsProviderResult) -> Void ) - func retrySendMessage(_ message: MessageTransaction, completion: @escaping (ChatsProviderRetryCancelResult) -> Void) + func retrySendMessage(_ message: ChatTransaction, completion: @escaping (ChatsProviderRetryCancelResult) -> Void) // MARK: - Delete local message - func cancelMessage(_ message: MessageTransaction, completion: @escaping (ChatsProviderRetryCancelResult) -> Void ) + func cancelMessage(_ message: ChatTransaction, completion: @escaping (ChatsProviderRetryCancelResult) -> Void ) // MARK: - Tools func validateMessage(_ message: AdamantMessage) -> ValidateMessageResult diff --git a/Adamant/ServiceProtocols/DataProviders/DataProvider.swift b/Adamant/ServiceProtocols/DataProviders/DataProvider.swift index 305f743b1..9ba2a6591 100644 --- a/Adamant/ServiceProtocols/DataProviders/DataProvider.swift +++ b/Adamant/ServiceProtocols/DataProviders/DataProvider.swift @@ -15,7 +15,7 @@ enum State { case failedToUpdate(Error) } -protocol DataProvider { +protocol DataProvider: class { var state: State { get } var isInitiallySynced: Bool { get } diff --git a/Adamant/ServiceProtocols/DialogService.swift b/Adamant/ServiceProtocols/DialogService.swift index 3c3465496..c760212c1 100644 --- a/Adamant/ServiceProtocols/DialogService.swift +++ b/Adamant/ServiceProtocols/DialogService.swift @@ -7,7 +7,6 @@ // import UIKit -import PMAlertController extension String.adamantLocalized.alert { static let copyToPasteboard = NSLocalizedString("Shared.CopyToPasteboard", comment: "Shared alert 'Copy' button. Used anywhere. Used for copy-paste info.") @@ -45,22 +44,36 @@ enum ShareContentType { case passphrase case address - var excludedActivityTypes: [UIActivityType]? { + func shareTypes(sharingTip: String?) -> [ShareType] { switch self { + case .address: + return [.copyToPasteboard, + .share, + .generateQr(sharingTip: sharingTip)] + case .passphrase: - var types: [UIActivityType] = [.postToFacebook, - .postToTwitter, - .postToWeibo, - .message, - .mail, - .assignToContact, - .saveToCameraRoll, - .addToReadingList, - .postToFlickr, - .postToVimeo, - .postToTencentWeibo, - .airDrop, - .openInIBooks] + return [.copyToPasteboard, + .share, + .generateQr(sharingTip: sharingTip)] + } + } + + 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] if #available(iOS 11.0, *) { types.append(.markupAsPDF) } return types @@ -83,6 +96,16 @@ protocol RichError: Error { var level: ErrorLevel { get } } +enum AdamantAlertStyle { + case alert, actionSheet, richNotification +} + +struct AdamantAlertAction { + let title: String + let style: UIAlertAction.Style + let handler: (() -> Void)? +} + protocol DialogService: class { func getTopmostViewController() -> UIViewController? @@ -111,11 +134,11 @@ protocol DialogService: class { func dismissNotification() // MARK: - ActivityControllers - func presentShareAlertFor(string: String, types: [ShareType], excludedActivityTypes: [UIActivityType]?, animated: Bool, completion: (() -> Void)?) + func presentShareAlertFor(string: String, types: [ShareType], excludedActivityTypes: [UIActivity.ActivityType]?, animated: Bool, completion: (() -> Void)?) func presentGoToSettingsAlert(title: String?, message: String?) // MARK: - Alerts - func showAlert(title:String, message: String, actions: [PMAlertAction]?) - func showSystemActionSheet(title: String?, message: String?, actions: [UIAlertAction]?) + func showAlert(title: String?, message: String?, style: UIAlertController.Style, actions: [UIAlertAction]?) + func showAlert(title: String?, message: String?, style: AdamantAlertStyle, actions: [AdamantAlertAction]?) } diff --git a/Adamant/ServiceProtocols/LskApiService.swift b/Adamant/ServiceProtocols/LskApiService.swift new file mode 100644 index 000000000..3a94f7d33 --- /dev/null +++ b/Adamant/ServiceProtocols/LskApiService.swift @@ -0,0 +1,43 @@ +// +// 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/LskApiServiceProtocol.swift b/Adamant/ServiceProtocols/LskApiServiceProtocol.swift new file mode 100644 index 000000000..3a94f7d33 --- /dev/null +++ b/Adamant/ServiceProtocols/LskApiServiceProtocol.swift @@ -0,0 +1,43 @@ +// +// 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/RichMessageProvider.swift b/Adamant/ServiceProtocols/RichMessageProvider.swift new file mode 100644 index 000000000..4c381abcf --- /dev/null +++ b/Adamant/ServiceProtocols/RichMessageProvider.swift @@ -0,0 +1,37 @@ +// +// RichMessageProvider.swift +// Adamant +// +// Created by Anokhov Pavel on 06.09.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import MessageKit + +enum CellSource { + case `class`(type: UICollectionViewCell.Type) + case nib(nib: UINib) +} + +protocol RichMessageProvider { + static var richMessageType: String { get } + + var cellIdentifierSent: String { get } + var cellIdentifierReceived: String { get } + var cellSource: CellSource? { get } + + // MARK: Events + func richMessageTapped(for transaction: RichMessageTransaction, at indexPath: IndexPath, in chat: ChatViewController) + + // MARK: Chats list + func shortDescription(for transaction: RichMessageTransaction) -> String + + // MARK: MessageKit + func cellSizeCalculator(for messagesCollectionViewFlowLayout: MessagesCollectionViewFlowLayout) -> CellSizeCalculator + func cell(for message: MessageType, isFromCurrentSender: Bool, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell +} + +protocol RichMessageProviderWithStatusCheck: RichMessageProvider { + func statusForTransactionBy(hash: String, completion: @escaping (WalletServiceResult) -> Void) +} diff --git a/Adamant/ServiceProtocols/TransactionDetailsProtocol.swift b/Adamant/ServiceProtocols/TransactionDetailsProtocol.swift new file mode 100644 index 000000000..45dba6998 --- /dev/null +++ b/Adamant/ServiceProtocols/TransactionDetailsProtocol.swift @@ -0,0 +1,66 @@ +// +// 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/Services/AdamantAccountService.swift b/Adamant/Services/AdamantAccountService.swift index b640c4faa..d964de469 100644 --- a/Adamant/Services/AdamantAccountService.swift +++ b/Adamant/Services/AdamantAccountService.swift @@ -22,7 +22,10 @@ class AdamantAccountService: AccountService { securedStoreSemaphore.signal() } - if securedStore.get(.publicKey) != nil, + if securedStore.get(.passphrase) != nil { + hasStayInAccount = true + _useBiometry = securedStore.get(.useBiometry) != nil + } else if securedStore.get(.publicKey) != nil, securedStore.get(.privateKey) != nil, securedStore.get(.pin) != nil { hasStayInAccount = true @@ -42,8 +45,9 @@ class AdamantAccountService: AccountService { private let stateSemaphore = DispatchSemaphore(value: 1) private let securedStoreSemaphore = DispatchSemaphore(value: 1) - private(set) var account: Account? + private(set) var account: AdamantAccount? private(set) var keypair: Keypair? + private var passphrase: String? private func setState(_ state: AccountServiceState) { stateSemaphore.wait() @@ -51,8 +55,6 @@ class AdamantAccountService: AccountService { stateSemaphore.signal() } - - private(set) var hasStayInAccount: Bool = false private var _useBiometry: Bool = false @@ -80,6 +82,13 @@ class AdamantAccountService: AccountService { } } } + + // MARK: Wallets + var wallets: [WalletService] = [ + AdmWalletService(), + try! EthWalletService(apiUrl: AdamantResources.ethServers.first!), // TODO: Move to background thread +// LskWalletService() + ] } // MARK: - Saved data @@ -101,11 +110,17 @@ extension AdamantAccountService { } securedStore.set(pin, for: .pin) - securedStore.set(keypair.publicKey, for: .publicKey) - securedStore.set(keypair.privateKey, for: .privateKey) + + if let passphrase = passphrase { + securedStore.set(passphrase, for: .passphrase) + } else { + securedStore.set(keypair.publicKey, for: .publicKey) + securedStore.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)) + completion(.success(account: account, alert: nil)) } func validatePin(_ pin: String) -> Bool { @@ -124,6 +139,10 @@ extension AdamantAccountService { return nil } + private func getSavedPassphrase() -> String? { + return securedStore.get(.passphrase) + } + func dropSavedAccount() { securedStoreSemaphore.wait() defer { @@ -135,6 +154,7 @@ extension AdamantAccountService { securedStore.remove(.publicKey) securedStore.remove(.privateKey) securedStore.remove(.useBiometry) + securedStore.remove(.passphrase) hasStayInAccount = false NotificationCenter.default.post(name: Notification.Name.AdamantAccountService.stayInChanged, object: self, userInfo: [AdamantUserInfoKey.AccountService.newStayInState : false]) notificationsService.setNotificationsMode(.disabled, completion: nil) @@ -184,13 +204,21 @@ extension AdamantAccountService { } self?.setState(.loggedIn) - completion?(.success(account: account)) + completion?(.success(account: account, alert: nil)) + + if let adm = self?.wallets.first(where: { $0 is AdmWalletService }) { + adm.update() + } case .failure(let error): completion?(.failure(.apiError(error: error))) self?.setState(prevState) } } + + for wallet in wallets.filter({ !($0 is AdmWalletService) }) { + wallet.update() + } } } @@ -219,7 +247,7 @@ extension AdamantAccountService { apiService.newAccount(byPublicKey: publicKey) { result in switch result { case .success(let account): - completion(.success(account: account)) + completion(.success(account: account, alert: nil)) case .failure(let error): completion(.failure(.apiError(error: error))) @@ -247,22 +275,31 @@ extension AdamantAccountService { return } - if let savedKeypair = getSavedKeypair() { - loginWith(keypair: keypair) { [weak self] result in - switch result { - case .success(_): - if let newKeypair = self?.keypair, newKeypair != savedKeypair { - self?.dropSavedAccount() - } - - default: - break - } - + loginWith(keypair: keypair) { [weak self] result in + guard case .success = result else { completion(result) + return } - } else { - loginWith(keypair: keypair, completion: completion) + + // MARK: Drop saved accs + if let storedPassphrase = self?.getSavedPassphrase(), storedPassphrase != passphrase { + self?.dropSavedAccount() + } + + if let storedKeypair = self?.getSavedKeypair(), storedKeypair != self?.keypair { + self?.dropSavedAccount() + } + + // Update and initiate wallet services + self?.passphrase = passphrase + + if let wallets = self?.wallets { + for case let wallet as InitiatedWithPassphraseService in wallets { + wallet.initWallet(withPassphrase: passphrase, completion: { _ in }) + } + } + + completion(result) } } @@ -283,12 +320,27 @@ extension AdamantAccountService { // MARK: Biometry func loginWithStoredAccount(completion: @escaping (AccountServiceResult) -> Void) { - guard let keypair = getSavedKeypair() else { - completion(.failure(.invalidPassphrase)) + if let passphrase = getSavedPassphrase() { + loginWith(passphrase: passphrase, completion: completion) return } - loginWith(keypair: keypair, completion: completion) + if let keypair = getSavedKeypair() { + loginWith(keypair: keypair) { result in + switch result { + case .success(let account, _): + completion(.success(account: account, + alert: (title: String.adamantLocalized.accountService.updateAlertTitleV12, + message: String.adamantLocalized.accountService.updateAlertMessageV12))) + + default: + completion(result) + } + } + return + } + + completion(.failure(.invalidPassphrase)) } @@ -325,7 +377,8 @@ extension AdamantAccountService { let userInfo = [AdamantUserInfoKey.AccountService.loggedAccountAddress:account.address] NotificationCenter.default.post(name: Notification.Name.AdamantAccountService.userLoggedIn, object: self, userInfo: userInfo) self.setState(.loggedIn) - completion(.success(account: account)) + + completion(.success(account: account, alert: nil)) case .failure(let error): self.setState(.notLogged) @@ -349,11 +402,16 @@ extension AdamantAccountService { } private func logout(lockSemaphore: Bool) { + if account != nil { + NotificationCenter.default.post(name: Notification.Name.AdamantAccountService.userWillLogOut, object: self) + } + dropSavedAccount() let wasLogged = account != nil account = nil keypair = nil + passphrase = nil if lockSemaphore { setState(.notLogged) @@ -375,6 +433,7 @@ extension StoreKey { static let privateKey = "accountService.privateKey" static let pin = "accountService.pin" static let useBiometry = "accountService.useBiometry" + static let passphrase = "accountService.passphrase" private init() {} } @@ -385,6 +444,7 @@ fileprivate enum Key { case privateKey case pin case useBiometry + case passphrase var stringValue: String { switch self { @@ -392,6 +452,7 @@ fileprivate enum Key { case .privateKey: return StoreKey.accountService.privateKey case .pin: return StoreKey.accountService.pin case .useBiometry: return StoreKey.accountService.useBiometry + case .passphrase: return StoreKey.accountService.passphrase } } } diff --git a/Adamant/Services/AdamantAddressBookService.swift b/Adamant/Services/AdamantAddressBookService.swift index 63c86270f..74c7d7a71 100644 --- a/Adamant/Services/AdamantAddressBookService.swift +++ b/Adamant/Services/AdamantAddressBookService.swift @@ -28,22 +28,100 @@ class AdamantAddressBookService: AddressBookService { private(set) var hasChanges = false private var timer: Timer? - private var isChangedSemaphore = DispatchSemaphore(value: 1) + private var removedNames = [String:String]() + + private var isChangingSemaphore = DispatchSemaphore(value: 1) + private var isSavingSemaphore = DispatchSemaphore(value: 1) + + private var savingBookTaskId = UIBackgroundTaskIdentifier.invalid + private var savingBookOnLogoutTaskId = UIBackgroundTaskIdentifier.invalid + + // MARK: - Lifecycle + init() { + // Update on login + NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userLoggedIn, object: nil, queue: nil) { [weak self] _ in + self?.update(nil) + } + + // Save on logout + NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userWillLogOut, object: nil, queue: nil) { [unowned self] _ in + self.isSavingSemaphore.wait() + + defer { + self.isSavingSemaphore.signal() + } + + guard self.hasChanges else { + return + } + + self.savingBookOnLogoutTaskId = UIApplication.shared.beginBackgroundTask { [unowned self] in + UIApplication.shared.endBackgroundTask(self.savingBookOnLogoutTaskId) + self.savingBookOnLogoutTaskId = .invalid + } + + self.saveAddressBook(self.addressBook) { _ in + UIApplication.shared.endBackgroundTask(self.savingBookOnLogoutTaskId) + self.savingBookOnLogoutTaskId = .invalid + } + } + + // Clean on logout + NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userLoggedOut, object: nil, queue: nil) { _ in + self.isChangingSemaphore.wait() + + defer { + self.isChangingSemaphore.signal() + } + + self.hasChanges = false + if let timer = self.timer { + timer.invalidate() + self.timer = nil + } + + self.addressBook.removeAll() + } + } + + deinit { + NotificationCenter.default.removeObserver(self) + } // MARK: - Setting func set(name: String, for address: String) { - isChangedSemaphore.wait() + isChangingSemaphore.wait() + + defer { + isChangingSemaphore.signal() + } 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 { + } else if let prevName = addressBook[address] { addressBook.removeValue(forKey: address) + removedNames[address] = prevName + changes = [AddressBookChange.removed(address: address)] + } else { + return } hasChanges = true @@ -54,22 +132,12 @@ class AdamantAddressBookService: AddressBookService { } timer = Timer.scheduledTimer(withTimeInterval: waitTime, repeats: false) { [weak self] _ in - self?.saveAddressBook { result in - switch result { - case .success: - self?.hasChanges = false - - case .failure(let error): - self?.dialogService.showRichError(error: error) - } - - self?.timer = nil - } + self?.saveIfNeeded() } - isChangedSemaphore.signal() - - NotificationCenter.default.post(name: Notification.Name.AddressBookService.addressBookUpdated, object: self) + NotificationCenter.default.post(name: Notification.Name.AdamantAddressBookService.addressBookUpdated, + object: self, + userInfo: [AdamantUserInfoKey.AddressBook.changes: changes]) } @@ -80,23 +148,44 @@ class AdamantAddressBookService: AddressBookService { } func update(_ completion: ((AddressBookServiceResult) -> Void)?) { + isSavingSemaphore.wait() + getAddressBook { result in + defer { + self.isSavingSemaphore.signal() + } + switch result { case .success(let book): if self.addressBook != book { - self.isChangedSemaphore.wait() + self.isChangingSemaphore.wait() + + var localBook = self.addressBook + var changes = [AddressBookChange]() - if self.hasChanges { - // Merging new keys from server, keeping our values. - // If contact had a name, but was renamed on different device - we will drop it. That's fiiiine - self.addressBook = self.addressBook.merging(book) { (c, _) in c } - } else { - self.addressBook = book + for (address, name) in book { + if let localName = localBook[address] { + if localName != name { + localBook[address] = name + changes.append(AddressBookChange.updated(address: address, name: name)) + } + } else { + if let removedName = self.removedNames[address], removedName == name { + continue + } + + localBook[address] = name + changes.append(AddressBookChange.newName(address: address, name: name)) + } } - self.isChangedSemaphore.signal() + self.addressBook = localBook - NotificationCenter.default.post(name: Notification.Name.AddressBookService.addressBookUpdated, object: self) + self.isChangingSemaphore.signal() + + NotificationCenter.default.post(name: Notification.Name.AdamantAddressBookService.addressBookUpdated, + object: self, + userInfo: [AdamantUserInfoKey.AddressBook.changes: changes]) } completion?(.success) @@ -111,26 +200,38 @@ class AdamantAddressBookService: AddressBookService { // MARK: - Saving func saveIfNeeded() { - isChangedSemaphore.wait() + isChangingSemaphore.wait() guard hasChanges else { - isChangedSemaphore.signal() + isChangingSemaphore.signal() return } - isChangedSemaphore.signal() + isChangingSemaphore.signal() + + isSavingSemaphore.wait() - if let timer = timer { - timer.invalidate() - self.timer = nil + // Background task + savingBookTaskId = UIApplication.shared.beginBackgroundTask { + UIApplication.shared.endBackgroundTask(self.savingBookTaskId) + self.savingBookTaskId = .invalid } - saveAddressBook { [unowned self] result in + saveAddressBook(addressBook) { result in + defer { + self.isSavingSemaphore.signal() + + UIApplication.shared.endBackgroundTask(self.savingBookTaskId) + self.savingBookTaskId = .invalid + } + switch result { case .success: - self.isChangedSemaphore.wait() + self.isChangingSemaphore.wait() self.hasChanges = false - self.isChangedSemaphore.signal() + self.isChangingSemaphore.signal() + + self.removedNames.removeAll() case .failure(let error): self.dialogService.showRichError(error: error) @@ -138,13 +239,13 @@ class AdamantAddressBookService: AddressBookService { } } - private func saveAddressBook(completion: @escaping (AddressBookServiceResult) -> Void) { + private func saveAddressBook(_ book: [String: String], completion: @escaping (AddressBookServiceResult) -> Void) { guard let loggedAccount = accountService.account, let keypair = accountService.keypair else { completion(.failure(.notLogged)) return } - guard loggedAccount.balance >= AdamantApiService.KVSfee else { + guard loggedAccount.balance >= AdamantApiService.KvsFee else { completion(.failure(.notEnoughtMoney)) return } @@ -152,7 +253,7 @@ class AdamantAddressBookService: AddressBookService { let address = loggedAccount.address // MARK: 1. Pack and ecode address book - let packed = AdamantAddressBookService.packAddressBook(book: self.addressBook) + let packed = AdamantAddressBookService.packAddressBook(book: book) if let encodeResult = adamantCore.encodeValue(packed, privateKey: keypair.privateKey) { let value = JSONStringify(value: ["message": encodeResult.message, "nonce": encodeResult.nonce] as AnyObject) diff --git a/Adamant/Services/AdamantDialogService.swift b/Adamant/Services/AdamantDialogService.swift index 68b0ffc63..6c88215ab 100644 --- a/Adamant/Services/AdamantDialogService.swift +++ b/Adamant/Services/AdamantDialogService.swift @@ -103,19 +103,15 @@ extension AdamantDialogService { func showError(withMessage message: String, error: Error? = nil) { if Thread.isMainThread { - internalShowError(withMessage: message, error: error) + FTIndicator.dismissProgress() } else { - DispatchQueue.main.async { - self.internalShowError(withMessage: message, error: error) + DispatchQueue.main.sync { + FTIndicator.dismissProgress() } } - } - - /// Must be called from main thread only - private func internalShowError(withMessage message: String, error: Error? = nil) { - let alertVC = PMAlertController(title: String.adamantLocalized.alert.error, description: message, image: #imageLiteral(resourceName: "error"), style: .alert) - FTIndicator.dismissProgress() + let alertVC = PMAlertController(title: String.adamantLocalized.alert.error, description: message, image: #imageLiteral(resourceName: "error"), style: .alert) + alertVC.gravityDismissAnimation = false alertVC.alertTitle.textColor = UIColor.adamant.primary alertVC.alertDescription.textColor = UIColor.adamant.secondary @@ -223,7 +219,7 @@ extension AdamantDialogService { // MAKR: - Activity controllers extension AdamantDialogService { - func presentShareAlertFor(string: String, types: [ShareType], excludedActivityTypes: [UIActivityType]?, animated: Bool, completion: (() -> Void)?) { + func presentShareAlertFor(string: String, types: [ShareType], excludedActivityTypes: [UIActivity.ActivityType]?, animated: Bool, completion: (() -> Void)?) { let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) for type in types { @@ -286,7 +282,7 @@ extension AdamantDialogService { alert.addAction(UIAlertAction(title: String.adamantLocalized.alert.settings, style: .default) { _ in DispatchQueue.main.async { - if let settingsURL = URL(string: UIApplicationOpenSettingsURLString) { + if let settingsURL = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(settingsURL, options: [:], completionHandler: nil) } } @@ -304,8 +300,83 @@ extension AdamantDialogService { } } -// MAKR: - Alerts + +// MARK: - Alerts +fileprivate extension UIAlertAction.Style { + func asPMAlertAction() -> PMAlertActionStyle { + switch self { + case .cancel: + return .cancel + + case .default, + .destructive: + return .default + } + } +} + +fileprivate extension AdamantAlertStyle { + func asUIAlertControllerStyle() -> UIAlertController.Style { + switch self { + case .alert, + .richNotification: + return .alert + + case .actionSheet: + return .actionSheet + } + } +} + +fileprivate extension AdamantAlertAction { + func asUIAlertAction() -> UIAlertAction { + let handler = self.handler + return UIAlertAction(title: self.title, style: self.style, handler: { _ in handler?() }) + } + + func asPMAlertAction() -> PMAlertAction { + let handler = self.handler + return PMAlertAction(title: self.title, style: self.style.asPMAlertAction(), action: handler) + } +} + extension AdamantDialogService { + func showAlert(title: String?, message: String?, style: AdamantAlertStyle, actions: [AdamantAlertAction]?) { + switch style { + case .alert, .actionSheet: + let uiStyle = style.asUIAlertControllerStyle() + if let actions = actions { + let uiActions: [UIAlertAction] = actions.map { $0.asUIAlertAction() } + + showAlert(title: title, message: message, style: uiStyle, actions: uiActions) + } else { + showAlert(title: title, message: message, style: uiStyle, actions: nil) + } + + case .richNotification: + if let actions = actions { + let pmActions: [PMAlertAction] = actions.map { $0.asPMAlertAction() } + showAlert(title: title ?? "", message: message ?? "", actions: pmActions) + } else { + showAlert(title: title ?? "", message: message ?? "", actions: nil) + } + } + } + + func showAlert(title: String?, message: String?, style: UIAlertController.Style, actions: [UIAlertAction]?) { + let alertVc = UIAlertController(title: title, message: message, preferredStyle: style) + + if let actions = actions { + for action in actions { + alertVc.addAction(action) + } + } else { + alertVc.addAction(UIAlertAction(title: String.adamantLocalized.alert.ok, style: .default)) + } + + present(alertVc, animated: true, completion: nil) + } + func showAlert(title: String, message: String, actions: [PMAlertAction]?) { let alertVC = PMAlertController(title: title, description: message, image: nil, style: .alert) @@ -343,22 +414,6 @@ extension AdamantDialogService { self.present(alertVC, animated: true, completion: nil) } - - func showSystemActionSheet(title: String?, message: String?, actions: [UIAlertAction]?) { - guard let actions = actions, actions.count > 0 else { - return - } - - let alertVC = UIAlertController(title: title, message: message, preferredStyle: .actionSheet) - - for action in actions { - alertVC.addAction(action) - } - - alertVC.addAction(UIAlertAction(title: String.adamantLocalized.alert.cancel, style: .cancel)) - - self.present(alertVC, animated: true, completion: nil) - } } fileprivate class MailDelegate: NSObject, MFMailComposeViewControllerDelegate { diff --git a/Adamant/Services/AdamantNotificationService.swift b/Adamant/Services/AdamantNotificationService.swift index c711fac1e..45e97652d 100644 --- a/Adamant/Services/AdamantNotificationService.swift +++ b/Adamant/Services/AdamantNotificationService.swift @@ -133,15 +133,15 @@ extension AdamantNotificationsService { switch mode { case .disabled: UIApplication.shared.unregisterForRemoteNotifications() - UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplicationBackgroundFetchIntervalNever) + UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalNever) case .backgroundFetch: UIApplication.shared.unregisterForRemoteNotifications() - UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplicationBackgroundFetchIntervalMinimum) + UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalMinimum) case .push: UIApplication.shared.registerForRemoteNotifications() - UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplicationBackgroundFetchIntervalNever) + UIApplication.shared.setMinimumBackgroundFetchInterval(UIApplication.backgroundFetchIntervalNever) } } @@ -162,7 +162,7 @@ extension AdamantNotificationsService { let content = UNMutableNotificationContent() content.title = title content.body = body - content.sound = UNNotificationSound(named: "notification.mp3") + content.sound = UNNotificationSound(named: UNNotificationSoundName("notification.mp3")) if let number = type.badge { if Thread.isMainThread { diff --git a/Adamant/Services/ApiService/AdamantApi+Accounts.swift b/Adamant/Services/ApiService/AdamantApi+Accounts.swift index fe360eb58..99251a374 100644 --- a/Adamant/Services/ApiService/AdamantApi+Accounts.swift +++ b/Adamant/Services/ApiService/AdamantApi+Accounts.swift @@ -20,7 +20,7 @@ extension AdamantApiService.ApiCommands { extension AdamantApiService { /// Create new account with publicKey - func newAccount(byPublicKey publicKey: String, completion: @escaping (ApiServiceResult) -> Void) { + func newAccount(byPublicKey publicKey: String, completion: @escaping (ApiServiceResult) -> Void) { // MARK: 1. Build endpoint let endpoint: URL do { @@ -40,7 +40,7 @@ extension AdamantApiService { ] // MARK: 3. Send - sendRequest(url: endpoint, method: .post, parameters: params, encoding: .json, headers: headers) { (serverResponse: ApiServiceResult>) in + sendRequest(url: endpoint, method: .post, parameters: params, encoding: .json, headers: headers) { (serverResponse: ApiServiceResult>) in switch serverResponse { case .success(let response): if let model = response.model { @@ -57,7 +57,7 @@ extension AdamantApiService { } /// Get existing account by passphrase. - func getAccount(byPassphrase passphrase: String, completion: @escaping (ApiServiceResult) -> Void) { + func getAccount(byPassphrase passphrase: String, completion: @escaping (ApiServiceResult) -> Void) { // MARK: 1. Get keypair from passphrase guard let keypair = adamantCore.createKeypairFor(passphrase: passphrase) else { completion(.failure(.accountNotFound)) @@ -69,7 +69,7 @@ extension AdamantApiService { } /// Get existing account by publicKey - func getAccount(byPublicKey publicKey: String, completion: @escaping (ApiServiceResult) -> Void) { + func getAccount(byPublicKey publicKey: String, completion: @escaping (ApiServiceResult) -> Void) { // MARK: 1. Build endpoint let endpoint: URL do { @@ -81,7 +81,7 @@ extension AdamantApiService { } // MARK: 2. Send - sendRequest(url: endpoint) { (serverResponse: ApiServiceResult>) in + sendRequest(url: endpoint) { (serverResponse: ApiServiceResult>) in switch serverResponse { case .success(let response): if let model = response.model { @@ -97,7 +97,7 @@ extension AdamantApiService { } } - func getAccount(byAddress address: String, completion: @escaping (ApiServiceResult) -> Void) { + func getAccount(byAddress address: String, completion: @escaping (ApiServiceResult) -> Void) { // MARK: 1. Build endpoint let endpoint: URL do { @@ -109,7 +109,7 @@ extension AdamantApiService { } // MARK: 2. Send - sendRequest(url: endpoint) { (serverResponse: ApiServiceResult>) in + sendRequest(url: endpoint) { (serverResponse: ApiServiceResult>) in switch serverResponse { case .success(let response): if let model = response.model { diff --git a/Adamant/Services/ApiService/AdamantApi+Chats.swift b/Adamant/Services/ApiService/AdamantApi+Chats.swift index 51490616b..6ff3158a8 100644 --- a/Adamant/Services/ApiService/AdamantApi+Chats.swift +++ b/Adamant/Services/ApiService/AdamantApi+Chats.swift @@ -51,96 +51,96 @@ extension AdamantApiService { } } } - + func sendMessage(senderId: String, recipientId: String, keypair: Keypair, message: String, type: ChatType, nonce: String, completion: @escaping (ApiServiceResult) -> Void) { - // MARK: 1. Prepare params - let params: [String : Any] = [ - "type": TransactionType.chatMessage.rawValue, - "senderId": senderId, - "recipientId": recipientId, - "publicKey": keypair.publicKey, - "message": message, - "own_message": nonce, - "message_type": type.rawValue - ] - - let headers = [ - "Content-Type": "application/json" - ] - - // MARK: 2. Build Endpoints - let normalizeEndpoint: URL - let processEndpoin: URL - - do { - normalizeEndpoint = try buildUrl(path: ApiCommands.Chats.normalizeTransaction) - processEndpoin = try buildUrl(path: ApiCommands.Chats.processTransaction) - } catch { - let err = InternalError.endpointBuildFailed.apiServiceErrorWith(error: error) - completion(.failure(err)) - return - } - - // MARK: 3. Normalize transaction - sendRequest(url: normalizeEndpoint, method: .post, parameters: params, encoding: .json, headers: headers) { (serverResponse: ApiServiceResult>) in - switch serverResponse { - case .success(let response): - // MARK: 4.1. Check server errors. - guard let normalizedTransaction = response.model else { - let error = AdamantApiService.translateServerError(response.error) - completion(.failure(error)) - return - } - - // MARK: 4.2. Sign normalized transaction - guard let signature = self.adamantCore.sign(transaction: normalizedTransaction, senderId: senderId, keypair: keypair) else { - completion(.failure(InternalError.signTransactionFailed.apiServiceErrorWith(error: nil))) - return - } - - // MARK: 4.3. Create transaction - let transaction: [String: Any] = [ - "type": normalizedTransaction.type.rawValue, - "amount": normalizedTransaction.amount, - "senderPublicKey": normalizedTransaction.senderPublicKey, - "requesterPublicKey": normalizedTransaction.requesterPublicKey ?? NSNull(), - "timestamp": normalizedTransaction.timestamp, - "recipientId": normalizedTransaction.recipientId ?? NSNull(), - "senderId": senderId, - "signature": signature, - "asset": [ - "chat": [ - "message": message, - "own_message": nonce, - "type": type.rawValue - ] - ] - ] - - let params: [String: Any] = [ - "transaction": transaction - ] - - // MARK: 5. Send - self.sendRequest(url: processEndpoin, method: .post, parameters: params, encoding: .json, headers: headers) { (serverResponse: ApiServiceResult) in - switch serverResponse { - case .success(let response): - if let id = response.transactionId { - completion(.success(id)) - } else { - let error = AdamantApiService.translateServerError(response.error) - completion(.failure(error)) - } - - case .failure(let error): - completion(.failure(.networkError(error: error))) - } - } - - - case .failure(let error): - completion(.failure(.networkError(error: error))) - } - } - } + // MARK: 1. Prepare params + let params: [String : Any] = [ + "type": TransactionType.chatMessage.rawValue, + "senderId": senderId, + "recipientId": recipientId, + "publicKey": keypair.publicKey, + "message": message, + "own_message": nonce, + "message_type": type.rawValue + ] + + let headers = [ + "Content-Type": "application/json" + ] + + // MARK: 2. Build Endpoints + let normalizeEndpoint: URL + let processEndpoin: URL + + do { + normalizeEndpoint = try buildUrl(path: ApiCommands.Chats.normalizeTransaction) + processEndpoin = try buildUrl(path: ApiCommands.Chats.processTransaction) + } catch { + let err = InternalError.endpointBuildFailed.apiServiceErrorWith(error: error) + completion(.failure(err)) + return + } + + // MARK: 3. Normalize transaction + sendRequest(url: normalizeEndpoint, method: .post, parameters: params, encoding: .json, headers: headers) { (serverResponse: ApiServiceResult>) in + switch serverResponse { + case .success(let response): + // MARK: 4.1. Check server errors. + guard let normalizedTransaction = response.model else { + let error = AdamantApiService.translateServerError(response.error) + completion(.failure(error)) + return + } + + // MARK: 4.2. Sign normalized transaction + guard let signature = self.adamantCore.sign(transaction: normalizedTransaction, senderId: senderId, keypair: keypair) else { + completion(.failure(InternalError.signTransactionFailed.apiServiceErrorWith(error: nil))) + return + } + + // MARK: 4.3. Create transaction + let transaction: [String: Any] = [ + "type": normalizedTransaction.type.rawValue, + "amount": normalizedTransaction.amount, + "senderPublicKey": normalizedTransaction.senderPublicKey, + "requesterPublicKey": normalizedTransaction.requesterPublicKey ?? NSNull(), + "timestamp": normalizedTransaction.timestamp, + "recipientId": normalizedTransaction.recipientId ?? NSNull(), + "senderId": senderId, + "signature": signature, + "asset": [ + "chat": [ + "message": message, + "own_message": nonce, + "type": type.rawValue + ] + ] + ] + + let params: [String: Any] = [ + "transaction": transaction + ] + + // MARK: 5. Send + self.sendRequest(url: processEndpoin, method: .post, parameters: params, encoding: .json, headers: headers) { (serverResponse: ApiServiceResult) in + switch serverResponse { + case .success(let response): + if let id = response.transactionId { + completion(.success(id)) + } else { + let error = AdamantApiService.translateServerError(response.error) + completion(.failure(error)) + } + + case .failure(let error): + completion(.failure(.networkError(error: error))) + } + } + + + case .failure(let error): + completion(.failure(.networkError(error: error))) + } + } + } } diff --git a/Adamant/Services/ApiService/AdamantApi+States.swift b/Adamant/Services/ApiService/AdamantApi+States.swift index 1cdf7be08..84550cb21 100644 --- a/Adamant/Services/ApiService/AdamantApi+States.swift +++ b/Adamant/Services/ApiService/AdamantApi+States.swift @@ -18,7 +18,7 @@ extension AdamantApiService.ApiCommands { extension AdamantApiService { - static let KVSfee: Decimal = 0.001 + static let KvsFee: Decimal = 0.001 func store(key: String, value: String, type: StateType, sender: String, keypair: Keypair, completion: @escaping (ApiServiceResult) -> Void) { diff --git a/Adamant/Services/ApiService/AdamantApi+Transfers.swift b/Adamant/Services/ApiService/AdamantApi+Transfers.swift index b43f7722e..e71552d74 100644 --- a/Adamant/Services/ApiService/AdamantApi+Transfers.swift +++ b/Adamant/Services/ApiService/AdamantApi+Transfers.swift @@ -9,8 +9,9 @@ import Foundation extension AdamantApiService { - func transferFunds(sender: String, recipient: String, amount: Decimal, keypair: Keypair, completion: @escaping (ApiServiceResult) -> Void) { - // MARK: 1. Prepare params + func transferFunds(sender: String, recipient: String, amount: Decimal, keypair: Keypair, completion: @escaping (ApiServiceResult) -> Void) { + + // MARK: 1. Prepare params let params: [String : Any] = [ "type": TransactionType.send.rawValue, "amount": amount.shiftedToAdamant(), @@ -53,14 +54,14 @@ extension AdamantApiService { // MARK: 4.2. Create transaction let transaction: [String: Any] = [ - "type": TransactionType.send.rawValue, - "amount": amount.shiftedToAdamant(), - "senderPublicKey": keypair.publicKey, - "requesterPublicKey": normalizedTransaction.requesterPublicKey ?? NSNull(), - "timestamp": normalizedTransaction.timestamp, - "recipientId": recipient, - "senderId": sender, - "signature": signature + "type": normalizedTransaction.type.rawValue, + "amount": normalizedTransaction.amount.shiftedToAdamant(), + "senderPublicKey": normalizedTransaction.senderPublicKey, + "requesterPublicKey": normalizedTransaction.requesterPublicKey ?? NSNull(), + "timestamp": normalizedTransaction.timestamp, + "recipientId": normalizedTransaction.recipientId ?? NSNull(), + "senderId": sender, + "signature": signature ] let params: [String: Any] = [ @@ -68,10 +69,18 @@ extension AdamantApiService { ] // MARK: 5. Send - self.sendRequest(url: processEndpoin, method: .post, parameters: params, encoding: .json, headers: headers) { (response: ApiServiceResult) in + self.sendRequest(url: processEndpoin, method: .post, parameters: params, encoding: .json, headers: headers) { (response: ApiServiceResult) in switch response { - case .success(_): - completion(.success(true)) + case .success(let result): + if let id = result.transactionId { + completion(.success(id)) + } else { + if let error = result.error { + completion(.failure(.internalError(message: error, error: nil))) + } else { + completion(.failure(.internalError(message: "Unknown Error", error: nil))) + } + } case .failure(let error): completion(.failure(error)) diff --git a/Adamant/Services/ApiService/AdamantApiService.swift b/Adamant/Services/ApiService/AdamantApiService.swift index 0841dd3ab..9d23adefc 100644 --- a/Adamant/Services/ApiService/AdamantApiService.swift +++ b/Adamant/Services/ApiService/AdamantApiService.swift @@ -42,7 +42,7 @@ class AdamantApiService: ApiService { return NSLocalizedString("ApiService.InternalError.ParsingFailed", comment: "Serious internal error: Error parsing response") case .unknownError: - return NSLocalizedString("ApiService.InternalError.UnknownError", comment: "Unknown internal error") + return String.adamantLocalized.sharedErrors.unknownError } } } diff --git a/Adamant/Services/DataProviders/AdamantAccountsProvider.swift b/Adamant/Services/DataProviders/AdamantAccountsProvider.swift index 6d227984e..7ef8a148c 100644 --- a/Adamant/Services/DataProviders/AdamantAccountsProvider.swift +++ b/Adamant/Services/DataProviders/AdamantAccountsProvider.swift @@ -52,8 +52,8 @@ class AdamantAccountsProvider: AccountsProvider { AdamantContacts.iosSupport.address: iosSupport ] - NotificationCenter.default.addObserver(forName: Notification.Name.AddressBookService.addressBookUpdated, object: nil, queue: nil) { [weak self] _ in - guard let book = self?.addressBookService.addressBook, + NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAddressBookService.addressBookUpdated, object: nil, queue: nil) { [weak self] notification in + guard let changes = notification.userInfo?[AdamantUserInfoKey.AddressBook.changes] as? [AddressBookChange], let viewContext = self?.stack.container.viewContext else { return } @@ -62,23 +62,33 @@ class AdamantAccountsProvider: AccountsProvider { let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) context.parent = viewContext - let request = NSFetchRequest(entityName: CoreDataAccount.entityName) - request.fetchLimit = 1 + let requestSingle = NSFetchRequest(entityName: CoreDataAccount.entityName) + requestSingle.fetchLimit = 1 - for (address, name) in book { - let predicate = NSPredicate(format: "address == %@", address) - request.predicate = predicate - - guard let account = (try? context.fetch(request))?.first else { - continue - } - - if account.name != name { + // 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? context.fetch(requestSingle), let account = result.first else { + continue + } + account.name = name + account.chatroom?.title = name - if let chatroom = account.chatroom { - chatroom.title = name + case .removed(let address): + let predicate = NSPredicate(format: "address == %@", address) + requestSingle.predicate = predicate + + guard let result = try? context.fetch(requestSingle), let account = result.first else { + continue } + + account.name = nil + account.chatroom?.title = nil } } @@ -301,7 +311,7 @@ extension AdamantAccountsProvider { return coreAccount } - private func createCoreDataAccount(from account: Account) -> CoreDataAccount { + private func createCoreDataAccount(from account: AdamantAccount) -> CoreDataAccount { let coreAccount: CoreDataAccount if Thread.isMainThread { coreAccount = createCoreDataAccount(from: account, context: stack.container.viewContext) @@ -316,7 +326,7 @@ extension AdamantAccountsProvider { return coreAccount } - private func createCoreDataAccount(from account: Account, context: NSManagedObjectContext) -> CoreDataAccount { + private func createCoreDataAccount(from account: AdamantAccount, context: NSManagedObjectContext) -> CoreDataAccount { let coreAccount = CoreDataAccount(entity: CoreDataAccount.entity(), insertInto: context) coreAccount.address = account.address coreAccount.publicKey = account.publicKey diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider+fakeMessages.swift b/Adamant/Services/DataProviders/AdamantChatsProvider+fakeMessages.swift index dd32564da..11515a9ee 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider+fakeMessages.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider+fakeMessages.swift @@ -19,6 +19,9 @@ extension AdamantChatsProvider { case .text(let text): self?.fakeSent(text: text, loggedAddress: loggedAddress, recipient: partner, date: date, status: status, markdown: false, completion: completion) + case .richMessage(let payload): + self?.fakeSent(text: payload.serialized(), loggedAddress: loggedAddress, recipient: partner, date: date, status: status, markdown: false, completion: completion) + case .markdownText(let text): self?.fakeSent(text: text, loggedAddress: loggedAddress, recipient: partner, date: date, status: status, markdown: true, completion: completion) } @@ -37,6 +40,9 @@ extension AdamantChatsProvider { case .text(let text): self?.fakeReceived(text: text, loggedAddress: loggedAccount, sender: partner, date: date, unread: unread, silent: silent, markdown: false, completion: completion) + case .richMessage(let payload): + self?.fakeReceived(text: payload.serialized(), loggedAddress: loggedAccount, sender: partner, date: date, unread: unread, silent: silent, markdown: false, completion: completion) + case .markdownText(let text): self?.fakeReceived(text: text, loggedAddress: loggedAccount, sender: partner, date: date, unread: unread, silent: silent, markdown: true, completion: completion) } diff --git a/Adamant/Services/DataProviders/AdamantChatsProvider.swift b/Adamant/Services/DataProviders/AdamantChatsProvider.swift index 7072e5f4e..66fc390d9 100644 --- a/Adamant/Services/DataProviders/AdamantChatsProvider.swift +++ b/Adamant/Services/DataProviders/AdamantChatsProvider.swift @@ -18,13 +18,15 @@ class AdamantChatsProvider: ChatsProvider { var accountsProvider: AccountsProvider! var securedStore: SecuredStore! + var richProviders: [String:RichMessageProviderWithStatusCheck]! + // MARK: Properties private(set) var state: State = .empty private(set) var isInitiallySynced: Bool = false private(set) var receivedLastHeight: Int64? private(set) var readedLastHeight: Int64? private let apiTransactions = 100 - private var unconfirmedTransactions: [UInt64:MessageTransaction] = [:] + private var unconfirmedTransactions: [UInt64:NSManagedObjectID] = [:] private let processingQueue = DispatchQueue(label: "im.adamant.processing.chat", qos: .utility, attributes: [.concurrent]) private let sendingQueue = DispatchQueue(label: "im.adamant.sending.chat", qos: .utility, attributes: [.concurrent]) @@ -256,137 +258,167 @@ extension AdamantChatsProvider { // MARK: - Sending messages { extension AdamantChatsProvider { func sendMessage(_ message: AdamantMessage, recipientId: String, completion: @escaping (ChatsProviderResult) -> Void) { - guard let loggedAccount = accountService.account, let keypair = accountService.keypair else { - completion(.failure(.notLogged)) - return - } - - guard loggedAccount.balance >= message.fee else { - completion(.failure(.notEnoughtMoneyToSend)) - return - } - - switch validateMessage(message) { - case .isValid: - break - - case .empty: - completion(.failure(.messageNotValid(.empty))) - return - - case .tooLong: - completion(.failure(.messageNotValid(.tooLong))) - return - } - - sendingQueue.async { - switch message { + guard let loggedAccount = accountService.account, let keypair = accountService.keypair else { + completion(.failure(.notLogged)) + return + } + + guard loggedAccount.balance >= message.fee else { + completion(.failure(.notEnoughtMoneyToSend)) + return + } + + switch validateMessage(message) { + case .isValid: + break + + case .empty: + completion(.failure(.messageNotValid(.empty))) + return + + case .tooLong: + completion(.failure(.messageNotValid(.tooLong))) + return + } + + sendingQueue.async { + switch message { case .text(let text), .markdownText(let text): - self.sendTextMessage(text: text, senderId: loggedAccount.address, recipientId: recipientId, keypair: keypair, completion: completion) - } - } - } - - private func sendTextMessage(text: String, senderId: String, recipientId: String, keypair: Keypair, completion: @escaping (ChatsProviderResult) -> Void) { - // MARK: 0. Prepare - let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) - privateContext.parent = stack.container.viewContext - - - // MARK: 1. Get recipient account - let accountsGroup = DispatchGroup() - accountsGroup.enter() - var acc: CoreDataAccount? = nil - accountsProvider.getAccount(byAddress: recipientId) { result in - defer { - accountsGroup.leave() - } - - switch result { - case .notFound, .invalidAddress: - completion(.failure(.accountNotFound(recipientId))) - - case .serverError(let error): - completion(.failure(.serverError(error))) + self.sendTextMessage(text: text, senderId: loggedAccount.address, recipientId: recipientId, keypair: keypair, type: message.chatType, completion: completion) - case .networkError(_): - completion(.failure(ChatsProviderError.networkError)) - - case .success(let account): - acc = account + case .richMessage(let payload): + self.sendRichMessage(richContent: payload.content(), richType: payload.type, senderId: loggedAccount.address, recipientId: recipientId, keypair: keypair, completion: completion) } - } - - accountsGroup.wait() - - guard let account = acc, let recipientPublicKey = account.publicKey else { - return - } - - - // MARK 2. Get Chatroom - let chatroom = privateContext.object(with: account.chatroom!.objectID) as! Chatroom - - - // MARK: 3. Create chat transaction - let type = ChatType.message - let transaction = MessageTransaction(entity: MessageTransaction.entity(), insertInto: privateContext) - transaction.date = Date() as NSDate - transaction.recipientId = recipientId - transaction.senderId = senderId - transaction.type = Int16(type.rawValue) - transaction.isOutgoing = true - transaction.message = text - - transaction.transactionId = UUID().uuidString - transaction.blockId = UUID().uuidString + } + } + + private func sendTextMessage(text: String, senderId: String, recipientId: String, keypair: Keypair, type: ChatType, completion: @escaping (ChatsProviderResult) -> Void) { + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + context.parent = stack.container.viewContext + + let transaction = MessageTransaction(context: context) + transaction.date = Date() as NSDate + transaction.recipientId = recipientId + transaction.senderId = senderId + transaction.type = Int16(type.rawValue) + transaction.isOutgoing = true + + transaction.message = text + + prepareAndSendChatTransaction(transaction, in: context, recipientId: recipientId, type: type, keypair: keypair, completion: completion) + } + + private func sendRichMessage(richContent: [String:String], richType: String, senderId: String, recipientId: String, keypair: Keypair, completion: @escaping (ChatsProviderResult) -> Void) { + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + context.parent = stack.container.viewContext + + let type = ChatType.richMessage + + let transaction = RichMessageTransaction(context: context) + transaction.date = Date() as NSDate + transaction.recipientId = recipientId + transaction.senderId = senderId + transaction.type = Int16(type.rawValue) + transaction.isOutgoing = true + + transaction.richContent = richContent + transaction.richType = richType + + transaction.transactionStatus = richProviders[richType] != nil ? .notInitiated : nil + + prepareAndSendChatTransaction(transaction, in: context, recipientId: recipientId, type: type, keypair: keypair, completion: completion) + } + + + /// Transaction must be in passed context + private func prepareAndSendChatTransaction(_ transaction: ChatTransaction, in context: NSManagedObjectContext, recipientId: String, type: ChatType, keypair: Keypair, completion: @escaping (ChatsProviderResult) -> Void) { + // MARK: 1. Get account + let accountsGroup = DispatchGroup() + accountsGroup.enter() + + var result: AccountsProviderResult! = nil + accountsProvider.getAccount(byAddress: recipientId) { r in + result = r + accountsGroup.leave() + } + + accountsGroup.wait() + + let recipientAccount: CoreDataAccount + switch result! { + case .success(let account): + recipientAccount = account + + case .notFound, .invalidAddress: + completion(.failure(.accountNotFound(recipientId))) + return + + case .serverError(let error): + completion(.failure(.serverError(error))) + return + + case .networkError(_): + completion(.failure(ChatsProviderError.networkError)) + return + } + + guard let recipientPublicKey = recipientAccount.publicKey else { + completion(.failure(.accountNotFound(recipientId))) + return + } + + // MARK: 2. Get Chatroom + guard let id = recipientAccount.chatroom?.objectID, let chatroom = context.object(with: id) as? Chatroom else { + completion(.failure(.accountNotFound(recipientId))) + return + } + // MARK: 3. Prepare transaction + transaction.transactionId = UUID().uuidString + transaction.blockId = UUID().uuidString transaction.statusEnum = MessageStatus.pending - - 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 { - chatroom.lastTransaction = transaction - chatroom.updatedAt = transaction.date - } - } else { - chatroom.lastTransaction = transaction - chatroom.updatedAt = transaction.date - } - - - // MARK: 5. Save unconfirmed transaction - do { - try privateContext.save() - } catch { - completion(.failure(.internalError(error))) - return - } - - - // MARK: 6. Send - sendTransaction(transaction, type: type, keypair: keypair, recipientPublicKey: recipientPublicKey) { result in - switch result { - case .success: - do { - try privateContext.save() - completion(.success) - } catch { - completion(.failure(.internalError(error))) - } - - case .failure(let error): - try? privateContext.save() - completion(.failure(error)) - } - } - } + + 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 { + chatroom.lastTransaction = transaction + chatroom.updatedAt = transaction.date + } + } else { + chatroom.lastTransaction = transaction + chatroom.updatedAt = transaction.date + } + + // MARK: 5. Save unconfirmed transaction + do { + try context.save() + } catch { + completion(.failure(.internalError(error))) + return + } + + // MARK: 6. Send + sendTransaction(transaction, type: type, keypair: keypair, recipientPublicKey: recipientPublicKey) { result in + switch result { + case .success: + do { + try context.save() + completion(.success) + } catch { + completion(.failure(.internalError(error))) + } + + case .failure(let error): + try? context.save() + completion(.failure(error)) + } + } + } - func retrySendMessage(_ message: MessageTransaction, completion: @escaping (ChatsProviderRetryCancelResult) -> Void) { + func retrySendMessage(_ message: ChatTransaction, completion: @escaping (ChatsProviderRetryCancelResult) -> Void) { // MARK: 0. Prepare switch message.statusEnum { case .delivered, .pending: @@ -455,7 +487,7 @@ extension AdamantChatsProvider { } // MARK: - Delete local message - func cancelMessage(_ message: MessageTransaction, completion: @escaping (ChatsProviderRetryCancelResult) -> Void) { + func cancelMessage(_ message: ChatTransaction, completion: @escaping (ChatsProviderRetryCancelResult) -> Void) { // MARK: 0. Prepare switch message.statusEnum { case .delivered, .pending: @@ -489,7 +521,7 @@ extension AdamantChatsProvider { /// /// If success - update transaction's id and add it to unconfirmed transactions. /// If fails - set transaction status to .failed - private func sendTransaction(_ transaction: MessageTransaction, type: ChatType, keypair: Keypair, recipientPublicKey: String, completion: @escaping (ChatsProviderResult) -> Void) { + private func sendTransaction(_ transaction: ChatTransaction, type: ChatType, keypair: Keypair, recipientPublicKey: String, completion: @escaping (ChatsProviderResult) -> Void) { // MARK: 0. Prepare guard let senderId = transaction.senderId, let recipientId = transaction.recipientId else { @@ -498,7 +530,7 @@ extension AdamantChatsProvider { } // MARK: 1. Encode - guard let text = transaction.message, let encodedMessage = adamantCore.encodeMessage(text, recipientPublicKey: recipientPublicKey, privateKey: keypair.privateKey) else { + guard let text = transaction.serializedMessage(), let encodedMessage = adamantCore.encodeMessage(text, recipientPublicKey: recipientPublicKey, privateKey: keypair.privateKey) else { completion(.failure(.dependencyError("Failed to encode message"))) return } @@ -510,10 +542,9 @@ extension AdamantChatsProvider { // Update ID with recieved, add to unconfirmed transactions. transaction.transactionId = String(id) - // If we will save transaction from privateContext, we will hold strong reference to whole context, and we won't ever save it. self.unconfirmedsSemaphore.wait() DispatchQueue.main.sync { - self.unconfirmedTransactions[id] = self.stack.container.viewContext.object(with: transaction.objectID) as? MessageTransaction + self.unconfirmedTransactions[id] = transaction.objectID } self.unconfirmedsSemaphore.signal() @@ -733,18 +764,18 @@ extension AdamantChatsProvider { var height: Int64 = 0 let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) privateContext.parent = context - var newMessageTransactions = [MessageTransaction]() + var newMessageTransactions = [ChatTransaction]() for (account, transactions) in partners { // We can't save whole context while we are mass creating MessageTransactions. let privateChatroom = privateContext.object(with: account.chatroom!.objectID) as! Chatroom // MARK: Transactions - var messages = Set() + var messages = Set() for trs in transactions { unconfirmedsSemaphore.wait() - if unconfirmedTransactions.count > 0, let unconfirmed = unconfirmedTransactions[trs.transaction.id] { + if let objectId = unconfirmedTransactions[trs.transaction.id], let unconfirmed = context.object(with: objectId) as? ChatTransaction { confirmTransaction(unconfirmed, id: trs.transaction.id, height: Int64(trs.transaction.height), blockId: trs.transaction.blockId, confirmations: trs.transaction.confirmations) let h = Int64(trs.transaction.height) if height < h { @@ -764,18 +795,19 @@ extension AdamantChatsProvider { publicKey = trs.transaction.senderPublicKey } - if let messageTransaction = messageTransaction(from: trs.transaction, isOutgoing: trs.isOut, publicKey: publicKey, privateKey: privateKey, context: privateContext) { - if height < messageTransaction.height { - height = messageTransaction.height + if let chatTransaction = chatTransaction(from: trs.transaction, isOutgoing: trs.isOut, publicKey: publicKey, privateKey: privateKey, context: privateContext) { + if height < chatTransaction.height { + height = chatTransaction.height } if !trs.isOut { - newMessageTransactions.append(messageTransaction) + newMessageTransactions.append(chatTransaction) // Preset messages if account.isSystem, let address = account.address, let messages = AdamantContacts.messagesFor(address: address), - let key = messageTransaction.message, + let messageTransaction = chatTransaction as? MessageTransaction, + let key = messageTransaction.message, let systemMessage = messages.first(where: { key.range(of: $0.key) != nil })?.value { switch systemMessage.message { @@ -785,13 +817,16 @@ extension AdamantChatsProvider { case .markdownText(let text): messageTransaction.message = text messageTransaction.isMarkdown = true + + case .richMessage(let payload): + messageTransaction.message = payload.serialized() } messageTransaction.silentNotification = systemMessage.silentNotification } } - messages.insert(messageTransaction) + messages.insert(chatTransaction) } } @@ -802,7 +837,7 @@ extension AdamantChatsProvider { // MARK: 4. Unread messagess if let readedLastHeight = readedLastHeight { let unreadTransactions = newMessageTransactions.filter { $0.height > readedLastHeight } - let chatrooms = Dictionary(grouping: unreadTransactions, by: ({ (t: MessageTransaction) -> Chatroom in t.chatroom! })) + let chatrooms = Dictionary(grouping: unreadTransactions, by: ({ (t: ChatTransaction) -> Chatroom in t.chatroom! })) for (chatroom, trs) in chatrooms { chatroom.hasUnreadMessages = true @@ -880,6 +915,19 @@ extension AdamantChatsProvider { 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 } } @@ -892,29 +940,49 @@ extension AdamantChatsProvider { /// - privateKey: logged account private key /// - context: context to insert parsed transaction to /// - Returns: New parsed transaction - private func messageTransaction(from transaction: Transaction, isOutgoing: Bool, publicKey: String, privateKey: String, context: NSManagedObjectContext) -> MessageTransaction? { + private func chatTransaction(from transaction: Transaction, isOutgoing: Bool, publicKey: String, privateKey: String, context: NSManagedObjectContext) -> ChatTransaction? { guard let chat = transaction.asset.chat else { return nil } - let messageTransaction = MessageTransaction(entity: MessageTransaction.entity(), insertInto: context) + let decodedMessage = adamantCore.decodeMessage(rawMessage: chat.message, rawNonce: chat.ownMessage, senderPublicKey: publicKey, privateKey: privateKey) + + let messageTransaction: ChatTransaction + switch chat.type { + case .message, .messageOld, .signal, .unknown: + let transaction = MessageTransaction(entity: MessageTransaction.entity(), insertInto: context) + transaction.message = decodedMessage + messageTransaction = transaction + + case .richMessage: + let transaction = RichMessageTransaction(entity: RichMessageTransaction.entity(), insertInto: context) + + if let decodedMessage = decodedMessage, + let data = decodedMessage.data(using: String.Encoding.utf8), + let json = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: String], + let type = json["type"] { + transaction.richType = type + transaction.richContent = json + + transaction.transactionStatus = richProviders[type] != nil ? .notInitiated : nil + } + + messageTransaction = transaction + } + 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.isConfirmed = true messageTransaction.isOutgoing = isOutgoing messageTransaction.blockId = transaction.blockId messageTransaction.confirmations = transaction.confirmations messageTransaction.statusEnum = MessageStatus.delivered - if let decodedMessage = adamantCore.decodeMessage(rawMessage: chat.message, rawNonce: chat.ownMessage, senderPublicKey: publicKey, privateKey: privateKey) { - messageTransaction.message = decodedMessage - } - return messageTransaction } @@ -924,7 +992,7 @@ extension AdamantChatsProvider { /// - Parameters: /// - transaction: Unconfirmed transaction /// - id: New transaction id /// - height: New transaction height - private func confirmTransaction(_ transaction: MessageTransaction, id: UInt64, height: Int64, blockId: String, confirmations: Int64) { + private func confirmTransaction(_ transaction: ChatTransaction, id: UInt64, height: Int64, blockId: String, confirmations: Int64) { if transaction.isConfirmed { return } diff --git a/Adamant/Services/DataProviders/AdamantTransfersProvider.swift b/Adamant/Services/DataProviders/AdamantTransfersProvider.swift index eb05b0faf..84806c037 100644 --- a/Adamant/Services/DataProviders/AdamantTransfersProvider.swift +++ b/Adamant/Services/DataProviders/AdamantTransfersProvider.swift @@ -26,9 +26,13 @@ class AdamantTransfersProvider: TransfersProvider { private(set) var readedLastHeight: Int64? private let apiTransactions = 100 - private let processingQueue = DispatchQueue(label: "im.Adamant.processing.transfers", qos: .utility, attributes: [.concurrent]) + private let processingQueue = DispatchQueue(label: "im.adamant.processing.transfers", qos: .utility, attributes: [.concurrent]) + private let sendingQueue = DispatchQueue(label: "im.adamant.sending.transfers", qos: .utility, attributes: [.concurrent]) private let stateSemaphore = DispatchSemaphore(value: 1) + private var unconfirmedTransactions: [UInt64:NSManagedObjectID] = [:] + private let unconfirmedsSemaphore = DispatchSemaphore(value: 1) + // MARK: Tools /// Free stateSemaphore before calling this method, or you will deadlock. @@ -279,15 +283,106 @@ extension AdamantTransfersProvider { } // MARK: Sending Funds + + // Wrapper func transferFunds(toAddress recipient: String, amount: Decimal, completion: @escaping (TransfersProviderResult) -> Void) { - guard let senderAddress = accountService.account?.address, let keypair = accountService.keypair else { + // Go background + sendingQueue.async { + self.transferFundsInternal(toAddress: recipient, amount: amount, completion: completion) + } + } + + private func transferFundsInternal(toAddress recipient: String, amount: Decimal, completion: @escaping (TransfersProviderResult) -> Void) { + // MARK: 0. Prepare + guard let senderId = accountService.account?.address, let keypair = accountService.keypair else { completion(.failure(.notLogged)) return } - - apiService.transferFunds(sender: senderAddress, recipient: recipient, amount: amount, keypair: keypair) { result in + + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + context.parent = stack.container.viewContext + + // MARK: 1. Get recipient + let accountsGroup = DispatchGroup() + accountsGroup.enter() + + var result: AccountsProviderResult! = nil + accountsProvider.getAccount(byAddress: recipient) { r in + result = r + accountsGroup.leave() + } + + accountsGroup.wait() + + let recipientAccount: CoreDataAccount + switch result! { + case .success(let account): + recipientAccount = account + + case .notFound, .invalidAddress: + completion(.failure(.accountNotFound(address: recipient))) + return + + case .serverError(let error): + completion(.failure(.serverError(error))) + return + + case .networkError(_): + completion(.failure(.networkError)) + return + } + + // MARK: 2. Create transaction + let transaction = TransferTransaction(context: context) + transaction.amount = amount as NSDecimalNumber + transaction.date = Date() as NSDate + transaction.recipientId = recipient + transaction.senderId = senderId + transaction.type = Int16(TransactionType.send.rawValue) + transaction.isOutgoing = true + + transaction.transactionId = UUID().uuidString + transaction.blockId = UUID().uuidString + transaction.statusEnum = MessageStatus.pending + + // MARK: 3. Chatroom + if let id = recipientAccount.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 { + chatroom.lastTransaction = transaction + chatroom.updatedAt = transaction.date + } + } else { + chatroom.lastTransaction = transaction + chatroom.updatedAt = transaction.date + } + } + + // MARK: 4. Save unconfirmed transaction + do { + try context.save() + } catch { + completion(.failure(.internalError(message: "Failed to save context", error: error))) + return + } + + // MARK: 5. Send + apiService.transferFunds(sender: senderId, recipient: recipient, amount: amount, keypair: keypair) { result in switch result { - case .success(_): + case .success(let id): + // Update ID with recieved, add to unconfirmed transactions. + transaction.transactionId = String(id) + + self.unconfirmedsSemaphore.wait() + DispatchQueue.main.sync { + self.unconfirmedTransactions[id] = transaction.objectID + } + self.unconfirmedsSemaphore.signal() + + completion(.success) case .failure(let error): @@ -508,7 +603,28 @@ extension AdamantTransfersProvider { var transfers = [TransferTransaction]() var height: Int64 = 0 for t in transactions { - let transfer = TransferTransaction(entity: TransferTransaction.entity(), insertInto: context) + unconfirmedsSemaphore.wait() + if let objectId = unconfirmedTransactions[t.id], let transaction = context.object(with: objectId) as? TransferTransaction { + transaction.isConfirmed = true + transaction.height = t.height + transaction.blockId = t.blockId + transaction.confirmations = t.confirmations + transaction.statusEnum = .delivered + + unconfirmedTransactions.removeValue(forKey: t.id) + + let h = Int64(t.height) + if height < h { + height = h + } + + unconfirmedsSemaphore.signal() + continue + } else { + unconfirmedsSemaphore.signal() + } + + let transfer = TransferTransaction(context: context) transfer.amount = t.amount as NSDecimalNumber transfer.date = t.date as NSDate transfer.fee = t.fee as NSDecimalNumber @@ -519,6 +635,7 @@ extension AdamantTransfersProvider { transfer.type = Int16(t.type.rawValue) transfer.blockId = t.blockId transfer.confirmations = t.confirmations + transfer.statusEnum = .delivered transfer.isOutgoing = t.senderId == address let partnerId = transfer.isOutgoing ? t.recipientId : t.senderId diff --git a/Adamant/Services/RepeaterService.swift b/Adamant/Services/RepeaterService.swift index 5a4c626b0..4f7c23bd9 100644 --- a/Adamant/Services/RepeaterService.swift +++ b/Adamant/Services/RepeaterService.swift @@ -61,7 +61,7 @@ class RepeaterService { let timer = Timer(timeInterval: interval, target: self, selector: #selector(timerFired), userInfo: client, repeats: true) client.timer = timer - RunLoop.main.add(timer, forMode: .commonModes) + RunLoop.main.add(timer, forMode: .common) } } @@ -106,7 +106,7 @@ class RepeaterService { let timer = Timer(timeInterval: client.interval, target: self, selector: #selector(timerFired), userInfo: client, repeats: true) client.timer = timer - RunLoop.main.add(timer, forMode: .commonModes) + RunLoop.main.add(timer, forMode: .common) } isPaused = false diff --git a/Adamant/Services/TokensApiService/AdamantLskApiService.swift b/Adamant/Services/TokensApiService/AdamantLskApiService.swift new file mode 100644 index 000000000..eee59458e --- /dev/null +++ b/Adamant/Services/TokensApiService/AdamantLskApiService.swift @@ -0,0 +1,284 @@ +// +// LskApiService.swift +// Adamant +// +// Created by Anton Boyarkin on 12/07/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import BigInt +import Lisk +import Ed25519 +import web3swift + +class AdamantLskApiService: LskApiService { + + // MARK: - Constans + static let kvsAddress = "lsk:address" + static let defaultFee = 0.1 + + // MARK: - Dependencies + var apiService: ApiService! + var accountService: AccountService! + + // MARK: - Properties + private(set) var account: LskAccount? + + private var accountApi: Accounts! + private var transactionApi: Transactions! + + init() { + accountApi = Accounts(client: .testnet) + transactionApi = Transactions(client: .testnet) + + NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userLoggedOut, object: nil, queue: OperationQueue.main) { [weak self] _ in + self?.account = nil + } + } + + func newAccount(byPassphrase passphrase: String, completion: @escaping (ApiServiceResult) -> Void) { + /* + do { + let keys: KeyPair! = try Crypto.keyPair(fromPassphrase: passphrase) + let address: String! = Crypto.address(fromPublicKey: keys.publicKeyString) + let account = LskAccount(keys: keys, address: address, balance: BigUInt(0), balanceString: "0") + self.account = account +// print(address) + completion(.success(account)) + } catch { + print("\(error)") + completion(.failure(.accountNotFound)) + return + } + + NotificationCenter.default.post(name: Notification.Name.LskApiService.userLoggedIn, object: self) + + self.getBalance({ _ in }) + + if let account = self.account, let address = self.accountService.account?.address, let keypair = self.accountService.keypair { + self.getLskAddress(byAdamandAddress: address) { (result) in + switch result { + case .success(let value): + if value == nil { + guard let loggedAccount = self.accountService.account else { + DispatchQueue.main.async { + completion(.failure(.notLogged)) + } + return + } + + guard loggedAccount.balance >= AdamantApiService.KvsFee else { + DispatchQueue.main.async { + completion(.failure(.internalError(message: "LSK Wallet: Not enought ADM to save address to KVS", error: nil))) + } + return + } + + self.apiService.store(key: AdamantLskApiService.kvsAddress, value: account.address, type: StateType.keyValue, sender: address, keypair: keypair, completion: { (result) in + switch result { + case .success(let transactionId): + print("SAVED LSK in KVS: \(transactionId)") + break + case .failure(let error): + DispatchQueue.main.async { + completion(.failure(.internalError(message: "LSK Wallet: fail to save address to KVS", error: error))) + } + break + } + }) + } else { + print("FOUND LSK in KVS: \(value!)") + } + break + case .failure(let error): + DispatchQueue.main.async { + completion(.failure(.internalError(message: "LSK Wallet: fail to get address from KVS", error: error))) + } + break + } + } + } + */ + } + + func createTransaction(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult) -> Void) { + if let keys = self.account?.keys { + do { + let transaction = LocalTransaction(.transfer, lsk: amount, recipientId: address) + let signedTransaction = try transaction.signed(keyPair: keys) + + completion(.success(signedTransaction)) + } catch { + completion(.failure(.internalError(message: error.localizedDescription, error: error))) + } + } + } + + func sendTransaction(transaction: LocalTransaction, completion: @escaping (ApiServiceResult) -> Void) { + transactionApi.submit(signedTransaction: transaction) { response in + switch response { + case .success(let result): + print(result.data.hashValue) + print(result.data.message) + + completion(.success(transaction.id ?? "")) + case .error(let error): + print("ERROR: " + error.message) + completion(.failure(.internalError(message: error.message, error: nil))) + } + } + } + + func sendFunds(toAddress address: String, amount: Double, completion: @escaping (ApiServiceResult) -> Void) { + if let keys = self.account?.keys { + + do { + let transaction = LocalTransaction(.transfer, lsk: amount, recipientId: address) + let signedTransaction = try transaction.signed(keyPair: keys) + + transactionApi.submit(signedTransaction: signedTransaction) { response in + switch response { + case .success(let result): + print(result.data.hashValue) + print(result.data.message) + + if let id = signedTransaction.id { + let result = ["type": "lsk_transaction", "amount": "\(amount)", "hash": id, "comments":""] + + do { + let data = try JSONEncoder().encode(result) + guard let raw = String(data: data, encoding: String.Encoding.utf8) else { + return + } + completion(.success(raw)) + } catch { + completion(.failure(.internalError(message: "LSK Wallet: Send - wrong data issue", error: nil))) + } + } else { + completion(.failure(.internalError(message: "LSK Wallet: Send - wrong data issue", error: nil))) + } + + + case .error(let error): + print("ERROR: " + error.message) + completion(.failure(.internalError(message: error.message, error: nil))) + } + } + } catch { + completion(.failure(.internalError(message: error.localizedDescription, error: error))) + } + } + } + + func getTransactions(_ completion: @escaping (ApiServiceResult<[Transactions.TransactionModel]>) -> Void) { + if let address = self.account?.address { + transactionApi.transactions(senderIdOrRecipientId: address, limit: 100, offset: 0, sort: APIRequest.Sort("timestamp", direction: .descending)) { (response) in + switch response { + case .success(response: let result): + completion(.success(result.data)) + break + case .error(response: let error): + print("ERROR: " + error.message) + completion(.failure(.internalError(message: error.message, error: nil))) + break + } + } + } + } + + func getTransaction(byHash hash: String, completion: @escaping (ApiServiceResult) -> Void) { + transactionApi.transactions(id: hash, limit: 1, offset: 0) { (response) in + switch response { + case .success(response: let result): + if let transaction = result.data.first { + completion(.success(transaction)) + } else { + completion(.failure(.internalError(message: "No transaction", error: nil))) + } + break + case .error(response: let error): + print("ERROR: " + error.message) + completion(.failure(.internalError(message: error.message, error: nil))) + break + } + } + } + + // MARK: - Tools + func getBalance(_ completion: @escaping (ApiServiceResult) -> Void) { + if let address = self.account?.address { + accountApi.accounts(address: address) { (response) in + switch response { + case .success(response: let response): + if let account = response.data.first { + let balance = BigUInt(account.balance ?? "0") ?? BigUInt(0) + + self.account?.balance = balance + self.account?.balanceString = self.fromRawLsk(value: balance) + + if let balanceString = self.account?.balanceString, let balance = Double(balanceString) { + self.account?.balanceString = "\(balance)" + } + } + + completion(.success("\(self.account?.balanceString ?? "--") LSK")) + + break + case .error(response: let error): + print(error) + completion(.failure(.serverError(error: error.message))) + break + } + } + } else { + completion(.failure(.internalError(message: "LSK Wallet: not found", error: nil))) + } + } + + func getLskAddress(byAdamandAddress address: String, completion: @escaping (ApiServiceResult) -> Void) { + apiService.get(key: AdamantLskApiService.kvsAddress, sender: address, completion: completion) + } + + func fromRawLsk(value: BigUInt) -> String { + if let formattedAmount = Web3.Utils.formatToPrecision(value, numberDecimals: 8, formattingDecimals: 8, decimalSeparator: ".", fallbackToScientific: false) { + return formattedAmount + } else { + return "--" + } + } + + func toRawLsk(value: Double) -> String { + if let formattedAmount = Web3.Utils.parseToBigUInt("\(value)", decimals: 8) { + return "\(formattedAmount)" + } else { + return "--" + } + } + + private static let addressRegexString = "^([0-9]{2,22})L$" + private static let addressRegex = try! NSRegularExpression(pattern: addressRegexString, options: []) + private static let maxAddressNumber = BigUInt("18446744073709551615")! + + /// Rules are simple: + /// + /// - Tailing uppercase L + /// - From 2 to 22 numbers + /// - Address number lower 18446744073709551615 + /// - No leading or trailing whitespaces + static func validateAddress(address: String) -> AdamantUtilities.AddressValidationResult { + let value = address.replacingOccurrences(of: "L", with: "") + + if validate(string: address, with: addressRegex), let number = BigUInt(value), number < maxAddressNumber { + return .valid + } else { + return .invalid + } + } + + 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 + } +} diff --git a/Adamant/SharedViews/AlertLabelCell.xib b/Adamant/SharedViews/AlertLabelCell.xib index cda198dee..38692a465 100644 --- a/Adamant/SharedViews/AlertLabelCell.xib +++ b/Adamant/SharedViews/AlertLabelCell.xib @@ -30,7 +30,7 @@ - + diff --git a/Adamant/SharedViews/DoubleDetailsTableViewCell.swift b/Adamant/SharedViews/DoubleDetailsTableViewCell.swift new file mode 100644 index 000000000..d8049947c --- /dev/null +++ b/Adamant/SharedViews/DoubleDetailsTableViewCell.swift @@ -0,0 +1,60 @@ +// +// DoubleDetailsTableViewCell +// Adamant +// +// Created by Anokhov Pavel on 30.07.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import Eureka + +public struct DoubleDetail: Equatable { + let first: String + let second: String? +} + +public final class DoubleDetailsTableViewCell: Cell, CellType { + + // MARK: Constants + static let compactHeight: CGFloat = 50.0 + static let fullHeight: CGFloat = 70.0 + + // MARK: IBOutlets + @IBOutlet var titleLabel: UILabel! + @IBOutlet var detailsLabel: UILabel! + @IBOutlet var secondDetailsLabel: UILabel! + + // MARK: Properties + var secondValue: String? { + get { + return secondDetailsLabel.text + } + set { + secondDetailsLabel.text = newValue + if newValue == nil { + secondDetailsLabel.isHidden = true + } + } + } + + public override func update() { + super.update() + + if let value = row.value { + detailsLabel.text = value.first + secondDetailsLabel.text = value.second + } else { + detailsLabel.text = nil + secondDetailsLabel.text = nil + } + } +} + +public final class DoubleDetailsRow: Row, RowType { + required public init(tag: String?) { + super.init(tag: tag) + // We set the cellProvider to load the .xib corresponding to our cell + cellProvider = CellProvider(nibName: "DoubleDetailsTableViewCell") + } +} diff --git a/Adamant/SharedViews/DoubleDetailsTableViewCell.xib b/Adamant/SharedViews/DoubleDetailsTableViewCell.xib new file mode 100644 index 000000000..9d18ddb87 --- /dev/null +++ b/Adamant/SharedViews/DoubleDetailsTableViewCell.xib @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Adamant/SharedViews/EurekaAlertLabelRow.swift b/Adamant/SharedViews/EurekaAlertLabelRow.swift index a1f7abe0e..4b6934a0c 100644 --- a/Adamant/SharedViews/EurekaAlertLabelRow.swift +++ b/Adamant/SharedViews/EurekaAlertLabelRow.swift @@ -14,7 +14,7 @@ public final class AlertLabelCell: Cell, CellType { var inCellAccessoryView: UIView! private (set) var alertLabel: RoundedLabel! - required public init(style: UITableViewCellStyle, reuseIdentifier: String?) { + required public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) if style == .value1, let detailTextLabel = detailTextLabel { diff --git a/Adamant/SharedViews/FullscreenAlertView.swift b/Adamant/SharedViews/FullscreenAlertView.swift new file mode 100644 index 000000000..972ebfbeb --- /dev/null +++ b/Adamant/SharedViews/FullscreenAlertView.swift @@ -0,0 +1,51 @@ +// +// FullscreenAlertView.swift +// Adamant +// +// Created by Anokhov Pavel on 04.09.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit + +class FullscreenAlertView: UIView { + + // MARK: IBOutlets + + @IBOutlet weak var alertBackgroundView: UIView! + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var messageLabel: UILabel! + + // MARK: Setters & Getters + + var title: String? { + get { + return titleLabel.text + } + set { + titleLabel.isHidden = newValue == nil + titleLabel.text = newValue + } + } + + var message: String? { + get { + return messageLabel.text + } + set { + messageLabel.isHidden = newValue == nil + messageLabel.text = newValue + } + } + + + // MARK: Lifecycle + + override func awakeFromNib() { + super.awakeFromNib() + alertBackgroundView.layer.cornerRadius = 14 + titleLabel.isHidden = true + messageLabel.isHidden = true + } +} diff --git a/Adamant/SharedViews/FullscreenAlertView.xib b/Adamant/SharedViews/FullscreenAlertView.xib new file mode 100644 index 000000000..5b508432c --- /dev/null +++ b/Adamant/SharedViews/FullscreenAlertView.xib @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Adamant/SharedViews/TransferCollectionViewCell.swift b/Adamant/SharedViews/TransferCollectionViewCell.swift new file mode 100644 index 000000000..89cdaf4fa --- /dev/null +++ b/Adamant/SharedViews/TransferCollectionViewCell.swift @@ -0,0 +1,159 @@ +// +// TransferCollectionViewCell.swift +// Adamant +// +// Created by Anokhov Pavel on 08.09.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +//import MessageKit + +class TransferCollectionViewCell: UICollectionViewCell, ChatCell, TapRecognizerCustomCell { + @IBOutlet weak var sentLabel: UILabel! + @IBOutlet weak var amountLabel: UILabel! + @IBOutlet weak var currencySymbolLabel: UILabel! + @IBOutlet weak var currencyLogoImageView: UIImageView! + @IBOutlet weak var tapForDetailsLabel: UILabel! + @IBOutlet weak var dateLabel: UILabel! + + @IBOutlet weak var transferContentView: UIView! + @IBOutlet weak var bubbleView: UIView! + + @IBOutlet weak var leadingConstraint: NSLayoutConstraint? + @IBOutlet weak var trailingConstraint: NSLayoutConstraint? + + @IBOutlet weak var statusView: UIView! + @IBOutlet weak var statusImageView: UIImageView! + @IBOutlet weak var statusLeadingConstraint: NSLayoutConstraint? + @IBOutlet weak var statusTrailingConstraint: NSLayoutConstraint? + + weak var delegate: CustomCellDelegate? = nil + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + + bubbleView.layer.cornerRadius = 16.0 + bubbleView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(didTap))) + } + + // MARK: - Tap + + @objc func didTap(sender: UITapGestureRecognizer) { + delegate?.didTapCustomCell(self) + } + + // MARK: - Status + var transactionStatus: TransactionStatus? { + didSet { + if let status = transactionStatus { + statusView.isHidden = false + statusImageView.image = status.image + statusImageView.tintColor = status.imageTintColor + } else { + statusView.isHidden = true + } + } + } + + // MARK: - Appearance + + var bubbleBackgroundColor: UIColor? { + get { return bubbleView.backgroundColor } + set { bubbleView.backgroundColor = newValue } + } + + var isAlignedRight: Bool = false { + didSet { + if isAlignedRight { + // Bubble + if let leadingConstraint = leadingConstraint { + contentView.removeConstraint(leadingConstraint) + } + + if trailingConstraint == nil { + let trailing = NSLayoutConstraint(item: contentView, + attribute: .trailing, + relatedBy: .equal, + toItem: transferContentView, + attribute: .trailing, + multiplier: 1.0, + constant: 5.0) + contentView.addConstraint(trailing) + trailingConstraint = trailing + } + + // Status + if let statusLeadingConstraint = statusLeadingConstraint { + contentView.removeConstraint(statusLeadingConstraint) + } + + if statusTrailingConstraint == nil { + let trailing = NSLayoutConstraint(item: transferContentView, + attribute: .leading, + relatedBy: .equal, + toItem: statusView, + attribute: .trailing, + multiplier: 1.0, + constant: 12.0) + contentView.addConstraint(trailing) + statusTrailingConstraint = trailing + } + } else { + // Bubble + if let trailingConstraint = trailingConstraint { + contentView.removeConstraint(trailingConstraint) + } + + if leadingConstraint == nil { + let leading = NSLayoutConstraint(item: contentView, + attribute: .leading, + relatedBy: .equal, + toItem: transferContentView, + attribute: .leading, + multiplier: 1.0, + constant: 5.0) + contentView.addConstraint(leading) + leadingConstraint = leading + } + + // Status + if let statusTrailingConstraint = statusTrailingConstraint { + contentView.removeConstraint(statusTrailingConstraint) + } + + if statusLeadingConstraint == nil { + let leading = NSLayoutConstraint(item: transferContentView, + attribute: .trailing, + relatedBy: .equal, + toItem: statusView, + attribute: .leading, + multiplier: 1.0, + constant: 12.0) + contentView.addConstraint(leading) + statusLeadingConstraint = leading + } + } + } + } +} + +extension TransactionStatus { + var image: UIImage { + switch self { + case .notInitiated, .updating: return #imageLiteral(resourceName: "status_updating") + case .pending:return #imageLiteral(resourceName: "status_pending") + case .success: return #imageLiteral(resourceName: "status_success") + case .failed: return #imageLiteral(resourceName: "status_failed") + } + } + + var imageTintColor: UIColor { + switch self { + case .notInitiated, .updating: return UIColor.adamant.secondary + case .pending, .success: return UIColor.adamant.primary + case .failed: return UIColor.adamant.transferOutcomeIconBackground + } + } +} diff --git a/Adamant/SharedViews/TransferCollectionViewCell.xib b/Adamant/SharedViews/TransferCollectionViewCell.xib new file mode 100644 index 000000000..c5b859836 --- /dev/null +++ b/Adamant/SharedViews/TransferCollectionViewCell.xib @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Adamant/SharedViews/TransferMessageSizeCalculator.swift b/Adamant/SharedViews/TransferMessageSizeCalculator.swift new file mode 100644 index 000000000..35eb4049b --- /dev/null +++ b/Adamant/SharedViews/TransferMessageSizeCalculator.swift @@ -0,0 +1,50 @@ +// +// TransferMessageSizeCalculator.swift +// Adamant +// +// Created by Anokhov Pavel on 08.09.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import MessageKit + +open class TransferMessageSizeCalculator: MessageSizeCalculator { + let cellWidth: CGFloat = 87 + let cellHeight: CGFloat = 145 + var font: UIFont! = nil + + override open func messageContainerSize(for message: MessageType) -> CGSize { + guard case MessageKind.custom(let raw) = message.kind, let transfer = raw as? RichMessageTransfer else { + fatalError("messageContainerSize received unhandled MessageDataType: \(message.kind)") + } + + let amount = transfer.amount + + var messageContainerSize = CGSize(width: cellWidth, height: cellHeight) + + let maxWidth = messageContainerMaxWidth(for: message) + let attributedText = NSAttributedString(string: amount, attributes: [.font: font]) + let constraintBox = CGSize(width: maxWidth, height: .greatestFiniteMagnitude) + let rect = attributedText.boundingRect(with: constraintBox, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil).integral + + messageContainerSize.width += rect.width + + return messageContainerSize + } + + open override func cellContentHeight(for message: MessageType, at indexPath: IndexPath) -> CGFloat { + return cellHeight + } + + +} + +extension NSAttributedString { + func width(withConstrainedHeight height: CGFloat) -> CGFloat { + let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height) + let boundingBox = boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, context: nil) + + return ceil(boundingBox.width) + } +} diff --git a/Adamant/Stories/Account/AccountHeader.xib b/Adamant/Stories/Account/AccountHeader.xib index 3c5a2eefa..cbfa36ff1 100644 --- a/Adamant/Stories/Account/AccountHeader.xib +++ b/Adamant/Stories/Account/AccountHeader.xib @@ -17,14 +17,10 @@ - - + + - - - - @@ -56,31 +52,19 @@ - - + + - - - - - - - - - - + - - + + - - - - + - + @@ -88,10 +72,9 @@ - - + - + diff --git a/Adamant/Stories/Account/AccountHeaderView.swift b/Adamant/Stories/Account/AccountHeaderView.swift index 3124d551e..cc1a67850 100644 --- a/Adamant/Stories/Account/AccountHeaderView.swift +++ b/Adamant/Stories/Account/AccountHeaderView.swift @@ -16,9 +16,8 @@ class AccountHeaderView: UIView { // MARK: - IBOutlets @IBOutlet weak var avatarImageView: UIImageView! - @IBOutlet weak var walletCollectionView: UICollectionView! @IBOutlet weak var addressButton: UIButton! - @IBOutlet weak var backgroundTopConstraint: NSLayoutConstraint! + @IBOutlet weak var walletViewContainer: UIView! weak var delegate: AccountHeaderViewDelegate? diff --git a/Adamant/Stories/Account/AccountRoutes.swift b/Adamant/Stories/Account/AccountRoutes.swift index a6db08315..8bdd4d57d 100644 --- a/Adamant/Stories/Account/AccountRoutes.swift +++ b/Adamant/Stories/Account/AccountRoutes.swift @@ -20,14 +20,6 @@ extension AdamantScene { return c } - static let transfer = AdamantScene(identifier: "TransferViewController") { r in - let c = TransferViewController() - c.apiService = r.resolve(ApiService.self) - c.accountService = r.resolve(AccountService.self) - c.dialogService = r.resolve(DialogService.self) - return c - } - private init() {} } } diff --git a/Adamant/Stories/Account/AccountViewController.swift b/Adamant/Stories/Account/AccountViewController.swift index fb3ca2885..9afd54dba 100644 --- a/Adamant/Stories/Account/AccountViewController.swift +++ b/Adamant/Stories/Account/AccountViewController.swift @@ -11,6 +11,7 @@ import Eureka import SafariServices import FreakingSimpleRoundImageView import CoreData +import Parchment // MARK: - Localization @@ -31,25 +32,6 @@ extension String.adamantLocalized.alert { static let logoutButton = NSLocalizedString("AccountTab.ConfirmLogout.Logout", comment: "Account tab: Confirm logout alert: Logout (Ok) button") } - -// MARK: - Wallet extension -fileprivate extension Wallet { - var sectionTag: String { - switch self { - case .adamant: return "s_adm" - case .ethereum: return "s_eth" - } - } - - var sectionTitle: String { - switch self { - case .adamant: return NSLocalizedString("AccountTab.Sections.adamant_wallet", comment: "Account tab: Adamant wallet section") - case .ethereum: return NSLocalizedString("AccountTab.Sections.ethereum_wallet", comment: "Account tab: Ethereum wallet section") - } - } -} - - // MARK: AccountViewController class AccountViewController: FormViewController { // MARK: - Rows & Sections @@ -133,27 +115,17 @@ class AccountViewController: FormViewController { var transfersProvider: TransfersProvider! - // MARK: - Wallets - var selectedWalletIndex: Int = 0 - - // MARK: - Properties var hideFreeTokensRow = false let walletCellIdentifier = "wllt" private (set) var accountHeaderView: AccountHeaderView! - var wallets: [Wallet]? { - didSet { - selectedWalletIndex = 0 - accountHeaderView?.walletCollectionView.reloadData() - } - } private var transfersController: NSFetchedResultsController? + private var pagingViewController: PagingViewController! - private let accessoryContentInsets = UIEdgeInsets(top: 2, left: 4, bottom: 2, right: 4) - private let accessoryContainerInsets = UIEdgeInsets(top: 1, left: 1, bottom: 1, right: 1) + private var initiated = false // MARK: - Lifecycle @@ -163,7 +135,10 @@ class AccountViewController: FormViewController { navigationOptions = .Disabled navigationController?.setNavigationBarHidden(true, animated: false) - wallets = [.adamant(balance: Decimal(floatLiteral: 100.001)), .ethereum] + // MARK: Status Bar + let statusBarView = UIView(frame: UIApplication.shared.statusBarFrame) + statusBarView.backgroundColor = UIColor.white + view.addSubview(statusBarView) // MARK: Transfers controller let controller = transfersProvider.unreadTransfersController() @@ -184,13 +159,6 @@ class AccountViewController: FormViewController { accountHeaderView = header accountHeaderView.delegate = self - accountHeaderView.walletCollectionView.delegate = self - accountHeaderView.walletCollectionView.dataSource = self - accountHeaderView.walletCollectionView.register(UINib(nibName: "WalletCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: walletCellIdentifier) - - if #available(iOS 11.0, *), let topInset = UIApplication.shared.keyWindow?.safeAreaInsets.top, topInset > 0 { - accountHeaderView.backgroundTopConstraint.constant = -topInset - } updateAccountInfo() @@ -200,25 +168,26 @@ class AccountViewController: FormViewController { tableView.tableFooterView = footer } + // MARK: Wallet view + pagingViewController = PagingViewController() - // MARK: Wallets - if let wallets = wallets { - for (walletIndex, wallet) in wallets.enumerated() { - let section = createSectionFor(wallet: wallet) - - section.hidden = Condition.function([], { [weak self] _ -> Bool in - guard let selectedIndex = self?.selectedWalletIndex else { - return true - } - - return walletIndex != selectedIndex - }) - - form.append(section) + pagingViewController.menuItemSource = .nib(nib: UINib(nibName: "WalletCollectionViewCell", bundle: nil)) + 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 + pagingViewController.select(index: 0) + accountHeaderView.walletViewContainer.addSubview(pagingViewController.view) + accountHeaderView.walletViewContainer.constrainToEdges(pagingViewController.view) + addChild(pagingViewController) + + for wallet in accountService.wallets { + NotificationCenter.default.addObserver(forName: wallet.walletUpdatedNotification, object: nil, queue: OperationQueue.main) { [weak self] _ in + self?.pagingViewController.reloadData() } } - // MARK: Application form +++ Section(Sections.application.localized) { $0.tag = Sections.application.tag @@ -331,8 +300,6 @@ class AccountViewController: FormViewController { form.allRows.forEach { $0.baseCell.imageView?.tintColor = UIColor.adamant.tableRowIcons } - accountHeaderView.walletCollectionView.selectItem(at: IndexPath(row: 0, section: 0), animated: false, scrollPosition: .centeredHorizontally) - // MARK: Notification Center NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userLoggedIn, object: nil, queue: OperationQueue.main) { [weak self] _ in @@ -346,6 +313,27 @@ class AccountViewController: FormViewController { NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.accountDataUpdated, object: nil, queue: OperationQueue.main) { [weak self] _ in self?.updateAccountInfo() } + + NotificationCenter.default.addObserver(forName: Notification.Name.WalletViewController.heightUpdated, object: nil, queue: OperationQueue.main) { [weak self] notification in + if let vc = notification.object as? WalletViewController, + let cvc = self?.pagingViewController.pageViewController.selectedViewController, + vc.viewController == cvc { + + if let initiated = self?.initiated { + self?.updateHeaderSize(with: vc, animated: initiated) + } else { + self?.updateHeaderSize(with: vc, animated: false) + } + } + } + + for (index, service) in accountService.wallets.enumerated() { + NotificationCenter.default.addObserver(forName: service.walletUpdatedNotification, + object: service, + queue: OperationQueue.main) { [weak self] _ in + self?.pagingViewController.collectionView.reloadItems(at: [IndexPath(row: index, section: 0)]) + } + } } override func viewWillAppear(_ animated: Bool) { @@ -355,13 +343,9 @@ class AccountViewController: FormViewController { if let indexPath = tableView.indexPathForSelectedRow { tableView.deselectRow(at: indexPath, animated: animated) } - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - if #available(iOS 11.0, *) { - navigationController?.navigationBar.prefersLargeTitles = false + for vc in pagingViewController.pageViewController.children { + vc.viewWillAppear(animated) } } @@ -370,6 +354,18 @@ class AccountViewController: FormViewController { navigationController?.setNavigationBarHidden(false, animated: animated) } + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if !initiated { + initiated = true + } + + if #available(iOS 11.0, *) { + navigationController?.navigationBar.prefersLargeTitles = false + } + } + deinit { NotificationCenter.default.removeObserver(self) } @@ -377,11 +373,11 @@ class AccountViewController: FormViewController { // MARK: TableView configuration - override func insertAnimation(forSections sections: [Section]) -> UITableViewRowAnimation { + override func insertAnimation(forSections sections: [Section]) -> UITableView.RowAnimation { return .fade } - override func deleteAnimation(forSections sections: [Section]) -> UITableViewRowAnimation { + override func deleteAnimation(forSections sections: [Section]) -> UITableView.RowAnimation { return .fade } @@ -389,141 +385,24 @@ class AccountViewController: FormViewController { // MARK: Other func updateAccountInfo() { let address: String - let adamantWallet: Wallet if let account = accountService.account { address = account.address - adamantWallet = Wallet.adamant(balance: account.balance) hideFreeTokensRow = account.balance > 0 } else { address = "" - adamantWallet = Wallet.adamant(balance: 0) hideFreeTokensRow = true } - if wallets != nil { - wallets![0] = adamantWallet - accountHeaderView.walletCollectionView.reloadItems(at: [IndexPath(row: 0, section: 0)]) - } else { - wallets = [adamantWallet, Wallet.ethereum] - accountHeaderView.walletCollectionView.reloadData() - } - - if let row: AlertLabelRow = form.rowBy(tag: Rows.balance.tag) { - row.value = adamantWallet.format(numberFormat: .full, includeCurrencySymbol: true) - row.updateCell() - } - if let row: LabelRow = form.rowBy(tag: Rows.freeTokens.tag) { row.evaluateHidden() } - accountHeaderView.walletCollectionView.selectItem(at: IndexPath(row: selectedWalletIndex, section: 0), animated: false, scrollPosition: .centeredHorizontally) accountHeaderView.addressButton.setTitle(address, for: .normal) } } -// MARK: - UICollectionViewDelegate, UICollectionViewDataSource -extension AccountViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { - func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - guard let wallets = wallets else { - return 0 - } - - return wallets.count - } - - func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: walletCellIdentifier, for: indexPath) as? WalletCollectionViewCell else { - fatalError("Can't dequeue wallet cell") - } - - guard let wallet = wallets?[indexPath.row] else { - fatalError("Wallets collectionView: Out of bounds row") - } - - if !cell.isInitialized { - cell.tintColor = UIColor.adamant.secondary - - cell.balanceLabel.textColor = UIColor.adamant.primary - cell.currencySymbolLabel.textColor = UIColor.adamant.primary - - cell.accessoryContainerView.accessoriesBackgroundColor = UIColor.adamant.primary - cell.accessoryContainerView.accessoriesBorderColor = UIColor.white - cell.accessoryContainerView.accessoriesBorderWidth = 2 - - if cell.accessoryContainerView.accessoriesContentInsets != accessoryContentInsets { - cell.accessoryContainerView.accessoriesContentInsets = accessoryContentInsets - } - - if cell.accessoryContainerView.accessoriesContainerInsets == accessoryContainerInsets { - cell.accessoryContainerView.accessoriesContainerInsets = accessoryContainerInsets - } - - cell.isInitialized = true - } - - cell.currencyImageView.image = wallet.currencyLogo - cell.balanceLabel.text = wallet.format(numberFormat: .compact, includeCurrencySymbol: false) - cell.currencySymbolLabel.text = wallet.currencySymbol - - if indexPath.row == 0, let count = transfersController?.fetchedObjects?.count, count > 0 { - let accessory = AccessoryType.label(text: String(count)) - cell.accessoryContainerView.setAccessory(accessory, at: AccessoryPosition.topRight) - } else { - cell.accessoryContainerView.setAccessory(nil, at: AccessoryPosition.topRight) - } - - cell.setSelected(indexPath.row == selectedWalletIndex, animated: false) - - if wallet.enabled { - cell.currencyImageView.alpha = 1 - cell.currencySymbolLabel.alpha = 1 - } else { - cell.currencyImageView.alpha = 0.3 - cell.currencySymbolLabel.alpha = 0.3 - } - - return cell - } - - func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { - guard let wallet = wallets?[indexPath.row] else { - return false - } - - return wallet.enabled - } - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - selectedWalletIndex = indexPath.row - - form.allSections.filter { $0.hidden != nil }.forEach { $0.evaluateHidden() } - - if let cell = collectionView.cellForItem(at: indexPath) as? WalletCollectionViewCell { - cell.setSelected(true, animated: true) - } - } - - func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { - if let cell = collectionView.cellForItem(at: indexPath) as? WalletCollectionViewCell { - cell.setSelected(false, animated: true) - } - } - - // Flow delegate - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - return CGSize(width: 110, height: 110) - } - - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { - return UIEdgeInsets.zero - } -} - - - // MARK: - AccountHeaderViewDelegate extension AccountViewController: AccountHeaderViewDelegate { func addressLabelTapped() { @@ -539,7 +418,8 @@ extension AccountViewController: AccountHeaderViewDelegate { tableView.deselectRow(at: indexPath, animated: true) } - dialogService.presentShareAlertFor(string: address, + let encodedAddress = AdamantUriTools.encode(request: AdamantUri.address(address: address, params: nil)) + dialogService.presentShareAlertFor(string: encodedAddress, types: [.copyToPasteboard, .share, .generateQr(sharingTip: address)], excludedActivityTypes: ShareContentType.address.excludedActivityTypes, animated: true, @@ -551,8 +431,6 @@ extension AccountViewController: AccountHeaderViewDelegate { // MARK: - NSFetchedResultsControllerDelegate extension AccountViewController: NSFetchedResultsControllerDelegate { func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { - accountHeaderView.walletCollectionView.reloadItems(at: [IndexPath(row: 0, section: 0)]) - if let row: AlertLabelRow = form.rowBy(tag: Rows.balance.tag), let alertLabel = row.cell.alertLabel, let count = controller.fetchedObjects?.count { if count > 0 { alertLabel.isHidden = false @@ -565,121 +443,61 @@ extension AccountViewController: NSFetchedResultsControllerDelegate { } -// MARK: - Tools -extension AccountViewController { - func createSectionFor(wallet: Wallet) -> Section { - let section = Section(wallet.sectionTitle) { - $0.tag = wallet.sectionTag +// MARK: - PagingViewControllerDataSource +extension AccountViewController: PagingViewControllerDataSource, PagingViewControllerDelegate { + func numberOfViewControllers(in pagingViewController: PagingViewController) -> Int { + return accountService.wallets.count + } + + func pagingViewController(_ pagingViewController: PagingViewController, viewControllerForIndex index: Int) -> UIViewController { + return accountService.wallets[index].walletViewController.viewController + } + + func pagingViewController(_ pagingViewController: PagingViewController, pagingItemForIndex index: Int) -> T { + let service = accountService.wallets[index] + + guard let wallet = service.wallet else { + return WalletPagingItem(index: index, currencySymbol: "", currencyImage: #imageLiteral(resourceName: "wallet_adm")) as! T } - switch wallet { - case .adamant: - // Balance - section <<< AlertLabelRow() { [weak self] in - $0.title = Rows.balance.localized - $0.tag = Rows.balance.tag - $0.value = wallet.format(numberFormat: .full, includeCurrencySymbol: true) - $0.cell.imageView?.image = Rows.balance.image - $0.cell.selectionStyle = .gray - - if let alertLabel = $0.cell.alertLabel { - alertLabel.backgroundColor = UIColor.adamant.primary - alertLabel.textColor = UIColor.white - alertLabel.clipsToBounds = true - alertLabel.textInsets = UIEdgeInsets(top: 1, left: 5, bottom: 1, right: 5) - - if let count = self?.transfersController?.fetchedObjects?.count, count > 0 { - alertLabel.text = String(count) - } else { - alertLabel.isHidden = true - } - } - }.cellUpdate({ (cell, _) in - cell.accessoryType = .disclosureIndicator - }).onCellSelection({ [weak self] (_, _) in - guard let vc = self?.router.get(scene: AdamantScene.Transactions.transactions), let nav = self?.navigationController else { - return - } - - nav.pushViewController(vc, animated: true) - }) - - // Send Tokens -// <<< LabelRow() { -// $0.title = Rows.sendTokens.localized -// $0.tag = Rows.sendTokens.tag -// $0.cell.imageView?.image = Rows.sendTokens.image -// $0.cell.selectionStyle = .gray -// }.cellUpdate({ (cell, _) in -// cell.accessoryType = .disclosureIndicator -// }) - - // Buy tokens - <<< LabelRow() { - $0.title = Rows.buyTokens.localized - $0.tag = Rows.buyTokens.tag - $0.cell.imageView?.image = Rows.buyTokens.image - $0.cell.selectionStyle = .gray - }.cellUpdate({ (cell, _) in - cell.accessoryType = .disclosureIndicator - }).onCellSelection({ [weak self] (_, _) in - let urlOpt: URL? - if let address = self?.accountService.account?.address { - urlOpt = URL(string: String.localizedStringWithFormat(String.adamantLocalized.account.buyTokensUrlFormat, address)) - } else { - urlOpt = nil - } - - guard let url = urlOpt else { - self?.dialogService.showError(withMessage: "Failed to build 'Buy tokens' url, report a bug", error: nil) - return - } - - let safari = SFSafariViewController(url: url) - safari.preferredControlTintColor = UIColor.adamant.primary - self?.present(safari, animated: true, completion: nil) - }) - - // Get free tokens - <<< LabelRow() { - $0.title = Rows.freeTokens.localized - $0.tag = Rows.freeTokens.tag - $0.cell.imageView?.image = Rows.freeTokens.image - $0.cell.selectionStyle = .gray - - $0.hidden = Condition.function([], { [weak self] _ -> Bool in - guard let hideFreeTokensRow = self?.hideFreeTokensRow else { - return true - } - - return hideFreeTokensRow - }) - }.cellUpdate({ (cell, _) in - cell.accessoryType = .disclosureIndicator - }).onCellSelection({ [weak self] (_, _) in - let raw: URL? - if let address = self?.accountService.account?.address { - raw = URL(string: String.localizedStringWithFormat(String.adamantLocalized.account.getFreeTokensUrlFormat, address)) - } else { - raw = URL(string: String.adamantLocalized.account.getFreeTokensUrlFormat) - } - - guard let url = raw else { - self?.dialogService.showError(withMessage: "Failed to build 'Free tokens' url, report a bug", error: nil) - return - } - - let safari = SFSafariViewController(url: url) - safari.preferredControlTintColor = UIColor.adamant.primary - self?.present(safari, animated: true, completion: nil) - }) - - case .ethereum: - section <<< LabelRow() { - $0.title = "Soon..." - } + let serviceType = type(of: service) + + let item = WalletPagingItem(index: index, currencySymbol: serviceType.currencySymbol, currencyImage: serviceType.currencyLogo) + item.balance = wallet.balance + item.notifications = wallet.notifications + + return item as! T + } + + func pagingViewController(_ pagingViewController: PagingViewController, didScrollToItem pagingItem: T, startingViewController: UIViewController?, destinationViewController: UIViewController, transitionSuccessful: Bool) { + guard transitionSuccessful, + let first = startingViewController as? WalletViewController, + let second = destinationViewController as? WalletViewController, + first.height != second.height else { + return } + + updateHeaderSize(with: second, animated: true) + } + + func updateHeaderSize(with walletViewController: WalletViewController, animated: Bool) { + guard case let .fixed(_, menuHeight) = pagingViewController.menuItemSize else { + return + } + + let pagingHeight = menuHeight + walletViewController.height - return section + var headerBounds = accountHeaderView.bounds + headerBounds.size.height = accountHeaderView.walletViewContainer.frame.origin.y + pagingHeight + + if animated { + UIView.animate(withDuration: 0.2) { [unowned self] in + self.accountHeaderView.bounds = headerBounds + self.tableView.tableHeaderView = self.accountHeaderView + } + } else { + accountHeaderView.frame = headerBounds + tableView.tableHeaderView = accountHeaderView + } } } diff --git a/Adamant/Stories/Account/TransferViewController.swift b/Adamant/Stories/Account/TransferViewController.swift deleted file mode 100644 index 3053b1903..000000000 --- a/Adamant/Stories/Account/TransferViewController.swift +++ /dev/null @@ -1,331 +0,0 @@ -// -// TransferViewController.swift -// Adamant -// -// Created by Anokhov Pavel on 09.01.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import UIKit -import Eureka - - -// MARK: - Localization -extension String.adamantLocalized { - struct transfer { - static let addressPlaceholder = NSLocalizedString("TransferScene.Recipient.Placeholder", comment: "Transfer: recipient address placeholder") - static let amountPlaceholder = NSLocalizedString("TransferScene.Amount.Placeholder", comment: "Transfer: transfer amount placeholder") - - static let addressValidationError = NSLocalizedString("TransferScene.Error.InvalidAddress", comment: "Transfer: Address validation error") - static let amountZeroError = NSLocalizedString("TransferScene.Error.TooLittleMoney", comment: "Transfer: Amount is zero, or even negative notification") - static let amountTooHigh = NSLocalizedString("TransferScene.Error.NotEnoughtMoney", comment: "Transfer: Amount is hiegher that user's total money notification") - static let accountNotFound = NSLocalizedString("TransferScene.Error.AddressNotFound", comment: "Transfer: Address not found error") - - static let transferProcessingMessage = NSLocalizedString("TransferScene.SendingFundsProgress", comment: "Transfer: Processing message") - static let transferSuccess = NSLocalizedString("TransferScene.TransferSuccessMessage", comment: "Transfer: Tokens transfered successfully message") - - private init() { } - } -} - -fileprivate extension String.adamantLocalized.alert { - static let confirmSendMessageFormat = NSLocalizedString("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 let send = NSLocalizedString("TransferScene.Send", comment: "Transfer: Confirm transfer alert: Send tokens button") -} - - -// MARK: - -class TransferViewController: FormViewController { - - // MARK: - Rows - - private enum Row { - case balance - case amount - case maxToTransfer - case address - case fee - case total - case sendButton - - var tag: String { - switch self { - case .balance: return "balance" - case .amount: return "amount" - case .maxToTransfer: return "max" - case .address: return "recipient" - case .fee: return "fee" - case .total: return "total" - case .sendButton: return "send" - } - } - - var localized: String { - switch self { - case .balance: return NSLocalizedString("TransferScene.Row.Balance", comment: "Transfer: logged user balance.") - case .amount: return NSLocalizedString("TransferScene.Row.Amount", comment: "Transfer: amount of adamant to transfer.") - case .maxToTransfer: return NSLocalizedString("TransferScene.Row.MaxToTransfer", comment: "Transfer: maximum amount to transfer: available account money substracting transfer fee.") - case .address: return NSLocalizedString("TransferScene.Row.Recipient", comment: "Transfer: recipient address") - case .fee: return NSLocalizedString("TransferScene.Row.TransactionFee", comment: "Transfer: transfer fee") - case .total: return NSLocalizedString("TransferScene.Row.Total", comment: "Transfer: total amount of transaction: money to transfer adding fee") - case .sendButton: return NSLocalizedString("TransferScene.Row.Send", comment: "Transfer: Send button") - } - } - } - - private enum Sections { - case wallet - case transferInfo - - var localized: String { - switch self { - case .wallet: return NSLocalizedString("TransferScene.Section.YourWallet", comment: "Transfer: 'Your wallet' section") - case .transferInfo: return NSLocalizedString("TransferScene.Section.TransferInfo", comment: "Transfer: 'Transfer info' section") - } - } - } - - - // MARK: - Dependencies - - var apiService: ApiService! - var accountService: AccountService! - var dialogService: DialogService! - - private(set) var maxToTransfer: Double = 0.0 - - - // MARK: - Properties - - let defaultFee = 0.5 - var account: Account? - - private(set) var totalAmount: Double? = nil - - - // MARK: - IBOutlets - - @IBOutlet weak var sendButton: UIBarButtonItem! - - - // MARK: - Lifecycle - - override func viewDidLoad() { - super.viewDidLoad() - - // MARK: - Wallet section - if let account = account { - sendButton.isEnabled = maxToTransfer > 0.0 - let balance = (account.balance as NSDecimalNumber).doubleValue - maxToTransfer = balance - defaultFee > 0 ? balance - defaultFee : 0.0 - - form +++ Section(Sections.wallet.localized) - <<< DecimalRow() { - $0.title = Row.balance.localized - $0.value = balance - $0.tag = Row.balance.tag - $0.disabled = true - $0.formatter = AdamantUtilities.currencyFormatter - } - <<< DecimalRow() { - $0.title = Row.maxToTransfer.localized - $0.value = maxToTransfer - $0.tag = Row.maxToTransfer.tag - $0.disabled = true - $0.formatter = AdamantUtilities.currencyFormatter - } - } else { - sendButton.isEnabled = false - } - - // MARK: - Transfer section - form +++ Section(Sections.transferInfo.localized) - - <<< TextRow() { - $0.title = Row.address.localized - $0.placeholder = String.adamantLocalized.transfer.addressPlaceholder - $0.tag = Row.address.tag - $0.add(rule: RuleClosure(closure: { value -> ValidationError? in - guard let value = value?.uppercased() else { - return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) - } - - switch AdamantUtilities.validateAdamantAddress(address: value) { - case .valid: - return nil - - case .system, .invalid: - return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) - } - })) - $0.validationOptions = .validatesOnBlur - }.cellUpdate({ (cell, row) in - cell.titleLabel?.textColor = row.isValid ? .black : .red - }) - <<< DecimalRow() { - $0.title = Row.amount.localized - $0.placeholder = String.adamantLocalized.transfer.amountPlaceholder - $0.tag = Row.amount.tag - $0.formatter = AdamantUtilities.currencyFormatter -// $0.add(rule: RuleSmallerOrEqualThan(max: maxToTransfer)) -// $0.validationOptions = .validatesOnChange - }.onChange(amountChanged) - <<< DecimalRow() { - $0.title = Row.fee.localized - $0.value = defaultFee - $0.tag = Row.fee.tag - $0.disabled = true - $0.formatter = AdamantUtilities.currencyFormatter - } - <<< DecimalRow() { - $0.title = Row.total.localized - $0.value = nil - $0.tag = Row.total.tag - $0.disabled = true - $0.formatter = AdamantUtilities.currencyFormatter - } - <<< ButtonRow() { - $0.title = Row.sendButton.localized - $0.tag = Row.sendButton.tag - $0.disabled = Condition.function([Row.total.tag], { [weak self] form -> Bool in - guard let row: DecimalRow = form.rowBy(tag: Row.amount.tag), - let amount = row.value, - amount > 0, - AdamantUtilities.validateAmount(amount: Decimal(amount)), - let maxToTransfer = self?.maxToTransfer else { - return true - } - - return amount > maxToTransfer - }) - }.onCellSelection({ [weak self] (cell, row) in - self?.sendFunds(row) - }) - - - // MARK: - UI - navigationAccessoryView.tintColor = UIColor.adamant.primary - - let button: ButtonRow? = form.rowBy(tag: Row.sendButton.tag) - button?.evaluateDisabled() - } - - - // MARK: - Form Events - - private func amountChanged(row: DecimalRow) { - guard let totalRow: DecimalRow = form.rowBy(tag: Row.total.tag), let account = account else { - return - } - - guard let amount = row.value else { - totalAmount = nil - sendButton.isEnabled = false - row.cell.titleLabel?.textColor = .black - return - } - - totalAmount = amount + defaultFee - totalRow.evaluateDisabled() - - totalRow.value = totalAmount - totalRow.evaluateDisabled() - - if let totalAmount = totalAmount { - if amount > 0, AdamantUtilities.validateAmount(amount: Decimal(amount)), - totalAmount > 0.0 && totalAmount < (account.balance as NSDecimalNumber).doubleValue { - sendButton.isEnabled = true - row.cell.titleLabel?.textColor = .black - } else { - sendButton.isEnabled = false - row.cell.titleLabel?.textColor = .red - } - } else { - sendButton.isEnabled = false - row.cell.titleLabel?.textColor = .black - } - } - - - // MARK: - IBActions - - @IBAction func sendFunds(_ sender: Any) { - guard let dialogService = self.dialogService, let apiService = self.apiService else { - fatalError("Dependecies fatal error") - } - - guard let account = accountService.account, let keypair = accountService.keypair else { - return - } - - guard let recipientRow = form.rowBy(tag: Row.address.tag) as? TextRow, - let recipient = recipientRow.value, - let amountRow = form.rowBy(tag: Row.amount.tag) as? DecimalRow, - let raw = amountRow.value else { - return - } - - let amount = Decimal(raw) - - guard AdamantUtilities.validateAmount(amount: amount) else { - dialogService.showWarning(withMessage: String.adamantLocalized.transfer.amountZeroError) - return - } - - switch AdamantUtilities.validateAdamantAddress(address: recipient) { - case .valid: - break - - case .system, .invalid: - dialogService.showWarning(withMessage: String.adamantLocalized.transfer.addressValidationError) - return - } - - guard amount <= Decimal(maxToTransfer) else { - dialogService.showWarning(withMessage: String.adamantLocalized.transfer.amountTooHigh) - return - } - - let alert = UIAlertController(title: String.localizedStringWithFormat(String.adamantLocalized.alert.confirmSendMessageFormat, "\(amount) \(AdamantUtilities.currencyCode)", recipient), message: "You can't undo this action.", preferredStyle: .alert) - let cancelAction = UIAlertAction(title: String.adamantLocalized.alert.cancel , style: .cancel, handler: nil) - let sendAction = UIAlertAction(title: String.adamantLocalized.alert.send, style: .default, handler: { _ in - dialogService.showProgress(withMessage: String.adamantLocalized.transfer.transferProcessingMessage, userInteractionEnable: false) - - // Check if address is valid - apiService.getPublicKey(byAddress: recipient) { result in - switch result { - case .success(_): - apiService.transferFunds(sender: account.address, recipient: recipient, amount: amount, keypair: keypair) { [weak self] result in - switch result { - case .success(_): - DispatchQueue.main.async { - dialogService.showSuccess(withMessage: String.adamantLocalized.transfer.transferSuccess) - - self?.accountService.update() - - if let nav = self?.navigationController { - nav.popViewController(animated: true) - } else { - self?.dismiss(animated: true, completion: nil) - } - } - - case .failure(let error): - dialogService.showRichError(error: error) - } - - } - - - case .failure(let error): - dialogService.showRichError(error: error) - } - } - }) - - alert.addAction(cancelAction) - alert.addAction(sendAction) - - present(alert, animated: true, completion: nil) - } -} diff --git a/Adamant/Stories/Account/WalletCollectionViewCell.swift b/Adamant/Stories/Account/WalletCollectionViewCell.swift index fe4fb0c6b..6dd9ca2f3 100644 --- a/Adamant/Stories/Account/WalletCollectionViewCell.swift +++ b/Adamant/Stories/Account/WalletCollectionViewCell.swift @@ -8,35 +8,34 @@ import UIKit import FreakingSimpleRoundImageView +import Parchment -class WalletCollectionViewCell: UICollectionViewCell { +class WalletCollectionViewCell: PagingCell { @IBOutlet weak var currencyImageView: UIImageView! @IBOutlet weak var balanceLabel: UILabel! @IBOutlet weak var currencySymbolLabel: UILabel! - @IBOutlet weak var markerView: UIView! - @IBOutlet weak var markerWidthConstraint: NSLayoutConstraint! @IBOutlet weak var accessoryContainerView: AccessoryContainerView! - var activeMarkerMultiplier: CGFloat = 0.68 - var markerAnimationDuration: TimeInterval = 0.15 - - override var tintColor: UIColor! { - didSet { - markerView.backgroundColor = tintColor + override func setPagingItem(_ pagingItem: PagingItem, selected: Bool, options: PagingOptions) { + guard let item = pagingItem as? WalletPagingItem else { + return } - } - - func setSelected(_ selected: Bool, animated: Bool) { - let width = selected ? frame.width * activeMarkerMultiplier : 0.0 - if animated { - UIView.animate(withDuration: markerAnimationDuration) { - self.markerWidthConstraint.constant = width - self.layoutIfNeeded() - } + + currencyImageView.image = item.currencyImage + currencySymbolLabel.text = item.currencySymbol + + if item.balance < 1 { + balanceLabel.text = AdamantBalanceFormat.compact.format(item.balance) } else { - markerWidthConstraint.constant = width + balanceLabel.text = AdamantBalanceFormat.short.format(item.balance) + } + + accessoryContainerView.accessoriesBackgroundColor = options.indicatorColor + + if item.notifications > 0 { + accessoryContainerView.setAccessory(AccessoryType.label(text: String(item.notifications)), at: .topRight) + } else { + accessoryContainerView.setAccessory(nil, at: .topRight) } } - - var isInitialized = false } diff --git a/Adamant/Stories/Account/WalletCollectionViewCell.xib b/Adamant/Stories/Account/WalletCollectionViewCell.xib index 21047983e..cd37aefef 100644 --- a/Adamant/Stories/Account/WalletCollectionViewCell.xib +++ b/Adamant/Stories/Account/WalletCollectionViewCell.xib @@ -14,10 +14,10 @@ - + - + @@ -36,16 +36,8 @@ - - - - - - - - - diff --git a/Adamant/Stories/Account/WalletPagingItem.swift b/Adamant/Stories/Account/WalletPagingItem.swift new file mode 100644 index 000000000..d5a669ad1 --- /dev/null +++ b/Adamant/Stories/Account/WalletPagingItem.swift @@ -0,0 +1,39 @@ +// +// WalletPagingItem.swift +// Adamant +// +// Created by Anokhov Pavel on 10.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import Parchment + +class WalletPagingItem: PagingItem, Hashable, Comparable { + let index: Int + let currencySymbol: String + let currencyImage: UIImage + + var balance: Decimal = 0 + var notifications: Int = 0 + + init(index: Int, currencySymbol symbol: String, currencyImage image: UIImage) { + self.index = index + currencySymbol = symbol + currencyImage = image + } + + // MARK: Hashable, Comparable + var hashValue: Int { + return index.hashValue &+ currencySymbol.hashValue + } + + static func < (lhs: WalletPagingItem, rhs: WalletPagingItem) -> Bool { + return lhs.index < rhs.index + } + + static func == (lhs: WalletPagingItem, rhs: WalletPagingItem) -> Bool { + return lhs.index == rhs.index && + lhs.currencySymbol == rhs.currencySymbol + } +} diff --git a/Adamant/Stories/Chats/ChatCell.swift b/Adamant/Stories/Chats/ChatCell.swift new file mode 100644 index 000000000..f6465e80e --- /dev/null +++ b/Adamant/Stories/Chats/ChatCell.swift @@ -0,0 +1,15 @@ +// +// ChatCell.swift +// Adamant +// +// Created by Anokhov Pavel on 27.09.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import MessageKit + +protocol ChatCell: class { +// var bubbleStyle: MessageStyle { get set } + var bubbleBackgroundColor: UIColor? { get set } +} diff --git a/Adamant/Stories/Chats/ChatListViewController.swift b/Adamant/Stories/Chats/ChatListViewController.swift index f366e2543..5b8abffba 100644 --- a/Adamant/Stories/Chats/ChatListViewController.swift +++ b/Adamant/Stories/Chats/ChatListViewController.swift @@ -29,6 +29,9 @@ class ChatListViewController: UIViewController { var router: Router! var notificationsService: NotificationsService! var dialogService: DialogService! + var addressBook: AddressBookService! + + var richMessageProviders = [String:RichMessageProvider]() // MARK: IBOutlet @IBOutlet weak var tableView: UITableView! @@ -46,7 +49,7 @@ class ChatListViewController: UIViewController { let refreshControl = UIRefreshControl() refreshControl.addTarget(self, action: #selector(self.handleRefresh(_:)), - for: UIControlEvents.valueChanged) + for: UIControl.Event.valueChanged) refreshControl.tintColor = UIColor.adamant.primary return refreshControl @@ -263,26 +266,12 @@ extension ChatListViewController { } cell.hasUnreadMessages = chatroom.hasUnreadMessages - - switch chatroom.lastTransaction { - case let message as MessageTransaction: - guard let text = message.message else { - cell.lastMessageLabel.text = nil - break - } - - if message.isOutgoing { - cell.lastMessageLabel.text = String.localizedStringWithFormat(String.adamantLocalized.chatList.sentMessagePrefix, text) - } else { - cell.lastMessageLabel.text = text - } - - case let transfer as TransferTransaction: - cell.lastMessageLabel.text = formatTransferPreview(transfer) - - default: - cell.lastMessageLabel.text = nil - } + + if let lastTransaction = chatroom.lastTransaction { + cell.lastMessageLabel.text = shortDescription(for: lastTransaction) + } else { + cell.lastMessageLabel.text = nil + } if let date = chatroom.updatedAt as Date?, date != Date.adamantNullDate { cell.dateLabel.text = date.humanizedDay() @@ -425,6 +414,11 @@ extension ChatListViewController: ChatViewControllerDelegate { // MARK: - Working with in-app notifications extension ChatListViewController { private func showNotification(for transaction: ChatTransaction) { + // MARK: 0. Do not show notifications for initial sync + guard 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, @@ -432,30 +426,9 @@ extension ChatListViewController { return } - - // MARK: 2. Check transaction type. Do not show notifications for initial sync - let text: String - switch transaction { - case let message as MessageTransaction: - guard chatsProvider.isInitiallySynced, let t = message.message else { - return - } - - text = t - - case let transfer as TransferTransaction: - guard transfersProvider.isInitiallySynced, let t = formatTransferPreview(transfer) else { - return - } - - text = t - - default: - return - } - - // MARK: 3. Prepare notification - let title = partner.name ?? partner.address + // MARK: 2. Prepare notification + let title = partner.name ?? partner.address + let text = shortDescription(for: transaction) let image: UIImage if let ava = partner.avatar, let img = UIImage(named: ava) { @@ -498,17 +471,123 @@ extension ChatListViewController { present(vc, animated: true) } } - - private func formatTransferPreview(_ transfer: TransferTransaction) -> String? { - guard let balance = transfer.amount else { + + private func shortDescription(for transaction: ChatTransaction) -> String? { + switch transaction { + case let message as MessageTransaction: + return message.message + + case let transfer as TransferTransaction: + if let admService = richMessageProviders[AdmWalletService.richMessageType] as? AdmWalletService { + return admService.shortDescription(for: transfer) + } else { + return nil + } + + case let richMessage as RichMessageTransaction: + if let type = richMessage.richType, let provider = richMessageProviders[type] { + return provider.shortDescription(for: richMessage) + } else { + return richMessage.serializedMessage() + } + + default: + return nil + } + } +} + + +// MARK: - Swipe actions +extension ChatListViewController { + @available(iOS 11.0, *) + func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + guard let chatroom = chatsController?.object(at: indexPath) else { return nil } - if transfer.isOutgoing { - return String.localizedStringWithFormat(String.adamantLocalized.chatList.sentMessagePrefix, " ⬅️ \(AdamantUtilities.format(balance: balance))") + let actions: [UIContextualAction] + + // More + let more = UIContextualAction(style: .normal, title: nil) { [weak self] (_, _, completionHandler: (Bool) -> Void) in + guard let partner = chatroom.partner, let address = partner.address else { + completionHandler(false) + return + } + + let encodedAddress = AdamantUriTools.encode(request: AdamantUri.address(address: address, params: nil)) + + if partner.isSystem { + self?.dialogService.presentShareAlertFor(string: encodedAddress, + types: [.copyToPasteboard, .share, .generateQr(sharingTip: address)], + excludedActivityTypes: ShareContentType.address.excludedActivityTypes, + animated: true, + completion: nil) + } else { + let share = UIAlertAction(title: ShareType.share.localized, style: .default) { [weak self] action in + self?.dialogService.presentShareAlertFor(string: encodedAddress, + types: [.copyToPasteboard, .share, .generateQr(sharingTip: address)], + excludedActivityTypes: ShareContentType.address.excludedActivityTypes, + animated: true, + completion: nil) + } + + let rename = UIAlertAction(title: String.adamantLocalized.chat.rename, style: .default) { [weak self] action in + let alert = UIAlertController(title: String(format: String.adamantLocalized.chat.actionsBody, address), message: nil, preferredStyle: .alert) + + alert.addTextField { (textField) in + textField.placeholder = String.adamantLocalized.chat.name + textField.autocapitalizationType = .words + + if let name = partner.name { + textField.text = name + } + } + + alert.addAction(UIAlertAction(title: String.adamantLocalized.chat.rename, style: .default) { [weak alert] (_) in + if let textField = alert?.textFields?.first, let newName = textField.text { + self?.addressBook.set(name: newName, for: address) + } + }) + + alert.addAction(UIAlertAction(title: String.adamantLocalized.alert.cancel, style: .cancel, handler: nil)) + + self?.present(alert, animated: true, completion: nil) + } + + let cancel = UIAlertAction(title: String.adamantLocalized.alert.cancel, style: .cancel, handler: nil) + + self?.dialogService.showAlert(title: nil, message: nil, style: UIAlertController.Style.actionSheet, actions: [share, rename, cancel]) + } + + completionHandler(true) + } + + more.image = #imageLiteral(resourceName: "swipe_more") + more.backgroundColor = UIColor.adamant.primary + + // Mark as read + if chatroom.hasUnreadMessages { + let markAsRead = UIContextualAction(style: .normal, title: nil) { [weak self] (_, _, completionHandler: (Bool) -> Void) in + guard let chatroom = self?.chatsController?.object(at: indexPath) else { + completionHandler(false) + return + } + + chatroom.markAsReaded() + try? chatroom.managedObjectContext?.save() + completionHandler(true) + } + + markAsRead.image = #imageLiteral(resourceName: "swipe_mark-as-read") + markAsRead.backgroundColor = UIColor.adamant.primary + + actions = [markAsRead, more] } else { - return "➡️ \(AdamantUtilities.format(balance: balance))" + actions = [more] } + + return UISwipeActionsConfiguration(actions: actions) } } diff --git a/Adamant/Stories/Chats/ChatViewController+MessageKit.swift b/Adamant/Stories/Chats/ChatViewController+MessageKit.swift index f615dc315..426e36f21 100644 --- a/Adamant/Stories/Chats/ChatViewController+MessageKit.swift +++ b/Adamant/Stories/Chats/ChatViewController+MessageKit.swift @@ -8,9 +8,23 @@ import Foundation import MessageKit +import MessageInputBar import SafariServices import Haring + +// MARK: - Tools +extension ChatViewController { + private func getRichMessageType(of message: MessageType) -> String? { + guard case .custom(let raw) = message.kind, let transfer = raw as? RichMessageTransfer else { + return nil + } + + return transfer.type + } +} + + // MARK: - MessagesDataSource extension ChatViewController: MessagesDataSource { func currentSender() -> Sender { @@ -38,7 +52,7 @@ extension ChatViewController: MessagesDataSource { func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { if self.shouldDisplayHeader(for: message, at: indexPath, in: self.messagesCollectionView) { - return NSAttributedString(string: message.sentDate.humanizedDay(), attributes: [NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 10), NSAttributedStringKey.foregroundColor: UIColor.gray]) + return NSAttributedString(string: message.sentDate.humanizedDay(), attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10), NSAttributedString.Key.foregroundColor: UIColor.gray]) } return nil } @@ -51,10 +65,10 @@ extension ChatViewController: MessagesDataSource { switch transaction.statusEnum { case .failed: - return NSAttributedString(string: String.adamantLocalized.chat.failToSend, attributes: [NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 10), NSAttributedStringKey.foregroundColor: UIColor.darkText]) + return NSAttributedString(string: String.adamantLocalized.chat.failToSend, attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10), NSAttributedString.Key.foregroundColor: UIColor.darkText]) case .pending: - return NSAttributedString(string: String.adamantLocalized.chat.pending, attributes: [NSAttributedStringKey.font: UIFont.boldSystemFont(ofSize: 10), NSAttributedStringKey.foregroundColor: UIColor.darkText]) + return NSAttributedString(string: String.adamantLocalized.chat.pending, attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10), NSAttributedString.Key.foregroundColor: UIColor.darkText]) case .delivered: return nil @@ -95,8 +109,52 @@ extension ChatViewController: MessagesDataSource { } } - return NSAttributedString(string: humanizedTime.string, attributes: [NSAttributedStringKey.font: UIFont.preferredFont(forTextStyle: .caption2)]) + return NSAttributedString(string: humanizedTime.string, attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption2)]) } + + func customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell { + guard let type = getRichMessageType(of: message), let provider = richMessageProviders[type] else { + fatalError("Tried to render wrong messagetype: \(message.kind)") + } + + let fromCurrent = isFromCurrentSender(message: message) + + let cell = provider.cell(for: message, isFromCurrentSender: fromCurrent, at: indexPath, in: messagesCollectionView) + + if let chatCell = cell as? ChatCell { +// let corner: MessageStyle.TailCorner = fromCurrent ? .bottomRight : .bottomLeft +// chatCell.bubbleStyle = .bubbleTail(corner, .curved) + + let bgColor: UIColor + if fromCurrent { + if let transaction = message as? ChatTransaction { + switch transaction.statusEnum { + case .failed: bgColor = UIColor.adamant.failChatBackground + case .pending: bgColor = UIColor.adamant.pendingChatBackground + case .delivered: bgColor = UIColor.adamant.chatSenderBackground + } + } else { + bgColor = UIColor.adamant.chatSenderBackground + } + } else { + bgColor = UIColor.adamant.chatRecipientBackground + } + + chatCell.bubbleBackgroundColor = bgColor + } + + if let customCell = cell as? TapRecognizerCustomCell { + customCell.delegate = self + } + + if let richTransaction = message as? RichMessageTransaction, + (richTransaction.transactionStatus == nil || richTransaction.transactionStatus == .notInitiated), + let updater = provider as? RichMessageProviderWithStatusCheck { + updateStatus(for: richTransaction, provider: updater) + } + + return cell + } } @@ -185,26 +243,12 @@ extension ChatViewController: MessageCellDelegate { } switch message { - case let transfer as TransferTransaction: - // MARK: Show transfer details - guard let vc = router.get(scene: AdamantScene.Transactions.transactionDetails) as? TransactionDetailsViewController else { - fatalError("Can't get TransactionDetails scene") - } - - vc.transaction = transfer - vc.showToChatRow = false - - if let nav = navigationController { - nav.pushViewController(vc, animated: true) - } else { - present(vc, animated: true, completion: nil) - } - - case let message as MessageTransaction: - // MARK: Show Retry/Cancel action sheet - guard message.messageStatus == .failed else { - break - } + // MARK: Show Retry/Cancel action sheet + case let message as MessageTransaction: + // Only for failed messages + guard message.messageStatus == .failed else { + break + } let retry = UIAlertAction(title: String.adamantLocalized.alert.retry, style: .default, handler: { [weak self] action in self?.chatsProvider.retrySendMessage(message) { result in @@ -237,8 +281,27 @@ extension ChatViewController: MessageCellDelegate { } }) - dialogService.showSystemActionSheet(title: String.adamantLocalized.alert.retryOrDeleteTitle, message: String.adamantLocalized.alert.retryOrDeleteBody, actions: [retry, cancelMessage]) + let cancel = UIAlertAction(title: String.adamantLocalized.alert.cancel, style: .cancel) + dialogService.showAlert(title: String.adamantLocalized.alert.retryOrDeleteTitle, message: String.adamantLocalized.alert.retryOrDeleteBody, style: .actionSheet, actions: [retry, cancelMessage, cancel]) + + + // MARK: Show ADM transfer details + case let transfer as TransferTransaction: + guard let provider = richMessageProviders[AdmWalletService.richMessageType] as? AdmWalletService else { + return + } + + provider.richMessageTapped(for: transfer, at: indexPath, in: self) + + // MARK: Pass event to rich message provider + case let richMessage as RichMessageTransaction: + guard let type = richMessage.richType, let provider = richMessageProviders[type] else { + break + } + + provider.richMessageTapped(for: richMessage, at: indexPath, in: self) + default: break } @@ -251,6 +314,35 @@ extension ChatViewController: MessageCellDelegate { } } +// MARK: - TransferCollectionViewCellDelegate +extension ChatViewController: CustomCellDelegate { + func didTapCustomCell(_ cell: TapRecognizerCustomCell) { + guard let c = cell as? UICollectionViewCell, + let indexPath = messagesCollectionView.indexPath(for: c), + let transaction = chatController?.object(at: IndexPath(row: indexPath.section, section: 0)) else { + return + } + + switch transaction { + case let transfer as TransferTransaction: + guard let provider = richMessageProviders[AdmWalletService.richMessageType] as? AdmWalletService else { + break + } + + provider.richMessageTapped(for: transfer, at: indexPath, in: self) + + case let richTransaction as RichMessageTransaction: + guard let type = richTransaction.richType, let provider = richMessageProviders[type] else { + break + } + + provider.richMessageTapped(for: richTransaction, at: indexPath, in: self) + + default: + return + } + } +} // MARK: - MessagesLayoutDelegate extension ChatViewController: MessagesLayoutDelegate { @@ -304,6 +396,22 @@ extension ChatViewController: MessagesLayoutDelegate { return 16 } } + + func customCellSizeCalculator(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CellSizeCalculator { + guard let type = getRichMessageType(of: message) else { + return (messagesCollectionView.collectionViewLayout as! MessagesCollectionViewFlowLayout).textMessageSizeCalculator + } + + if let calculator = cellCalculators[type] { + return calculator + } else if let provider = richMessageProviders[type] { + let calculator = provider.cellSizeCalculator(for: messagesCollectionView.collectionViewLayout as! MessagesCollectionViewFlowLayout) + cellCalculators[type] = calculator + return calculator + } else { + return (messagesCollectionView.collectionViewLayout as! MessagesCollectionViewFlowLayout).textMessageSizeCalculator + } + } } @@ -384,12 +492,12 @@ extension MessageTransaction: MessageType { return MessageKind.text("") } - if isMarkdown { - let parser = MarkdownParser(font: UIFont.adamantChatDefault) - return MessageKind.attributedText(parser.parse(message)) - } else { - return MessageKind.text(message) - } + if isMarkdown { + let parser = MarkdownParser(font: UIFont.adamantChatDefault) + return MessageKind.attributedText(parser.parse(message)) + } else { + return MessageKind.text(message) + } } public var messageStatus: MessageStatus { @@ -397,6 +505,22 @@ extension MessageTransaction: MessageType { } } +// MARK: - RichMessageTransaction +extension RichMessageTransaction: MessageType { + public var sender: Sender { + let id = self.senderId! + return Sender(id: id, displayName: id) + } + + public var messageId: String { + return self.transactionId! + } + + public var sentDate: Date { + return self.date! as Date + } +} + // MARK: TransferTransaction extension TransferTransaction: MessageType { public var sender: Sender { @@ -413,6 +537,16 @@ extension TransferTransaction: MessageType { } public var kind: MessageKind { - return MessageKind.attributedText(AdamantFormattingTools.formatTransferTransaction(self)) + let amountString: String + if let a = amount as Decimal? { + amountString = AdamantBalanceFormat.full.format(a) + } else { + amountString = "0" + } + + return MessageKind.custom(RichMessageTransfer(type: AdmWalletService.richMessageType, + amount: amountString, + hash: "", + comments: "")) } } diff --git a/Adamant/Stories/Chats/ChatViewController.swift b/Adamant/Stories/Chats/ChatViewController.swift index 740be8138..2f26d9a11 100644 --- a/Adamant/Stories/Chats/ChatViewController.swift +++ b/Adamant/Stories/Chats/ChatViewController.swift @@ -8,7 +8,10 @@ import UIKit import MessageKit +import MessageInputBar import CoreData +import SafariServices +import ProcedureKit // MARK: - Localization extension String.adamantLocalized { @@ -42,10 +45,11 @@ class ChatViewController: MessagesViewController { var dialogService: DialogService! var router: Router! var addressBookService: AddressBookService! + var stack: CoreDataStack! // MARK: Properties weak var delegate: ChatViewControllerDelegate? - var account: Account? + var account: AdamantAccount? var chatroom: Chatroom? var dateFormatter: DateFormatter { let formatter = DateFormatter() @@ -55,8 +59,17 @@ class ChatViewController: MessagesViewController { } private(set) var chatController: NSFetchedResultsController? - private var controllerChanges: [NSFetchedResultsChangeType:[(indexPath: IndexPath?, newIndexPath: IndexPath?)]] = [:] + + // Batch changes + private struct ControllerChange { + let type: NSFetchedResultsChangeType + let indexPath: IndexPath? + let newIndexPath: IndexPath? + } + + private var controllerChanges: [ControllerChange] = [] + // Cell update timing var cellUpdateTimers: [Timer] = [Timer]() var cellsUpdating: [IndexPath] = [IndexPath]() @@ -64,14 +77,49 @@ class ChatViewController: MessagesViewController { private var isFirstLayout = true + // Content insets are broken after modal view dissapears + private var fixKeyboardInsets = false + + // MARK: Rich Messages + var richMessageProviders = [String:RichMessageProvider]() + var cellCalculators = [String:CellSizeCalculator]() + // MARK: Fee label private var feeIsVisible: Bool = false private var feeTimer: Timer? private var feeLabel: InputBarButtonItem? private var prevFee: Decimal = 0 + // MARK: Attachment button + lazy var attachmentButton: InputBarButtonItem = { + return InputBarButtonItem() + .configure { + $0.setSize(CGSize(width: 36, height: 36), animated: false) + $0.image = #imageLiteral(resourceName: "attachment") + }.onTouchUpInside { [weak self] _ in + guard let vc = self?.router.get(scene: AdamantScene.Chats.complexTransfer) as? ComplexTransferViewController else { + return + } + + vc.partner = self?.chatroom?.partner + vc.transferDelegate = self + + let navigator = UINavigationController(rootViewController: vc) + self?.present(navigator, animated: true, completion: nil) + } + }() - // MARK: Lifecycle + // MARK: RichTransaction status updates + private lazy var richStatusDispatchQueue = DispatchQueue(label: "com.adamant.chat.status-update.dispatch-queue", qos: .utility, attributes: [.concurrent]) + private lazy var richStatusOperationQueue: ProcedureQueue = { + let queue = ProcedureQueue() + queue.name = "com.adamant.chat.status-update.operation-queue" + queue.underlyingQueue = richStatusDispatchQueue + queue.maxConcurrentOperationCount = 2 + return queue + }() + + // MARK: - Lifecycle override func viewDidLoad() { super.viewDidLoad() navigationItem.rightBarButtonItem = UIBarButtonItem(title: "•••", style: .plain, target: self, action: #selector(properties)) @@ -130,6 +178,7 @@ class ChatViewController: MessagesViewController { messageInputBar.textViewPadding.right = -buttonWidth messageInputBar.setRightStackViewWidthConstant(to: buttonWidth, animated: false) + messageInputBar.setLeftStackViewWidthConstant(to: 36, animated: false) // Make feeLabel let feeLabel = InputBarButtonItem() @@ -141,6 +190,7 @@ class ChatViewController: MessagesViewController { // Setup stack views messageInputBar.setStackViewItems([messageInputBar.sendButton], forStack: .right, animated: false) messageInputBar.setStackViewItems([feeLabel, .flexibleSpace], forStack: .bottom, animated: false) + messageInputBar.setStackViewItems([attachmentButton], forStack: .left, animated: false) messageInputBar.sendButton.configure { $0.layer.cornerRadius = size*2 @@ -149,7 +199,7 @@ class ChatViewController: MessagesViewController { $0.setSize(CGSize(width: buttonWidth, height: buttonHeight), animated: false) $0.title = nil $0.image = #imageLiteral(resourceName: "Arrow") - $0.setImage(#imageLiteral(resourceName: "Arrow_innactive"), for: UIControlState.disabled) + $0.setImage(#imageLiteral(resourceName: "Arrow_innactive"), for: UIControl.State.disabled) } if let delegate = delegate, let address = chatroom.partner?.address, let message = delegate.getPreservedMessageFor(address: address, thenRemoveIt: true) { @@ -163,8 +213,8 @@ class ChatViewController: MessagesViewController { messageInputBar.inputTextView.backgroundColor = UIColor.adamant.chatSenderBackground messageInputBar.inputTextView.isEditable = false messageInputBar.sendButton.isEnabled = false - } - + attachmentButton.isEnabled = false + } // MARK: 4. Data let controller = chatsProvider.getChatController(for: chatroom) @@ -176,6 +226,53 @@ class ChatViewController: MessagesViewController { } catch { print("There was an error performing fetch: \(error)") } + + // MARK: 4.1 Rich messages + if let fetched = controller.fetchedObjects { + for case let rich as RichMessageTransaction in fetched { + rich.kind = messageKind(for: rich) + } + } + + // MARK: 5. Notifications + // Fixing content insets after modal window + NotificationCenter.default.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: OperationQueue.main) { [weak self] notification in + guard let fixIt = self?.fixKeyboardInsets, fixIt else { + return + } + + guard let frame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect, + let scrollView = self?.messagesCollectionView else { + return + } + + var contentInsets = scrollView.contentInset + contentInsets.bottom = frame.size.height + scrollView.contentInset = contentInsets + + var scrollIndicatorInsets = scrollView.scrollIndicatorInsets + scrollIndicatorInsets.bottom = frame.size.height + scrollView.scrollIndicatorInsets = scrollIndicatorInsets + + scrollView.scrollToBottom(animated: true) + + self?.fixKeyboardInsets = false + } + + // MARK: 6. RichMessage handlers + for handler in richMessageProviders.values { + if let source = handler.cellSource { + switch source { + case .class(let type): + messagesCollectionView.register(type, forCellWithReuseIdentifier: handler.cellIdentifierSent) + messagesCollectionView.register(type, forCellWithReuseIdentifier: handler.cellIdentifierReceived) + + case .nib(let nib): + messagesCollectionView.register(nib, forCellWithReuseIdentifier: handler.cellIdentifierSent) + messagesCollectionView.register(nib, forCellWithReuseIdentifier: handler.cellIdentifierReceived) + } + } + } } override func viewDidAppear(_ animated: Bool) { @@ -215,6 +312,7 @@ class ChatViewController: MessagesViewController { } cellUpdateTimers.removeAll() + richStatusOperationQueue.cancelAllOperations() } func updateTitle() { @@ -282,8 +380,28 @@ class ChatViewController: MessagesViewController { self?.present(alert, animated: true, completion: nil) } - dialogService.showSystemActionSheet(title: nil, message: nil, actions: [share, rename]) + dialogService.showAlert(title: nil, message: nil, style: .actionSheet, actions: [share, rename]) } + + + // MARK: Tools + private func messageKind(for richMessage: RichMessageTransaction) -> MessageKind { + if let type = richMessage.richType, richMessageProviders[type] != nil, let richContent = richMessage.richContent, let richMessageTransfer = RichMessageTransfer(content: richContent) { + return MessageKind.custom(richMessageTransfer) + } else if var richContent = richMessage.richContent { + if let type = richMessage.richType { + richContent[RichContentKeys.type] = type + } + + if let data = try? JSONSerialization.data(withJSONObject: richContent, options: .prettyPrinted), let string = String(data: data, encoding: String.Encoding.utf8) { + return MessageKind.text(string) + } else { + return MessageKind.text(richMessage.richType ?? "") + } + } else { + return MessageKind.text(richMessage.richType ?? "") + } + } } @@ -342,52 +460,135 @@ extension ChatViewController: NSFetchedResultsControllerDelegate { } func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { - - if type == .insert, let trs = anObject as? MessageTransaction { - trs.isUnread = false - chatroom?.hasUnreadMessages = false + if type == .insert, let trs = anObject as? ChatTransaction { + trs.isUnread = false + chatroom?.hasUnreadMessages = false + + if let rich = anObject as? RichMessageTransaction { + rich.kind = messageKind(for: rich) + } } - if controllerChanges[type] == nil { - controllerChanges[type] = [(IndexPath?, IndexPath?)]() - } - controllerChanges[type]!.append((indexPath, newIndexPath)) + controllerChanges.append(ControllerChange(type: type, indexPath: indexPath, newIndexPath: newIndexPath)) } - private func performBatchChanges(_ changes: [NSFetchedResultsChangeType:[(indexPath: IndexPath?, newIndexPath: IndexPath?)]]) { - for (type, change) in changes { - switch type { - case .insert: - let sections = IndexSet(change.compactMap {$0.newIndexPath?.row}) - if sections.count > 0 { - messagesCollectionView.insertSections(sections) - messagesCollectionView.scrollToBottom(animated: true) - } - - case .delete: - let sections = IndexSet(change.compactMap {$0.indexPath?.row}) - if sections.count > 0 { - messagesCollectionView.deleteSections(sections) - } - - case .move: - for paths in change { - if let section = paths.indexPath?.row, let newSection = paths.newIndexPath?.row { - messagesCollectionView.moveSection(section, toSection: newSection) - } - } - - case .update: - let indexes = change.compactMap { (indexPath: IndexPath?, _) -> IndexPath? in - if let row = indexPath?.row { - return IndexPath(row: 0, section: row) - } else { - return nil - } - } - messagesCollectionView.reloadItems(at: indexes) - return + private func performBatchChanges(_ changes: [ControllerChange]) { + let chat = messagesCollectionView + + let scrollToBottom = changes.first { $0.type == .insert } != nil + + chat.performBatchUpdates({ + for change in changes { + switch change.type { + case .insert: + guard let newIndexPath = change.newIndexPath else { + continue + } + + chat.insertSections(IndexSet(integer: newIndexPath.row)) + chat.scrollToBottom(animated: true) + + case .delete: + guard let indexPath = change.indexPath else { + continue + } + + chat.deleteSections(IndexSet(integer: indexPath.row)) + + case .move: + if let section = change.indexPath?.row, let newSection = change.newIndexPath?.row { + chat.moveSection(section, toSection: newSection) + } + + case .update: + guard let section = change.indexPath?.row else { + continue + } + + chat.reloadItems(at: [IndexPath(row: 0, section: section)]) + } + } + }, completion: { animationSuccess in + if scrollToBottom { + chat.scrollToBottom(animated: animationSuccess) + } + }) + } +} + +extension ChatViewController: TransferViewControllerDelegate, ComplexTransferViewControllerDelegate { + func transferViewControllerDidFinishTransfer(_ viewController: TransferViewControllerBase) { + dismissTransferViewController() + } + + func complexTransferViewControllerDidFinish(_ viewController: ComplexTransferViewController) { + dismissTransferViewController() + } + + private func dismissTransferViewController() { + fixKeyboardInsets = true + + if Thread.isMainThread { + dismiss(animated: true, completion: nil) + } else { + DispatchQueue.main.async { [weak self] in + self?.dismiss(animated: true, completion: nil) } } } } + +// MARK: - RichTransfers status update +extension ChatViewController { + func updateStatus(for transaction: RichMessageTransaction, provider: RichMessageProviderWithStatusCheck) { + transaction.transactionStatus = .updating + + let operation = StatusUpdateProcedure(parentContext: stack.container.viewContext, + objectId: transaction.objectID, + provider: provider) + + richStatusOperationQueue.addOperation(operation) + } +} + +private class StatusUpdateProcedure: Procedure { + // MARK: Props + let parentContext: NSManagedObjectContext + let objectId: NSManagedObjectID + let provider: RichMessageProviderWithStatusCheck + + init(parentContext: NSManagedObjectContext, objectId: NSManagedObjectID, provider: RichMessageProviderWithStatusCheck) { + self.parentContext = parentContext + self.objectId = objectId + self.provider = provider + super.init() + } + + override func execute() { + let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + privateContext.parent = parentContext + + guard let transaction = privateContext.object(with: objectId) as? RichMessageTransaction else { + return + } + + guard let txHash = transaction.richContent?[RichContentKeys.transfer.hash] else { + transaction.transactionStatus = .failed + try? privateContext.save() + return + } + + provider.statusForTransactionBy(hash: txHash) { result in + switch result { + case .success(let status): + transaction.transactionStatus = status + + case .failure: + transaction.transactionStatus = .failed + } + + try? privateContext.save() + self.finish() + } + } +} diff --git a/Adamant/Stories/Chats/ChatsRoutes.swift b/Adamant/Stories/Chats/ChatsRoutes.swift index 02b825f7c..dd9e58867 100644 --- a/Adamant/Stories/Chats/ChatsRoutes.swift +++ b/Adamant/Stories/Chats/ChatsRoutes.swift @@ -18,6 +18,16 @@ extension AdamantScene { c.router = r.resolve(Router.self) c.notificationsService = r.resolve(NotificationsService.self) c.dialogService = r.resolve(DialogService.self) + c.addressBook = r.resolve(AddressBookService.self) + + // MARK: RichMessage handlers + // Transfer handlers from accountService' wallet services + if let accountService = r.resolve(AccountService.self) { + for case let provider as RichMessageProvider in accountService.wallets { + c.richMessageProviders[type(of: provider).richMessageType] = provider + } + } + return c }) @@ -27,7 +37,17 @@ extension AdamantScene { c.dialogService = r.resolve(DialogService.self) c.router = r.resolve(Router.self) c.addressBookService = r.resolve(AddressBookService.self) - return c + c.stack = r.resolve(CoreDataStack.self) + + // MARK: RichMessage handlers + // Transfer handlers from accountService' wallet services + if let accountService = r.resolve(AccountService.self) { + for case let provider as RichMessageProvider in accountService.wallets { + c.richMessageProviders[type(of: provider).richMessageType] = provider + } + } + + return c }) static let newChat = AdamantScene(identifier: "NewChatViewController", factory: { r in @@ -38,10 +58,15 @@ extension AdamantScene { c.router = r.resolve(Router.self) let navigator = UINavigationController(rootViewController: c) - return navigator }) + static let complexTransfer = AdamantScene(identifier: "ComplexTransferViewController", factory: { r in + let c = ComplexTransferViewController() + c.accountService = r.resolve(AccountService.self) + return c + }) + private init() {} } } diff --git a/Adamant/Stories/Chats/ComplexTransferViewController.swift b/Adamant/Stories/Chats/ComplexTransferViewController.swift new file mode 100644 index 000000000..e5c8a8a58 --- /dev/null +++ b/Adamant/Stories/Chats/ComplexTransferViewController.swift @@ -0,0 +1,126 @@ +// +// ComplexTransferViewController.swift +// Adamant +// +// Created by Anokhov Pavel on 19.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import Parchment + +protocol ComplexTransferViewControllerDelegate: class { + func complexTransferViewControllerDidFinish(_ viewController: ComplexTransferViewController) +} + +class ComplexTransferViewController: UIViewController { + // MARK: - Dependencies + + var accountService: AccountService! + + + // MARK: - Properties + var pagingViewController: PagingViewController! + + weak var transferDelegate: ComplexTransferViewControllerDelegate? + var services: [WalletServiceWithSend]! + var partner: CoreDataAccount? + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = UIColor.white + navigationItem.title = partner?.address + navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancel)) + + // MARK: Services + services = accountService.wallets.compactMap { $0 as? WalletServiceWithSend } + + for service in services { + NotificationCenter.default.addObserver(forName: service.walletUpdatedNotification, object: nil, queue: OperationQueue.main) { [weak self] _ in + self?.pagingViewController.reloadData() + } + } + + // MARK: PagingViewController + pagingViewController = PagingViewController() + pagingViewController.menuItemSource = .nib(nib: UINib(nibName: "WalletCollectionViewCell", bundle: nil)) + 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.select(index: 0) + + view.addSubview(pagingViewController.view) + view.constrainToEdges(pagingViewController.view, relativeToSafeArea: true) + addChild(pagingViewController) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + @objc + func cancel() { + transferDelegate?.complexTransferViewControllerDidFinish(self) + } +} + +extension ComplexTransferViewController: PagingViewControllerDataSource { + func numberOfViewControllers(in pagingViewController: PagingViewController) -> Int { + if let services = services { + return services.count + } else { + return 0 + } + } + + func pagingViewController(_ pagingViewController: PagingViewController, viewControllerForIndex index: Int) -> UIViewController { + let vc = services[index].transferViewController() + if let v = vc as? TransferViewControllerBase { + if let address = partner?.address { + v.admReportRecipient = address + v.recipientIsReadonly = true + v.showProgressView(animated: false) + + services[index].getWalletAddress(byAdamantAddress: address) { result in + switch result { + case .success(let walletAddress): + DispatchQueue.main.async { + v.recipient = walletAddress + v.hideProgress(animated: true) + } + + case .failure(let error): + v.showAlertView(title: nil, message: error.message, animated: true) + } + } + } + + v.delegate = self + } + + return vc + } + + func pagingViewController(_ pagingViewController: PagingViewController, pagingItemForIndex index: Int) -> T { + let service = accountService.wallets[index] + + guard let wallet = service.wallet else { + return WalletPagingItem(index: index, currencySymbol: "", currencyImage: #imageLiteral(resourceName: "wallet_adm")) as! T + } + + let serviceType = type(of: service) + + let item = WalletPagingItem(index: index, currencySymbol: serviceType.currencySymbol, currencyImage: serviceType.currencyLogo) + item.balance = wallet.balance + + return item as! T + } +} + +extension ComplexTransferViewController: TransferViewControllerDelegate { + func transferViewControllerDidFinishTransfer(_ viewController: TransferViewControllerBase) { + transferDelegate?.complexTransferViewControllerDidFinish(self) + } +} diff --git a/Adamant/Stories/Chats/CustomCellDeleage.swift b/Adamant/Stories/Chats/CustomCellDeleage.swift new file mode 100644 index 000000000..c3950455f --- /dev/null +++ b/Adamant/Stories/Chats/CustomCellDeleage.swift @@ -0,0 +1,18 @@ +// +// CustomCellDeleage.swift +// Adamant +// +// Created by Anokhov Pavel on 28.09.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation + +protocol TapRecognizerCustomCell: class { + /// Must be a weak reference + var delegate: CustomCellDelegate? { get set } +} + +protocol CustomCellDelegate: class { + func didTapCustomCell(_ cell: TapRecognizerCustomCell) +} diff --git a/Adamant/Stories/Chats/NewChatViewController.swift b/Adamant/Stories/Chats/NewChatViewController.swift index f76d57d1b..5a0a2ef3f 100644 --- a/Adamant/Stories/Chats/NewChatViewController.swift +++ b/Adamant/Stories/Chats/NewChatViewController.swift @@ -183,7 +183,8 @@ class NewChatViewController: FormViewController { }.cellUpdate { (cell, _) in cell.textLabel?.textColor = UIColor.adamant.primary }.onCellSelection { [weak self] (cell, row) in - switch AdamantQRTools.generateQrFrom(string: address) { + let encodedAddress = AdamantUriTools.encode(request: AdamantUri.address(address: address, params: nil)) + switch AdamantQRTools.generateQrFrom(string: encodedAddress) { case .success(let qr): guard let vc = self?.router.get(scene: AdamantScene.Shared.shareQr) as? ShareQrViewController else { fatalError("Can't find ShareQrViewController") @@ -339,8 +340,8 @@ extension NewChatViewController { alert.addAction(UIAlertAction(title: String.adamantLocalized.alert.settings, style: .default) { _ in DispatchQueue.main.async { - if let settingsURL = URL(string: UIApplicationOpenSettingsURLString) { - UIApplication.shared.open(settingsURL, options: [:], completionHandler: nil) + if let settingsURL = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(settingsURL) } } }) @@ -385,15 +386,39 @@ extension NewChatViewController { // MARK: - QRCodeReaderViewControllerDelegate extension NewChatViewController: QRCodeReaderViewControllerDelegate { func reader(_ reader: QRCodeReaderViewController, didScanResult result: QRCodeReaderResult) { - guard let uri = AdamantUriTools.decode(uri: result.value) else { - dialogService.showWarning(withMessage: String.adamantLocalized.newChat.wrongQrError) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - reader.startScanning() + let address: String? + var name: String? = nil + + if let uri = AdamantUriTools.decode(uri: result.value) { + switch uri { + case .address(address: let addr, params: let params): + address = addr + + if let params = params { + for param in params { + switch param { + case .label(let label): + name = label + break + } + } + } + + case .passphrase(_): + address = nil + } + } else { + switch AdamantUtilities.validateAdamantAddress(address: result.value) { + case .valid, .system: + address = result.value + + case .invalid: + address = nil } - return } - if startNewChat(with: uri) { + if let address = address { + startNewChat(with: address, name: name) dismiss(animated: true, completion: nil) } else { dialogService.showWarning(withMessage: String.adamantLocalized.newChat.wrongQrError) @@ -411,10 +436,10 @@ extension NewChatViewController: QRCodeReaderViewControllerDelegate { // MARK: - UIImagePickerControllerDelegate extension NewChatViewController: UINavigationControllerDelegate, UIImagePickerControllerDelegate { - func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) { - dismiss(animated: true, completion: nil) + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + dismiss(animated: true, completion: nil) - guard let image = info[UIImagePickerControllerOriginalImage] as? UIImage else { + guard let image = info[.originalImage] as? UIImage else { return } @@ -424,6 +449,15 @@ extension NewChatViewController: UINavigationControllerDelegate, UIImagePickerCo if startNewChat(with: uri) { return } + } else { + switch AdamantUtilities.validateAdamantAddress(address: aCode) { + case .valid, .system: + startNewChat(with: aCode, name: nil) + return + + case .invalid: + break + } } } diff --git a/Adamant/Stories/Delegates/DelegateDetailsViewController.swift b/Adamant/Stories/Delegates/DelegateDetailsViewController.swift index d7836c1ad..d7c2d2ead 100644 --- a/Adamant/Stories/Delegates/DelegateDetailsViewController.swift +++ b/Adamant/Stories/Delegates/DelegateDetailsViewController.swift @@ -228,7 +228,7 @@ extension DelegateDetailsViewController { case .vote: let weight = Decimal(string: delegate.vote)?.shiftedFromAdamant() ?? 0 - cell.detailTextLabel?.text = AdamantUtilities.currencyFormatterShort.string(for: weight) + cell.detailTextLabel?.text = AdamantBalanceFormat.currencyFormatterShort.string(for: weight) case .producedblocks: cell.detailTextLabel?.text = String(delegate.producedblocks) @@ -264,7 +264,7 @@ extension DelegateDetailsViewController { } case .forged: - cell.detailTextLabel?.text = AdamantUtilities.currencyFormatterShort.string(for: forged) + cell.detailTextLabel?.text = AdamantBalanceFormat.currencyFormatterShort.string(for: forged) } return cell diff --git a/Adamant/Stories/Delegates/DelegatesListViewController.swift b/Adamant/Stories/Delegates/DelegatesListViewController.swift index 0379a0a5d..b2c83cd62 100644 --- a/Adamant/Stories/Delegates/DelegatesListViewController.swift +++ b/Adamant/Stories/Delegates/DelegatesListViewController.swift @@ -59,7 +59,7 @@ class DelegatesListViewController: UIViewController { let refreshControl = UIRefreshControl() refreshControl.addTarget(self, action: #selector(self.handleRefresh(_:)), - for: UIControlEvents.valueChanged) + for: UIControl.Event.valueChanged) refreshControl.tintColor = UIColor.adamant.primary return refreshControl diff --git a/Adamant/Stories/Login/LoginViewController+Pinpad.swift b/Adamant/Stories/Login/LoginViewController+Pinpad.swift index 0760dccad..20ccf7f7e 100644 --- a/Adamant/Stories/Login/LoginViewController+Pinpad.swift +++ b/Adamant/Stories/Login/LoginViewController+Pinpad.swift @@ -55,14 +55,34 @@ extension LoginViewController { accountService.loginWithStoredAccount { [weak self] result in switch result { - case .success(account: _): + case .success(_, let alert): self?.dialogService.dismissProgress() + guard let presenter = self?.presentingViewController else { + return + } + + let alertVc: UIAlertController? + if let alert = alert { + alertVc = UIAlertController(title: alert.title, message: alert.message, preferredStyle: .alert) + alertVc!.addAction(UIAlertAction(title: String.adamantLocalized.alert.ok, style: .default)) + } else { + alertVc = nil + } + if Thread.isMainThread { - self?.presentingViewController?.dismiss(animated: true, completion: nil) + presenter.dismiss(animated: true, completion: nil) + + if let alertVc = alertVc { + presenter.present(alertVc, animated: true, completion: nil) + } } else { DispatchQueue.main.async { - self?.presentingViewController?.dismiss(animated: true, completion: nil) + presenter.dismiss(animated: true, completion: nil) + + if let alertVc = alertVc { + presenter.present(alertVc, animated: true, completion: nil) + } } } diff --git a/Adamant/Stories/Login/LoginViewController+QR.swift b/Adamant/Stories/Login/LoginViewController+QR.swift index 3d573c353..1c11a03b9 100644 --- a/Adamant/Stories/Login/LoginViewController+QR.swift +++ b/Adamant/Stories/Login/LoginViewController+QR.swift @@ -102,10 +102,10 @@ extension LoginViewController: QRCodeReaderViewControllerDelegate { // MARK: - UIImagePickerControllerDelegate extension LoginViewController: UINavigationControllerDelegate, UIImagePickerControllerDelegate { - func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) { + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { dismiss(animated: true, completion: nil) - guard let image = info[UIImagePickerControllerOriginalImage] as? UIImage else { + guard let image = info[.originalImage] as? UIImage else { return } diff --git a/Adamant/Stories/Login/LoginViewController.swift b/Adamant/Stories/Login/LoginViewController.swift index 85bf0f3f5..afca57e33 100644 --- a/Adamant/Stories/Login/LoginViewController.swift +++ b/Adamant/Stories/Login/LoginViewController.swift @@ -126,7 +126,6 @@ class LoginViewController: FormViewController { var localAuth: LocalAuthentication! var router: Router! - // MARK: Properties private var hideNewPassphrase: Bool = true private var generatedPassphrases = [String]() @@ -238,7 +237,7 @@ class LoginViewController: FormViewController { style.alignment = NSTextAlignment.center let mutableText = NSMutableAttributedString(attributedString: parser.parse(Rows.saveYourPassphraseAlert.localized)) - mutableText.addAttribute(NSAttributedStringKey.paragraphStyle, value: style, range: NSRange(location: 0, length: mutableText.length)) + mutableText.addAttribute(NSAttributedString.Key.paragraphStyle, value: style, range: NSRange(location: 0, length: mutableText.length)) cell.textView.attributedText = mutableText } @@ -306,7 +305,7 @@ class LoginViewController: FormViewController { // MARK: tableView position tuning if let row: PasswordRow = form.rowBy(tag: Rows.passphrase.tag) { - NotificationCenter.default.addObserver(forName: Notification.Name.UITextFieldTextDidBeginEditing, object: row.cell.textField, queue: nil) { [weak self] _ in + NotificationCenter.default.addObserver(forName: UITextField.textDidBeginEditingNotification, object: row.cell.textField, queue: nil) { [weak self] _ in guard let tableView = self?.tableView, let indexPath = self?.form.rowBy(tag: Rows.loginButton.tag)?.indexPath else { return } @@ -318,7 +317,7 @@ class LoginViewController: FormViewController { } // MARK: Requesting biometry onActive - NotificationCenter.default.addObserver(forName: Notification.Name.UIApplicationDidBecomeActive, object: nil, queue: OperationQueue.main) { [weak self] _ in + NotificationCenter.default.addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: OperationQueue.main) { [weak self] _ in if let firstTimeActive = self?.firstTimeActive, firstTimeActive, let accountService = self?.accountService, accountService.hasStayInAccount, accountService.useBiometry { self?.loginWithBiometry() @@ -385,7 +384,7 @@ extension LoginViewController { private func loginIntoExistingAccount(passphrase: String) { accountService.loginWith(passphrase: passphrase, completion: { [weak self] result in switch result { - case .success(_): + case .success(_, let alert): if let nav = self?.navigationController { nav.popViewController(animated: true) } else { @@ -394,6 +393,10 @@ extension LoginViewController { self?.dialogService.dismissProgress() + if let alert = alert { + self?.dialogService.showAlert(title: alert.title, message: alert.message, style: UIAlertController.Style.alert, actions: nil) + } + case .failure(let error): self?.dialogService.showRichError(error: error) } diff --git a/Adamant/Stories/NodesEditor/NodeEditorViewController.swift b/Adamant/Stories/NodesEditor/NodeEditorViewController.swift index aa48ff300..f953cfbac 100644 --- a/Adamant/Stories/NodesEditor/NodeEditorViewController.swift +++ b/Adamant/Stories/NodesEditor/NodeEditorViewController.swift @@ -99,7 +99,7 @@ class NodeEditorViewController: FormViewController { } } - fileprivate var accessoryType: UITableViewCellAccessoryType { + fileprivate var accessoryType: UITableViewCell.AccessoryType { switch self { case .notTested, .failed: return .disclosureIndicator case .passed: return .checkmark diff --git a/Adamant/Stories/Settings/QRGeneratorViewController.swift b/Adamant/Stories/Settings/QRGeneratorViewController.swift index 6ded1e2ad..d70e7eb0f 100644 --- a/Adamant/Stories/Settings/QRGeneratorViewController.swift +++ b/Adamant/Stories/Settings/QRGeneratorViewController.swift @@ -148,11 +148,11 @@ class QRGeneratorViewController: FormViewController { } } - override func insertAnimation(forSections sections: [Section]) -> UITableViewRowAnimation { + override func insertAnimation(forSections sections: [Section]) -> UITableView.RowAnimation { return .top } - override func insertAnimation(forRows rows: [BaseRow]) -> UITableViewRowAnimation { + override func insertAnimation(forRows rows: [BaseRow]) -> UITableView.RowAnimation { return .top } diff --git a/Adamant/Stories/Settings/SecurityViewController+StayIn.swift b/Adamant/Stories/Settings/SecurityViewController+StayIn.swift index e31ba4b55..d10113519 100644 --- a/Adamant/Stories/Settings/SecurityViewController+StayIn.swift +++ b/Adamant/Stories/Settings/SecurityViewController+StayIn.swift @@ -120,7 +120,7 @@ extension SecurityViewController: PinpadViewControllerDelegate { accountService.setStayLoggedIn(pin: pin) { [weak self] result in switch result { - case .success(account: _): + case .success: self?.pinpadRequest = nil DispatchQueue.main.async { if let row: SwitchRow = self?.form.rowBy(tag: Rows.biometry.tag) { diff --git a/Adamant/Stories/Settings/SecurityViewController+notifications.swift b/Adamant/Stories/Settings/SecurityViewController+notifications.swift index 68f5ad044..fa640de8c 100644 --- a/Adamant/Stories/Settings/SecurityViewController+notifications.swift +++ b/Adamant/Stories/Settings/SecurityViewController+notifications.swift @@ -38,8 +38,8 @@ extension SecurityViewController { alert.addAction(UIAlertAction(title: String.adamantLocalized.alert.settings, style: .default) { _ in DispatchQueue.main.async { - if let settingsURL = URL(string: UIApplicationOpenSettingsURLString) { - UIApplication.shared.open(settingsURL, options: [:], completionHandler: nil) + if let settingsURL = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(settingsURL) } } }) diff --git a/Adamant/Stories/Shared/ShareQrViewController.swift b/Adamant/Stories/Shared/ShareQrViewController.swift index 43b4b7481..ed009f8b6 100644 --- a/Adamant/Stories/Shared/ShareQrViewController.swift +++ b/Adamant/Stories/Shared/ShareQrViewController.swift @@ -77,7 +77,7 @@ class ShareQrViewController: FormViewController { } } - var excludedActivityTypes: [UIActivityType]? + var excludedActivityTypes: [UIActivity.ActivityType]? // MARK: - Lifecycle override func viewDidLoad() { @@ -140,7 +140,7 @@ class ShareQrViewController: FormViewController { vc.excludedActivityTypes = excludedActivityTypes } - vc.completionWithItemsHandler = { [weak self] (type: UIActivityType?, completed: Bool, _, error: Error?) in + vc.completionWithItemsHandler = { [weak self] (type: UIActivity.ActivityType?, completed: Bool, _, error: Error?) in if completed { if let error = error { self?.dialogService.showWarning(withMessage: error.localizedDescription) diff --git a/Adamant/Stories/Transactions/TransactionDetailsViewController.swift b/Adamant/Stories/Transactions/TransactionDetailsViewController.swift deleted file mode 100644 index 8aad088c3..000000000 --- a/Adamant/Stories/Transactions/TransactionDetailsViewController.swift +++ /dev/null @@ -1,355 +0,0 @@ -// -// TransactionDetailsViewController.swift -// Adamant -// -// Created by Anokhov Pavel on 09.01.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import UIKit -import SafariServices - - -// MARK: - Localization -extension String.adamantLocalized { - struct transactionDetails { - static let title = NSLocalizedString("TransactionDetailsScene.Title", comment: "Transaction details: scene title") - } -} - -extension String.adamantLocalized.alert { - static let exportUrlButton = NSLocalizedString("TransactionDetailsScene.Share.URL", comment: "Export transaction: 'Share transaction URL' button") - static let exportSummaryButton = NSLocalizedString("TransactionDetailsScene.Share.Summary", comment: "Export transaction: 'Share transaction summary' button") -} - - -// MARK: - -class TransactionDetailsViewController: UIViewController { - // MARK: - Rows - fileprivate enum Row: Int { - case transactionNumber = 0 - case from - case to - case date - case amount - case fee - case confirmations - case block - case openInExplorer - case openChat // if transaction.chatroom.isHidden, numberOfRowsInSection will return total-1 - - static let total = 10 - - var localized: String { - switch self { - case .transactionNumber: return NSLocalizedString("TransactionDetailsScene.Row.Id", comment: "Transaction details: Id row.") - case .from: return NSLocalizedString("TransactionDetailsScene.Row.From", comment: "Transaction details: sender row.") - case .to: return NSLocalizedString("TransactionDetailsScene.Row.To", comment: "Transaction details: recipient row.") - case .date: return NSLocalizedString("TransactionDetailsScene.Row.Date", comment: "Transaction details: date row.") - case .amount: return NSLocalizedString("TransactionDetailsScene.Row.Amount", comment: "Transaction details: amount row.") - case .fee: return NSLocalizedString("TransactionDetailsScene.Row.Fee", comment: "Transaction details: fee row.") - case .confirmations: return NSLocalizedString("TransactionDetailsScene.Row.Confirmations", comment: "Transaction details: confirmations row.") - case .block: return NSLocalizedString("TransactionDetailsScene.Row.Block", comment: "Transaction details: Block id row.") - case .openInExplorer: return NSLocalizedString("TransactionDetailsScene.Row.Explorer", comment: "Transaction details: 'Open transaction in explorer' row.") - case .openChat: return "" - } - } - - var image: UIImage? { - switch self { - case .openInExplorer: return #imageLiteral(resourceName: "row_explorer") - case .openChat: return #imageLiteral(resourceName: "row_chat") - - default: return nil - } - } - } - - // MARK: - Dependencies - var accountService: AccountService! - var dialogService: DialogService! - var transfersProvider: TransfersProvider! - var router: Router! - - // MARK: - Properties - private let cellIdentifier = "cell" - var transaction: TransferTransaction? - var explorerUrl: URL! - var haveChatroom = false - var showToChatRow = true - - private let autoupdateInterval: TimeInterval = 5.0 - - weak var timer: Timer? - - // MARK: - IBOutlets - @IBOutlet weak var tableView: UITableView! - - - // MARK: - Lifecycle - - override func viewDidLoad() { - if #available(iOS 11.0, *) { - navigationController?.navigationBar.prefersLargeTitles = true - } - - navigationItem.title = String.adamantLocalized.transactionDetails.title - navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(share)) - tableView.dataSource = self - tableView.delegate = self - - if let transaction = transaction { - if let chatroom = transaction.partner?.chatroom, let transactions = chatroom.transactions { - let messeges = transactions.first (where: { (object) -> Bool in - return !(object is TransferTransaction) - }) - - haveChatroom = (messeges != nil) - } - - tableView.reloadData() - - if let id = transaction.transactionId { - explorerUrl = URL(string: "https://explorer.adamant.im/tx/\(id)") - } - } else { - self.navigationItem.rightBarButtonItems = nil - } - - startUpdate() - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - if let indexPath = tableView.indexPathForSelectedRow { - tableView.deselectRow(at: indexPath, animated: animated) - } - } - - deinit { - stopUpdate() - } - - - // MARK: - IBActions - - @IBAction func share(_ sender: Any) { - guard let transaction = transaction, let url = explorerUrl else { - return - } - - let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - alert.addAction(UIAlertAction(title: String.adamantLocalized.alert.cancel, style: .cancel, handler: nil)) - - // URL - alert.addAction(UIAlertAction(title: String.adamantLocalized.alert.exportUrlButton, style: .default) { [weak self] _ in - let alert = UIActivityViewController(activityItems: [url], applicationActivities: nil) - self?.present(alert, animated: true, completion: nil) - }) - - // Description - alert.addAction(UIAlertAction(title: String.adamantLocalized.alert.exportSummaryButton, style: .default, handler: { [weak self] _ in - let text = AdamantFormattingTools.summaryFor(transaction: transaction, url: url) - let alert = UIActivityViewController(activityItems: [text], applicationActivities: nil) - self?.present(alert, animated: true, completion: nil) - })) - - present(alert, animated: true, completion: nil) - } - - - -} - - -// MARK: - UITableView -extension TransactionDetailsViewController: UITableViewDataSource, UITableViewDelegate { - func numberOfSections(in tableView: UITableView) -> Int { - return 1 - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - guard let transaction = transaction else { - return 0 - } - - var total = Row.total - - if let hidden = transaction.chatroom?.isHidden, hidden { - total -= 1 - } else if !showToChatRow { - total -= 1 - } - - return total - } - - func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { - return UIView() - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return 50 - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let row = Row(rawValue: indexPath.row) else { - tableView.deselectRow(at: indexPath, animated: true) - return - } - - switch row { - case .openInExplorer: - if let url = explorerUrl { - let safari = SFSafariViewController(url: url) - safari.preferredControlTintColor = UIColor.adamant.primary - present(safari, animated: true, completion: nil) - } - - case .openChat: - // TODO: Log errors - guard let vc = self.router.get(scene: AdamantScene.Chats.chat) as? ChatViewController else { - dialogService.showError(withMessage: "TransactionDetailsViewController: Failed to get ChatViewController", error: nil) - break - } - - guard let chatroom = transaction?.partner?.chatroom else { - dialogService.showError(withMessage: "TransactionDetailsViewController: Failed to get chatroom for transaction.", error: nil) - break - } - - guard let account = self.accountService.account else { - dialogService.showError(withMessage: "TransactionDetailsViewController: User not logged.", error: nil) - break - } - - vc.account = account - vc.chatroom = chatroom - vc.hidesBottomBarWhenPushed = true - - if let nav = self.navigationController { - nav.pushViewController(vc, animated: true) - } else { - self.present(vc, animated: true) - } - - default: - let share: String - if row == .date, let date = transaction?.date as Date? { - share = DateFormatter.localizedString(from: date, dateStyle: .medium, timeStyle: .medium) - - } else if let cell = tableView.cellForRow(at: indexPath), let details = cell.detailTextLabel?.text { - share = details - } else { - tableView.deselectRow(at: indexPath, animated: true) - break - } - - dialogService.presentShareAlertFor(string: share, - types: [.copyToPasteboard, .share], - excludedActivityTypes: nil, - animated: true) - { - tableView.deselectRow(at: indexPath, animated: true) - } - } - } -} - - -// MARK: - UITableView Cells -extension TransactionDetailsViewController { - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let transaction = transaction, let row = Row(rawValue: indexPath.row) else { - // TODO: Display & Log error - return UITableViewCell(style: .default, reuseIdentifier: cellIdentifier) - } - - let cell: UITableViewCell - if let c = tableView.dequeueReusableCell(withIdentifier: cellIdentifier) { - cell = c - cell.accessoryType = .none - cell.imageView?.image = nil - } else { - cell = UITableViewCell(style: .value1, reuseIdentifier: cellIdentifier) - } - - cell.textLabel?.text = row.localized - cell.imageView?.image = row.image - cell.imageView?.tintColor = UIColor.adamant.tableRowIcons - - switch row { - case .amount: - if let amount = transaction.amount { - cell.detailTextLabel?.text = AdamantUtilities.format(balance: amount) - } - - case .date: - if let date = transaction.date as Date? { - cell.detailTextLabel?.text = date.humanizedDateTimeFull() - } - - case .confirmations: - cell.detailTextLabel?.text = String(transaction.confirmations) - - case .fee: - if let fee = transaction.fee { - cell.detailTextLabel?.text = AdamantUtilities.format(balance: fee) - } - - case .transactionNumber: - if let id = transaction.transactionId { - cell.detailTextLabel?.text = String(id) - } - - case .from: - cell.detailTextLabel?.text = transaction.senderId - - case .to: - cell.detailTextLabel?.text = transaction.recipientId - - case .block: - cell.detailTextLabel?.text = transaction.blockId - - case .openInExplorer: - cell.detailTextLabel?.text = nil - cell.accessoryType = .disclosureIndicator - case .openChat: - cell.textLabel?.text = (self.haveChatroom) ? String.adamantLocalized.transactionList.toChat : String.adamantLocalized.transactionList.startChat - cell.detailTextLabel?.text = nil - cell.accessoryType = .disclosureIndicator -// cell.imageView?.image = (haveChatroom) ? #imageLiteral(resourceName: "chats_tab") : #imageLiteral(resourceName: "Chat") - } - - return cell - } -} - - -// MARK: - Autoupdate -extension TransactionDetailsViewController { - func startUpdate() { - timer?.invalidate() - timer = Timer.scheduledTimer(withTimeInterval: autoupdateInterval, repeats: true) { [weak self] _ in - guard let id = self?.transaction?.transactionId else { - return - } - - self?.transfersProvider.refreshTransfer(id: id) { result in - switch result { - case .success: - DispatchQueue.main.async { - self?.tableView.reloadData() - } - - case .failure: - return - } - } - } - } - - func stopUpdate() { - timer?.invalidate() - } -} diff --git a/Adamant/Stories/Transactions/TransactionDetailsViewController.xib b/Adamant/Stories/Transactions/TransactionDetailsViewController.xib deleted file mode 100644 index 6f359e85e..000000000 --- a/Adamant/Stories/Transactions/TransactionDetailsViewController.xib +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Adamant/Stories/Transactions/TransactionsRoutes.swift b/Adamant/Stories/Transactions/TransactionsRoutes.swift deleted file mode 100644 index 5a240a9a7..000000000 --- a/Adamant/Stories/Transactions/TransactionsRoutes.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// TransactionsRoutes.swift -// Adamant -// -// Created by Anokhov Pavel on 17.03.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import Foundation - -extension AdamantScene { - struct Transactions { - static let transactions = AdamantScene(identifier: "TransactionsViewController", factory: { r in - let c = TransactionsViewController(nibName: "TransactionsViewController", bundle: nil) - c.accountService = r.resolve(AccountService.self) - c.transfersProvider = r.resolve(TransfersProvider.self) - c.dialogService = r.resolve(DialogService.self) - c.router = r.resolve(Router.self) - c.stack = r.resolve(CoreDataStack.self) - return c - }) - - static let transactionDetails = AdamantScene(identifier: "TransactionDetailsViewController", factory: { r in - let c = TransactionDetailsViewController(nibName: "TransactionDetailsViewController", bundle: nil) - c.accountService = r.resolve(AccountService.self) - c.dialogService = r.resolve(DialogService.self) - c.transfersProvider = r.resolve(TransfersProvider.self) - c.router = r.resolve(Router.self) - return c - }) - - private init() {} - } -} diff --git a/Adamant/Stories/Transactions/TransactionsViewController.swift b/Adamant/Stories/Transactions/TransactionsViewController.swift deleted file mode 100644 index 8f6fe9475..000000000 --- a/Adamant/Stories/Transactions/TransactionsViewController.swift +++ /dev/null @@ -1,353 +0,0 @@ -// -// TransactionsViewController.swift -// Adamant -// -// Created by Anokhov Pavel on 08.01.2018. -// Copyright © 2018 Adamant. All rights reserved. -// - -import UIKit -import CoreData - -extension String.adamantLocalized { - struct transactionList { - static let title = NSLocalizedString("TransactionListScene.Title", comment: "TransactionList: scene title") - static let toChat = NSLocalizedString("TransactionListScene.ToChat", comment: "TransactionList: To Chat button") - static let startChat = NSLocalizedString("TransactionListScene.StartChat", comment: "TransactionList: Start Chat button") - } -} - -class TransactionsViewController: UIViewController { - let cellIdentifier = "cell" - let cellHeight: CGFloat = 90.0 - - // MARK: - Dependencies - var accountService: AccountService! - var transfersProvider: TransfersProvider! - var dialogService: DialogService! - var stack: CoreDataStack! - var router: Router! - - // MARK: - Properties - var controller: NSFetchedResultsController? - - private lazy var refreshControl: UIRefreshControl = { - let refreshControl = UIRefreshControl() - refreshControl.addTarget(self, action: - #selector(self.handleRefresh(_:)), - for: UIControlEvents.valueChanged) - refreshControl.tintColor = UIColor.adamant.primary - - return refreshControl - }() - - - // MARK: - IBOutlets - @IBOutlet weak var tableView: UITableView! - - - // MARK: - Lifecycle - - override func viewDidLoad() { - super.viewDidLoad() - - if #available(iOS 11.0, *) { - navigationController?.navigationBar.prefersLargeTitles = false - } - - navigationItem.title = String.adamantLocalized.transactionList.title - - if accountService.account != nil { - initFetchedResultController(provider: transfersProvider) - } - - tableView.register(UINib.init(nibName: "TransactionTableViewCell", bundle: nil), forCellReuseIdentifier: cellIdentifier) - tableView.dataSource = self - tableView.delegate = self - tableView.refreshControl = refreshControl - - NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userLoggedIn, object: nil, queue: nil) { [weak self] notification in - self?.initFetchedResultController(provider: self?.transfersProvider) - } - - NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userLoggedOut, object: nil, queue: nil) { [weak self] _ in - self?.initFetchedResultController(provider: nil) - } - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - if let indexPath = tableView.indexPathForSelectedRow { - tableView.deselectRow(at: indexPath, animated: animated) - } - - if tableView.isEditing { - tableView.setEditing(false, animated: false) - } - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - // TransactionDetails can reset this setting - if #available(iOS 11.0, *) { - navigationController?.navigationBar.prefersLargeTitles = false - } - - markTransfersAsRead() - } - - - /// - Parameter provider: nil to drop and reset - private func initFetchedResultController(provider: TransfersProvider?) { - controller = transfersProvider.transfersController() - controller?.delegate = self - - do { - try controller?.performFetch() - } catch { - print("There was an error performing fetch: \(error)") - controller = nil - } - - tableView.reloadData() - } - - @objc private func handleRefresh(_ refreshControl: UIRefreshControl) { - self.transfersProvider.update { [weak self] (result) in - guard let result = result else { - DispatchQueue.main.async { - refreshControl.endRefreshing() - } - return - } - - switch result { - case .success: - DispatchQueue.main.async { - self?.tableView.reloadData() - } - break - case .failure(let error): - self?.dialogService.showRichError(error: error) - } - - DispatchQueue.main.async { - refreshControl.endRefreshing() - } - } - } - - private func markTransfersAsRead() { - let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) - privateContext.parent = 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() - } - } - } -} - -// MARK: - UITableView Cells -extension TransactionsViewController { - private func configureCell(_ cell: TransactionTableViewCell, for transfer: TransferTransaction) { - cell.accountLabel.tintColor = UIColor.adamant.primary - cell.ammountLabel.tintColor = UIColor.adamant.primary - cell.dateLabel.tintColor = UIColor.adamant.secondary - cell.topImageView.tintColor = UIColor.black - - if transfer.isOutgoing { - cell.transactionType = .outcome - cell.accountLabel.text = transfer.recipientId - } else { - cell.transactionType = .income - cell.accountLabel.text = transfer.senderId - } - - if let amount = transfer.amount { - cell.ammountLabel.text = AdamantUtilities.format(balance: amount) - } - - if let date = transfer.date as Date? { - cell.dateLabel.text = date.humanizedDateTime() - } - } -} - - -// MARK: - UITableView -extension TransactionsViewController: UITableViewDataSource, UITableViewDelegate { - func numberOfSections(in tableView: UITableView) -> Int { - return 1 - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if let f = controller?.fetchedObjects { - return f.count - } else { - return 0 - } - } - - func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { - return UIView() - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return cellHeight - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard let transaction = controller?.object(at: indexPath) else { - tableView.deselectRow(at: indexPath, animated: true) - return - } - - guard let controller = router.get(scene: AdamantScene.Transactions.transactionDetails) as? TransactionDetailsViewController else { - return - } - - controller.transaction = transaction - navigationController?.pushViewController(controller, animated: true) - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? TransactionTableViewCell, - let transfer = controller?.object(at: indexPath) else { - // TODO: Display & Log error - return UITableViewCell(style: .default, reuseIdentifier: "cell") - } - - cell.accessoryType = .disclosureIndicator - - configureCell(cell, for: transfer) - return cell - } - - func tableView(_ tableView: UITableView, editActionsForRowAt: IndexPath) -> [UITableViewRowAction]? { - guard let transfer = controller?.object(at: editActionsForRowAt), let chatroom = transfer.partner?.chatroom, let transactions = chatroom.transactions else { - return nil - } - - let messeges = transactions.first (where: { (object) -> Bool in - return !(object is TransferTransaction) - }) - - let title = (messeges != nil) ? String.adamantLocalized.transactionList.toChat : String.adamantLocalized.transactionList.startChat - - let toChat = UITableViewRowAction(style: .normal, title: title) { action, index in - guard let vc = self.router.get(scene: AdamantScene.Chats.chat) as? ChatViewController else { - // TODO: Log this - return - } - - guard let account = self.accountService.account else { - return - } - - vc.account = account - vc.chatroom = chatroom - vc.hidesBottomBarWhenPushed = true - - if let nav = self.navigationController { - nav.pushViewController(vc, animated: true) - } else { - self.present(vc, animated: true) - } - } - - toChat.backgroundColor = UIColor.adamant.primary - - return [toChat] - } - - func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { - return true - } - - @available(iOS 11.0, *) - func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - guard let transfer = controller?.object(at: indexPath), let chatroom = transfer.partner?.chatroom, let transactions = chatroom.transactions else { - return nil - } - - let messeges = transactions.first (where: { (object) -> Bool in - return !(object is TransferTransaction) - }) - - let title = (messeges != nil) ? String.adamantLocalized.transactionList.toChat : String.adamantLocalized.transactionList.startChat - - let toChat = UIContextualAction(style: .normal, title: title, handler: { (ac:UIContextualAction, view:UIView, success:(Bool) -> Void) in - guard let vc = self.router.get(scene: AdamantScene.Chats.chat) as? ChatViewController else { - // TODO: Log this - return - } - - guard let account = self.accountService.account else { - return - } - - vc.account = account - vc.chatroom = chatroom - vc.hidesBottomBarWhenPushed = true - - if let nav = self.navigationController { - nav.pushViewController(vc, animated: true) - } else { - self.present(vc, animated: true) - } - }) - - toChat.image = (messeges != nil) ? #imageLiteral(resourceName: "chats_tab") : #imageLiteral(resourceName: "Chat") - toChat.backgroundColor = UIColor.adamant.primary - return UISwipeActionsConfiguration(actions: [toChat]) - } -} - -extension TransactionsViewController: NSFetchedResultsControllerDelegate { - func controllerWillChangeContent(_ controller: NSFetchedResultsController) { - tableView.beginUpdates() - } - - func controllerDidChangeContent(_ controller: NSFetchedResultsController) { - tableView.endUpdates() - } - - func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { - switch type { - case .insert: - if let newIndexPath = newIndexPath { - tableView.insertRows(at: [newIndexPath], with: .automatic) - - if let transfer = anObject as? TransferTransaction { - transfer.isUnread = false - } - } - - 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? TransactionTableViewCell, - let transfer = anObject as? TransferTransaction { - configureCell(cell, for: transfer) - } - - default: - break - } - } -} - - diff --git a/Adamant/SwinjectDependencies.swift b/Adamant/SwinjectDependencies.swift index 36b9ba5f2..516b5c073 100644 --- a/Adamant/SwinjectDependencies.swift +++ b/Adamant/SwinjectDependencies.swift @@ -83,7 +83,11 @@ extension Container { service.securedStore = r.resolve(SecuredStore.self)! service.notificationsService = r.resolve(NotificationsService.self)! return service - }.inObjectScope(.container) + }.inObjectScope(.container).initCompleted { (r, service) in + for case let wallet as SwinjectDependentService in service.wallets { + wallet.injectDependencies(from: self) + } + } // MARK: AddressBookServeice self.register(AddressBookService.self) { r in @@ -124,12 +128,19 @@ extension Container { // MARK: Chats self.register(ChatsProvider.self) { r in let provider = AdamantChatsProvider() - provider.accountService = r.resolve(AccountService.self) - provider.apiService = r.resolve(ApiService.self) - provider.stack = r.resolve(CoreDataStack.self) - provider.adamantCore = r.resolve(AdamantCore.self) - provider.accountsProvider = r.resolve(AccountsProvider.self) - provider.securedStore = r.resolve(SecuredStore.self) + provider.apiService = r.resolve(ApiService.self) + provider.stack = r.resolve(CoreDataStack.self) + provider.adamantCore = r.resolve(AdamantCore.self) + provider.securedStore = r.resolve(SecuredStore.self) + provider.accountsProvider = r.resolve(AccountsProvider.self) + + let accountService = r.resolve(AccountService.self)! + provider.accountService = accountService + var richProviders = [String: RichMessageProviderWithStatusCheck]() + for case let provider as RichMessageProviderWithStatusCheck in accountService.wallets { + richProviders[type(of: provider).richMessageType] = provider + } + provider.richProviders = richProviders return provider }.inObjectScope(.container) } diff --git a/Adamant/Utilities/AdamantFormattingTools.swift b/Adamant/Utilities/AdamantFormattingTools.swift index a910ace1c..a2e350fec 100644 --- a/Adamant/Utilities/AdamantFormattingTools.swift +++ b/Adamant/Utilities/AdamantFormattingTools.swift @@ -14,51 +14,59 @@ extension String.adamantLocalized.chat { } class AdamantFormattingTools { - static func summaryFor(transaction: BaseTransaction, url: URL) -> String { - return summaryFor(id: transaction.blockId!, sender: transaction.senderId!, recipient: transaction.recipientId!, date: transaction.date! as Date, amount: transaction.amount! as Decimal, fee: transaction.fee! as Decimal, confirmations: transaction.confirmations, blockId: transaction.blockId!, url: url) + static func summaryFor(transaction: Transaction, url: URL?) -> String { + return summaryFor(id: String(transaction.id), + sender: transaction.senderId, + recipient: transaction.recipientId, + date: transaction.date, + amount: transaction.amount, + fee: transaction.fee, + confirmations: String(transaction.confirmations), + blockId: transaction.blockId, + url: url) } + + static func summaryFor(transaction: TransactionDetails, url: URL?) -> String { + return summaryFor(id: transaction.id, + sender: transaction.senderAddress, + recipient: transaction.recipientAddress, + date: transaction.dateValue, + amount: transaction.amountValue, + fee: transaction.feeValue, + confirmations: transaction.confirmationsValue, + blockId: transaction.blockValue, + url: url) + } - static func summaryFor(transaction: Transaction, url: URL) -> String { - return summaryFor(id: String(transaction.id), sender: transaction.senderId, recipient: transaction.recipientId, date: transaction.date, amount: transaction.amount, fee: transaction.fee, confirmations: transaction.confirmations, blockId: transaction.blockId, url: url) - } - - private static func summaryFor(id: String, sender: String, recipient: String, date: Date, amount: Decimal, fee: Decimal, confirmations: Int64, blockId: String, url: URL) -> String { + private static func summaryFor(id: String, sender: String, recipient: String, date: Date?, amount: Decimal, fee: Decimal, confirmations: String?, blockId: String?, url: URL?) -> String { - return """ + var summary = """ Transaction #\(id) Summary Sender: \(sender) Recipient: \(recipient) -Date: \(DateFormatter.localizedString(from: date, dateStyle: .short, timeStyle: .medium)) Amount: \(AdamantUtilities.format(balance: amount)) Fee: \(AdamantUtilities.format(balance: fee)) -Confirmations: \(String(confirmations)) -Block: \(blockId) -URL: \(url) """ - } - - static func formatTransferTransaction(_ transfer: TransferTransaction) -> NSAttributedString { - let balance: String - if let raw = transfer.amount { - balance = AdamantUtilities.format(balance: raw) - } else { - balance = AdamantUtilities.format(balance: Decimal(0.0)) - } - - let sent = String.adamantLocalized.chat.sent - - let attributedString = NSMutableAttributedString(string: "\(sent)\n\(balance)\n\n\(String.adamantLocalized.chat.tapForDetails)") - - let rangeReference = attributedString.string as NSString - let sentRange = rangeReference.range(of: sent) - let amountRange = rangeReference.range(of: balance) - - attributedString.setAttributes([.font: UIFont.systemFont(ofSize: 14)], range: sentRange) - attributedString.setAttributes([.font: UIFont.systemFont(ofSize: 28)], range: amountRange) - - return attributedString + + if let date = date { + summary = summary + "Date: \(DateFormatter.localizedString(from: date, dateStyle: .short, timeStyle: .medium))" + } + + if let confirmations = confirmations { + summary = summary + "Confirmations: \(confirmations)" + } + + if let blockId = blockId { + summary = summary + "\nBlock: \(blockId)" + } + + if let url = url { + summary = summary + "\nURL: \(url)" + } + + return summary } private init() {} diff --git a/Adamant/Utilities/AdamantUtilities.swift b/Adamant/Utilities/AdamantUtilities.swift index 6d9acb41f..5741fa092 100644 --- a/Adamant/Utilities/AdamantUtilities.swift +++ b/Adamant/Utilities/AdamantUtilities.swift @@ -50,13 +50,13 @@ extension AdamantUtilities { return formatter }() - static var currencyFormatterShort: NumberFormatter = { + static func currencyFormatter(currencyCode: String) -> NumberFormatter { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.roundingMode = .floor - formatter.positiveFormat = "#.## \(currencyCode)" + formatter.positiveFormat = "#.######## \(currencyCode)" return formatter - }() + } static func format(balance: Decimal) -> String { return currencyFormatter.string(from: balance as NSNumber)! diff --git a/Adamant/Wallets/Adamant/AdmTransactionDetailsViewController.swift b/Adamant/Wallets/Adamant/AdmTransactionDetailsViewController.swift new file mode 100644 index 000000000..71d6c0b76 --- /dev/null +++ b/Adamant/Wallets/Adamant/AdmTransactionDetailsViewController.swift @@ -0,0 +1,130 @@ +// +// AdmTransactionDetailsViewController.swift +// Adamant +// +// Created by Anokhov Pavel on 01.10.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import Eureka + +class AdmTransactionDetailsViewController: TransactionDetailsViewControllerBase { + + // MARK: - Dependencies + var accountService: AccountService! + var transfersProvider: TransfersProvider! + var router: Router! + + // MARK: - Properties + private let autoupdateInterval: TimeInterval = 5.0 + + var haveChatroom = false + + weak var timer: Timer? + + // MARK: - Lifecycle + + override func viewDidLoad() { + currencySymbol = AdmWalletService.currencySymbol + + super.viewDidLoad() + + if let transfer = transaction as? TransferTransaction, let chatroom = transfer.partner?.chatroom, let transactions = chatroom.transactions { + let messeges = transactions.first (where: { (object) -> Bool in + return !(object is TransferTransaction) + }) + + haveChatroom = messeges != nil + } + + let chatLabel = haveChatroom ? String.adamantLocalized.transactionList.toChat : String.adamantLocalized.transactionList.startChat + + // MARK: Open chat + if let section = form.allSections.first { + let row = LabelRow() { + $0.tag = Rows.openChat.tag + $0.title = chatLabel + $0.cell.imageView?.image = Rows.openChat.image + }.cellSetup { (cell, _) in + cell.selectionStyle = .gray + }.cellUpdate { (cell, _) in + cell.accessoryType = .disclosureIndicator + }.onCellSelection { [weak self] (_, _) in + self?.goToChat() + } + + section.append(row) + } + + startUpdate() + } + + deinit { + stopUpdate() + } + + // MARK: - Overrides + + override func explorerUrl(for transaction: TransactionDetails) -> URL? { + return URL(string: "\(AdamantResources.adamantExplorerAddress)\(transaction.id)") + } + + func goToChat() { + guard let transfer = transaction as? TransferTransaction else { + return + } + + guard let vc = self.router.get(scene: AdamantScene.Chats.chat) as? ChatViewController else { + dialogService.showError(withMessage: "AdmTransactionDetailsViewController: Failed to get ChatViewController", error: nil) + return + } + + guard let chatroom = transfer.chatroom else { + dialogService.showError(withMessage: "AdmTransactionDetailsViewController: Failed to get chatroom for transaction.", error: nil) + return + } + + guard let account = accountService.account else { + dialogService.showError(withMessage: "AdmTransactionDetailsViewController: User not logged.", error: nil) + return + } + + vc.account = account + vc.chatroom = chatroom + vc.hidesBottomBarWhenPushed = true + + if let nav = self.navigationController { + nav.pushViewController(vc, animated: true) + } else { + self.present(vc, animated: true) + } + } + + // MARK: - Autoupdate + + func startUpdate() { + timer?.invalidate() + timer = Timer.scheduledTimer(withTimeInterval: autoupdateInterval, repeats: true) { [weak self] _ in + guard let id = self?.transaction?.id else { + return + } + + self?.transfersProvider.refreshTransfer(id: id) { result in + switch result { + case .success: + DispatchQueue.main.async { + self?.tableView.reloadData() + } + + case .failure: + return + } + } + } + } + + func stopUpdate() { + timer?.invalidate() + } +} diff --git a/Adamant/Wallets/Adamant/AdmTransactionsViewController.swift b/Adamant/Wallets/Adamant/AdmTransactionsViewController.swift new file mode 100644 index 000000000..b1cba322a --- /dev/null +++ b/Adamant/Wallets/Adamant/AdmTransactionsViewController.swift @@ -0,0 +1,297 @@ +// +// AdmTransactionsViewController.swift +// Adamant +// +// Created by Anton Boyarkin on 26/06/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import CoreData + +class AdmTransactionsViewController: TransactionsListViewControllerBase { + // MARK: - Dependencies + var accountService: AccountService! + var transfersProvider: TransfersProvider! + var chatsProvider: ChatsProvider! + var dialogService: DialogService! + var stack: CoreDataStack! + var router: Router! + + // MARK: - Properties + var controller: NSFetchedResultsController? + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + if accountService.account != nil { + reloadData() + } + + currencySymbol = AdmWalletService.currencySymbol + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + markTransfersAsRead() + } + + + // MARK: - Overrides + + override func reloadData() { + controller = transfersProvider.transfersController() + controller!.delegate = self + + do { + try controller?.performFetch() + } catch { + dialogService.showError(withMessage: "Failed to get transactions. Please, report a bug", error: error) + controller = nil + } + + tableView.reloadData() + } + + override func handleRefresh(_ refreshControl: UIRefreshControl) { + self.transfersProvider.update { [weak self] (result) in + guard let result = result else { + DispatchQueue.main.async { + refreshControl.endRefreshing() + } + return + } + + switch result { + case .success: + DispatchQueue.main.async { + refreshControl.endRefreshing() + self?.tableView.reloadData() + } + + case .failure(let error): + DispatchQueue.main.async { + refreshControl.endRefreshing() + } + + self?.dialogService.showRichError(error: error) + } + } + } + + private func markTransfersAsRead() { + guard let stack = stack else { + return + } + + DispatchQueue.global(qos: .utility).async { + let privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + privateContext.parent = 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() + } + } + } + } + + + // MARK: - UITableView + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if let f = controller?.fetchedObjects { + return f.count + } else { + return 0 + } + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let transaction = controller?.object(at: indexPath) else { + tableView.deselectRow(at: indexPath, animated: true) + return + } + + guard let controller = router.get(scene: AdamantScene.Wallets.Adamant.transactionDetails) as? AdmTransactionDetailsViewController else { + return + } + + controller.transaction = transaction + + if let address = accountService.account?.address { + if address == transaction.senderId { + controller.senderName = String.adamantLocalized.transactionDetails.yourAddress + } else { + controller.senderName = transaction.chatroom?.partner?.name + } + + if address == transaction.recipientId { + controller.recipientName = String.adamantLocalized.transactionDetails.yourAddress + } else { + controller.recipientName = transaction.chatroom?.partner?.name + } + } + + navigationController?.pushViewController(controller, animated: true) + } + + func configureCell(_ cell: TransactionTableViewCell, for transaction: TransferTransaction) { + let partnerId = (transaction.isOutgoing ? transaction.recipientId : transaction.senderId) ?? "" + + let amount: Decimal = transaction.amount as Decimal? ?? 0 + + configureCell(cell, + isOutgoing: transaction.isOutgoing, + partnerId: partnerId, + partnerName: transaction.chatroom?.partner?.name, + amount: amount, + date: transaction.date as Date?) + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let transaction = controller?.object(at: indexPath) else { + return UITableViewCell(style: .default, reuseIdentifier: "cell") + } + + let identifier = transaction.chatroom?.partner?.name != nil ? cellIdentifierFull : cellIdentifierCompact + + guard let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) as? TransactionTableViewCell else { + return UITableViewCell(style: .default, reuseIdentifier: "cell") + } + + configureCell(cell, for: transaction) + + cell.accessoryType = .disclosureIndicator + return cell + } + + func tableView(_ tableView: UITableView, editActionsForRowAt: IndexPath) -> [UITableViewRowAction]? { + guard let transaction = controller?.object(at: editActionsForRowAt), let chatroom = transaction.partner?.chatroom, let transactions = chatroom.transactions else { + return nil + } + + let messeges = transactions.first (where: { (object) -> Bool in + return !(object is TransferTransaction) + }) + + let title = (messeges != nil) ? String.adamantLocalized.transactionList.toChat : String.adamantLocalized.transactionList.startChat + + let toChat = UITableViewRowAction(style: .normal, title: title) { action, index in + guard let vc = self.router.get(scene: AdamantScene.Chats.chat) as? ChatViewController else { + // TODO: Log this + return + } + + guard let account = self.accountService.account else { + return + } + + vc.account = account + vc.chatroom = chatroom + vc.hidesBottomBarWhenPushed = true + + if let nav = self.navigationController { + nav.pushViewController(vc, animated: true) + } else { + self.present(vc, animated: true) + } + } + + toChat.backgroundColor = UIColor.adamant.primary + + return [toChat] + } + + func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + return true + } + + @available(iOS 11.0, *) + func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + guard let transaction = controller?.object(at: indexPath), let chatroom = transaction.partner?.chatroom, let transactions = chatroom.transactions else { + return nil + } + + let messeges = transactions.first (where: { (object) -> Bool in + return !(object is TransferTransaction) + }) + + let title = (messeges != nil) ? String.adamantLocalized.transactionList.toChat : String.adamantLocalized.transactionList.startChat + + let toChat = UIContextualAction(style: .normal, title: title, handler: { (ac:UIContextualAction, view:UIView, success:(Bool) -> Void) in + guard let vc = self.router.get(scene: AdamantScene.Chats.chat) as? ChatViewController else { + // TODO: Log this + return + } + + guard let account = self.accountService.account else { + return + } + + vc.account = account + vc.chatroom = chatroom + vc.hidesBottomBarWhenPushed = true + + if let nav = self.navigationController { + nav.pushViewController(vc, animated: true) + } else { + self.present(vc, animated: true) + } + }) + + toChat.image = (messeges != nil) ? #imageLiteral(resourceName: "chats_tab") : #imageLiteral(resourceName: "Chat") + toChat.backgroundColor = UIColor.adamant.primary + return UISwipeActionsConfiguration(actions: [toChat]) + } +} + +// MARK: - NSFetchedResultsControllerDelegate +extension AdmTransactionsViewController: NSFetchedResultsControllerDelegate { + func controllerWillChangeContent(_ controller: NSFetchedResultsController) { + tableView.beginUpdates() + } + + func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + tableView.endUpdates() + } + + func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { + switch type { + case .insert: + if let newIndexPath = newIndexPath { + tableView.insertRows(at: [newIndexPath], with: .automatic) + + if let transaction = anObject as? TransferTransaction { + transaction.isUnread = false + } + } + + 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? TransactionTableViewCell, + let transaction = anObject as? TransferTransaction { + configureCell(cell, for: transaction) + } + + case .move: + if let at = indexPath, let to = newIndexPath { + tableView.moveRow(at: at, to: to) + } + } + } +} diff --git a/Adamant/Wallets/Adamant/AdmTransferViewController.swift b/Adamant/Wallets/Adamant/AdmTransferViewController.swift new file mode 100644 index 000000000..9476c574c --- /dev/null +++ b/Adamant/Wallets/Adamant/AdmTransferViewController.swift @@ -0,0 +1,166 @@ +// +// AdmTransferViewController.swift +// Adamant +// +// Created by Anokhov Pavel on 18.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import Eureka + +class AdmTransferViewController: TransferViewControllerBase { + // MARK: Properties + + override var balanceFormatter: NumberFormatter { + return AdamantUtilities.currencyFormatter + } + + private var skipValueChange: Bool = false + + static let invalidCharactersSet = CharacterSet.decimalDigits.inverted + + // MARK: Sending + + override func sendFunds() { + guard let service = service as? AdmWalletService, let recipient = recipient, let amount = amount else { + return + } + + dialogService.showProgress(withMessage: String.adamantLocalized.transfer.transferProcessingMessage, userInteractionEnable: false) + + service.sendMoney(recipient: recipient, amount: amount, comments: "") { [weak self] result in + switch result { + case .success: + service.update() + + guard let vc = self else { + break + } + + vc.dialogService?.showSuccess(withMessage: String.adamantLocalized.transfer.transferSuccess) + vc.delegate?.transferViewControllerDidFinishTransfer(vc) + + case .failure(let error): + guard let dialogService = self?.dialogService else { + break + } + + dialogService.dismissProgress() + dialogService.showRichError(error: error) + } + } + } + + + // MARK: Overrides + + private var _recipient: String? + + override var recipient: String? { + set { + if let recipient = newValue, let first = recipient.first, first != "U" { + _recipient = "U\(recipient)" + } else { + _recipient = newValue + } + + if let row: TextRow = form.rowBy(tag: BaseRows.address.tag) { + row.value = _recipient + row.updateCell() + } + } + get { + return _recipient + } + } + + override func recipientRow() -> BaseRow { + let row = TextRow() { + $0.tag = BaseRows.address.tag + $0.cell.textField.placeholder = String.adamantLocalized.newChat.addressPlaceholder + $0.cell.textField.keyboardType = .numberPad + + if let recipient = recipient { + 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 + $0.cell.textField.leftView = view + $0.cell.textField.leftViewMode = .always + + if recipientIsReadonly { + $0.disabled = true + prefix.isEnabled = false + } + }.cellUpdate { (cell, row) in + if let text = cell.textField.text { + cell.textField.text = text.components(separatedBy: AdmTransferViewController.invalidCharactersSet).joined() + } + }.onChange { [weak self] row in + if let skip = self?.skipValueChange, skip { + self?.skipValueChange = false + return + } + + if let text = row.value { + let trimmed = text.components(separatedBy: AdmTransferViewController.invalidCharactersSet).joined() + + if text != trimmed { + self?.skipValueChange = true + + DispatchQueue.main.async { + row.value = trimmed + row.updateCell() + } + } + } + + self?.validateForm() + } + + return row + } + + override func validateRecipient(_ address: String) -> Bool { + let fixedAddress: String + if let first = address.first, first != "U" { + fixedAddress = "U\(address)" + } else { + fixedAddress = address + } + + switch AdamantUtilities.validateAdamantAddress(address: fixedAddress) { + case .valid: + return true + + case .system, .invalid: + return false + } + } + + override func handleRawAddress(_ address: String) -> Bool { + guard let uri = AdamantUriTools.decode(uri: address) else { + return false + } + + switch uri { + case .address(let address, _): + if let row: TextRow = form.rowBy(tag: BaseRows.address.tag) { + row.value = address + row.updateCell() + } + + return true + + default: + return false + } + } +} diff --git a/Adamant/Wallets/Adamant/AdmWallet.swift b/Adamant/Wallets/Adamant/AdmWallet.swift new file mode 100644 index 000000000..5bdc7d416 --- /dev/null +++ b/Adamant/Wallets/Adamant/AdmWallet.swift @@ -0,0 +1,19 @@ +// +// AdmWallet.swift +// Adamant +// +// Created by Anokhov Pavel on 03.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation + +class AdmWallet: WalletAccount { + let address: String + var balance: Decimal = 0 + var notifications: Int = 0 + + init(address: String) { + self.address = address + } +} diff --git a/Adamant/Wallets/Adamant/AdmWalletRoutes.swift b/Adamant/Wallets/Adamant/AdmWalletRoutes.swift new file mode 100644 index 000000000..575cc136f --- /dev/null +++ b/Adamant/Wallets/Adamant/AdmWalletRoutes.swift @@ -0,0 +1,51 @@ +// +// AdmWalletRoutes.swift +// Adamant +// +// Created by Anokhov Pavel on 28.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation + +extension AdamantScene.Wallets { + struct Adamant { + /// Wallet preview + static let wallet = AdamantScene(identifier: "AdmWalletViewController") { r in + let c = AdmWalletViewController(nibName: "WalletViewControllerBase", bundle: nil) + c.dialogService = r.resolve(DialogService.self) + return c + } + + /// Send money + static let transfer = AdamantScene(identifier: "AdmTransferViewController") { r in + let c = AdmTransferViewController() + c.dialogService = r.resolve(DialogService.self) + c.accountService = r.resolve(AccountService.self) + return c + } + + /// Transactions list + static let transactionsList = AdamantScene(identifier: "AdmTransactionsViewController", factory: { r in + let c = AdmTransactionsViewController(nibName: "TransactionsListViewControllerBase", bundle: nil) + c.accountService = r.resolve(AccountService.self) + c.transfersProvider = r.resolve(TransfersProvider.self) + c.dialogService = r.resolve(DialogService.self) + c.router = r.resolve(Router.self) + c.stack = r.resolve(CoreDataStack.self) + return c + }) + + /// Adamant transaction details + static let transactionDetails = AdamantScene(identifier: "TransactionDetailsViewController", factory: { r in + let c = AdmTransactionDetailsViewController() + c.accountService = r.resolve(AccountService.self) + c.dialogService = r.resolve(DialogService.self) + c.transfersProvider = r.resolve(TransfersProvider.self) + c.router = r.resolve(Router.self) + return c + }) + + private init() {} + } +} diff --git a/Adamant/Wallets/Adamant/AdmWalletService+RichMessageProvider.swift b/Adamant/Wallets/Adamant/AdmWalletService+RichMessageProvider.swift new file mode 100644 index 000000000..e11006da4 --- /dev/null +++ b/Adamant/Wallets/Adamant/AdmWalletService+RichMessageProvider.swift @@ -0,0 +1,110 @@ +// +// AdmWalletService+RichMessageProvider.swift +// Adamant +// +// Created by Anokhov Pavel on 27.09.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import MessageKit + +extension AdmWalletService: RichMessageProvider { + + // MARK: Events + + /// Not supported yet + func richMessageTapped(for transaction: RichMessageTransaction, at indexPath: IndexPath, in chat: ChatViewController) { + return + } + + func richMessageTapped(for transaction: TransferTransaction, at indexPath: IndexPath, in chat: ChatViewController) { + guard let controller = router.get(scene: AdamantScene.Wallets.Adamant.transactionDetails) as? TransactionDetailsViewControllerBase else { + fatalError("Can't get TransactionDetails scene") + } + + controller.transaction = transaction + + if let address = accountService.account?.address { + if address == transaction.senderId { + controller.senderName = String.adamantLocalized.transactionDetails.yourAddress + } else { + controller.senderName = transaction.chatroom?.partner?.name + } + + if address == transaction.recipientId { + controller.recipientName = String.adamantLocalized.transactionDetails.yourAddress + } else { + controller.recipientName = transaction.chatroom?.partner?.name + } + } + + if let nav = chat.navigationController { + nav.pushViewController(controller, animated: true) + } else { + chat.present(controller, animated: true, completion: nil) + } + } + + // MARK: Cells + + func cellSizeCalculator(for messagesCollectionViewFlowLayout: MessagesCollectionViewFlowLayout) -> CellSizeCalculator { + let calculator = TransferMessageSizeCalculator(layout: messagesCollectionViewFlowLayout) + calculator.font = UIFont.systemFont(ofSize: 24) + return calculator + } + + func cell(for message: MessageType, isFromCurrentSender: Bool, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell { + guard case .custom(let raw) = message.kind, let richMessage = raw as? RichMessageTransfer else { + fatalError("ADM service tried to render wrong message kind: \(message.kind)") + } + + let cellIdentifier = isFromCurrentSender ? cellIdentifierSent : cellIdentifierReceived + guard let cell = messagesCollectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as? TransferCollectionViewCell else { + fatalError("Can't dequeue \(cellIdentifier) cell") + } + + cell.currencyLogoImageView.image = AdmWalletService.currencyLogo + cell.currencySymbolLabel.text = AdmWalletService.currencySymbol + + cell.amountLabel.text = richMessage.amount + cell.dateLabel.text = message.sentDate.humanizedDateTime(withWeekday: false) + cell.transactionStatus = nil + + if cell.isAlignedRight != isFromCurrentSender { + cell.isAlignedRight = isFromCurrentSender + } + + return cell + } + + // MARK: Short description + private static var formatter: NumberFormatter = { + return AdamantBalanceFormat.currencyFormatter(for: .full, currencySymbol: currencySymbol) + }() + + func shortDescription(for transaction: RichMessageTransaction) -> String { + guard let balance = transaction.amount as Decimal? else { + return "" + } + + return 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 String.localizedStringWithFormat(String.adamantLocalized.chatList.sentMessagePrefix, " ⬅️ \(AdmWalletService.formatter.string(fromDecimal: balance)!)") + } else { + return "➡️ \(AdmWalletService.formatter.string(fromDecimal: balance)!)" + } + } +} diff --git a/Adamant/Wallets/Adamant/AdmWalletService+Send.swift b/Adamant/Wallets/Adamant/AdmWalletService+Send.swift new file mode 100644 index 000000000..ac5117fe0 --- /dev/null +++ b/Adamant/Wallets/Adamant/AdmWalletService+Send.swift @@ -0,0 +1,58 @@ +// +// AdmWalletService+Send.swift +// Adamant +// +// Created by Anokhov Pavel on 21.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit + +extension AdmWalletService: WalletServiceSimpleSend { + /// Transaction ID + typealias T = Int + + func transferViewController() -> UIViewController { + guard let vc = router.get(scene: AdamantScene.Wallets.Adamant.transfer) as? AdmTransferViewController else { + fatalError("Can't get AdmTransferViewController") + } + + vc.service = self + return vc + } + + + /// Comments not implemented + func sendMoney(recipient: String, amount: Decimal, comments: String, completion: @escaping (WalletServiceSimpleResult) -> Void) { + transfersProvider.transferFunds(toAddress: recipient, amount: amount) { result in + switch result { + case .success: + completion(.success) + + case .failure(let error): + completion(.failure(error: error.asWalletServiceError())) + } + } + } +} + +extension TransfersProviderError { + func asWalletServiceError() -> WalletServiceError { + switch self { + case .notLogged: + return .notLogged + case .serverError: + return .remoteServiceError(message: self.message) + case .accountNotFound: + return .accountNotFound + case .transactionNotFound: + return .internalError(message: self.message, error: nil) + case .networkError: + return .networkError + case .dependencyError: + return .internalError(message: self.message, error: nil) + case .internalError(let message, let error): + return .internalError(message: message, error: error) + } + } +} diff --git a/Adamant/Wallets/Adamant/AdmWalletService.swift b/Adamant/Wallets/Adamant/AdmWalletService.swift new file mode 100644 index 000000000..25bb70c70 --- /dev/null +++ b/Adamant/Wallets/Adamant/AdmWalletService.swift @@ -0,0 +1,165 @@ +// +// AdmWalletService.swift +// Adamant +// +// Created by Anokhov Pavel on 03.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import UIKit +import Swinject +import CoreData + +class AdmWalletService: NSObject, WalletService { + // MARK: - Constants + let addressRegex = try! NSRegularExpression(pattern: "^U([0-9]{6,20})$") + + let transactionFee: Decimal = 0.5 + static var currencySymbol = "ADM" + static var currencyLogo = #imageLiteral(resourceName: "wallet_adm") + + + // MARK: - Dependencies + weak var accountService: AccountService! + var apiService: ApiService! + var transfersProvider: TransfersProvider! + var router: Router! + + + // MARK: - Notifications + let walletUpdatedNotification = Notification.Name("adamant.admWallet.updated") + let serviceEnabledChanged = Notification.Name("adamant.admWallet.enabledChanged") + let transactionFeeUpdated = Notification.Name("adamant.admWallet.feeUpdated") + + // MARK: RichMessageProvider properties + static let richMessageType = "adm_transaction" // not used + let cellIdentifierSent = "admTransferSent" + let cellIdentifierReceived = "admTransferReceived" + var cellSource: CellSource? = CellSource.nib(nib: UINib(nibName: "TransferCollectionViewCell", bundle: nil)) + + // MARK: - Properties + let enabled: Bool = true + + var walletViewController: WalletViewController { + guard let vc = router.get(scene: AdamantScene.Wallets.Adamant.wallet) as? AdmWalletViewController else { + fatalError("Can't get AdmWalletViewController") + } + + vc.service = self + return vc + } + + private var transfersController: NSFetchedResultsController? + + // MARK: - State + private (set) var state: WalletServiceState = .notInitiated + private (set) var wallet: WalletAccount? = nil + + + // MARK: - Logic + override init() { + super.init() + + // MARK: Notifications + NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userLoggedIn, object: nil, queue: nil) { [weak self] _ in + self?.update() + } + + NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.accountDataUpdated, object: nil, queue: nil) { [weak self] _ in + self?.update() + } + + NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userLoggedOut, object: nil, queue: nil) { [weak self] _ in + self?.wallet = nil + } + } + + func update() { + guard let account = accountService.account else { + wallet = nil + return + } + + let notify: Bool + if let wallet = wallet as? AdmWallet { + if wallet.balance != account.balance { + wallet.balance = account.balance + notify = true + } else { + notify = false + } + } else { + let wallet = AdmWallet(address: account.address) + wallet.balance = account.balance + + self.wallet = wallet + notify = true + } + + if notify, let wallet = wallet { + postUpdateNotification(with: wallet) + } + } + + + // MARK: - Tools + func validate(address: String) -> AddressValidationResult { + guard !AdamantContacts.systemAddresses.contains(address) else { + return .system + } + + return addressRegex.perfectMatch(with: address) ? .valid : .invalid + } + + private func postUpdateNotification(with wallet: WalletAccount) { + NotificationCenter.default.post(name: walletUpdatedNotification, object: self, userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet]) + } + + func getWalletAddress(byAdamantAddress address: String, completion: @escaping (WalletServiceResult) -> Void) { + completion(.success(result: address)) + } +} + +extension AdmWalletService: WalletServiceWithTransfers { + func transferListViewController() -> UIViewController { + return router.get(scene: AdamantScene.Wallets.Adamant.transactionsList) + } +} + + +// MARK: - NSFetchedResultsControllerDelegate +extension AdmWalletService: NSFetchedResultsControllerDelegate { + 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) + } + } +} + + +// MARK: - Dependencies +extension AdmWalletService: SwinjectDependentService { + func injectDependencies(from container: Container) { + accountService = container.resolve(AccountService.self) + apiService = container.resolve(ApiService.self) + transfersProvider = container.resolve(TransfersProvider.self) + router = container.resolve(Router.self) + + let controller = transfersProvider.unreadTransfersController() + + do { + try controller.performFetch() + } catch { + print("AdmWalletService: Error performing fetch: \(error)") + } + + controller.delegate = self + transfersController = controller + } +} diff --git a/Adamant/Wallets/Adamant/AdmWalletViewController.swift b/Adamant/Wallets/Adamant/AdmWalletViewController.swift new file mode 100644 index 000000000..c6d152fc8 --- /dev/null +++ b/Adamant/Wallets/Adamant/AdmWalletViewController.swift @@ -0,0 +1,23 @@ +// +// AdmWalletViewController.swift +// Adamant +// +// Created by Anokhov Pavel on 12.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit + +extension String.adamantLocalized.wallets { + static let adamant = NSLocalizedString("AccountTab.Wallets.adamant_wallet", comment: "Account tab: Adamant wallet") +} + +class AdmWalletViewController: WalletViewControllerBase { + // MARK: Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + walletTitleLabel.text = String.adamantLocalized.wallets.adamant + } +} diff --git a/Adamant/Wallets/Ethereum/EthTransactionDetailsViewController.swift b/Adamant/Wallets/Ethereum/EthTransactionDetailsViewController.swift new file mode 100644 index 000000000..92f828fea --- /dev/null +++ b/Adamant/Wallets/Ethereum/EthTransactionDetailsViewController.swift @@ -0,0 +1,23 @@ +// +// EthTransactionDetailsViewController.swift +// Adamant +// +// Created by Anokhov Pavel on 05.10.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit + +class EthTransactionDetailsViewController: TransactionDetailsViewControllerBase { + // MARK: - Overrides + + override func viewDidLoad() { + currencySymbol = EthWalletService.currencySymbol + + super.viewDidLoad() + } + + override func explorerUrl(for transaction: TransactionDetails) -> URL? { + return URL(string: "\(AdamantResources.ethereumExplorerAddress)\(transaction.id)") + } +} diff --git a/Adamant/Wallets/Ethereum/EthTransactionsViewController.swift b/Adamant/Wallets/Ethereum/EthTransactionsViewController.swift new file mode 100644 index 000000000..a3dc3573d --- /dev/null +++ b/Adamant/Wallets/Ethereum/EthTransactionsViewController.swift @@ -0,0 +1,128 @@ +// +// EthTransactionsViewController.swift +// Adamant +// +// Created by Anton Boyarkin on 25/06/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import web3swift + +class EthTransactionsViewController: TransactionsListViewControllerBase { + + // MARK: - Dependencies + var ethWalletService: EthWalletService! { + didSet { + ethAddress = ethWalletService.wallet?.address ?? "" + } + } + var dialogService: DialogService! + var router: Router! + + // MARK: - Properties + var transactions: [EthTransaction] = [] + private var ethAddress: String = "" + + override func viewDidLoad() { + super.viewDidLoad() + + self.refreshControl.beginRefreshing() + + currencySymbol = EthWalletService.currencySymbol + + handleRefresh(self.refreshControl) + } + + + // MARK: - Overrides + + override func handleRefresh(_ refreshControl: UIRefreshControl) { + guard let address = ethWalletService.wallet?.address else { + transactions = [] + return + } + + ethWalletService.getTransactionsHistory(address: address) { [weak self] result in + guard let vc = self else { + return + } + + switch result { + case .success(let transactions): + vc.transactions = transactions + + case .failure(let error): + vc.transactions = [] + vc.dialogService.showRichError(error: error) + } + + DispatchQueue.main.async { + vc.tableView.reloadData() + vc.refreshControl.endRefreshing() + } + } + } + + override func reloadData() { + if Thread.isMainThread { + refreshControl.beginRefreshing() + } else { + DispatchQueue.main.async { [weak self] in + self?.refreshControl.beginRefreshing() + } + } + + handleRefresh(refreshControl) + } + + // MARK: - UITableView + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return transactions.count + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + + let transaction = transactions[indexPath.row] + + guard let vc = router.get(scene: AdamantScene.Wallets.Ethereum.transactionDetails) as? EthTransactionDetailsViewController else { + return + } + + vc.transaction = transaction + navigationController?.pushViewController(vc, animated: true) + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifierCompact, for: indexPath) as? TransactionTableViewCell else { + return UITableViewCell(style: .default, reuseIdentifier: "cell") + } + + let transaction = transactions[indexPath.row] + cell.accessoryType = .disclosureIndicator + configureCell(cell, for: transaction) + return cell + } + + func configureCell(_ cell: TransactionTableViewCell, for transaction: EthTransaction) { + let outgoing = isOutgoing(transaction) + let partnerId = outgoing ? transaction.to : transaction.from + + configureCell(cell, + isOutgoing: outgoing, + partnerId: partnerId, + partnerName: nil, + amount: transaction.value, + date: transaction.date) + } +} + + +// MARK: - Tools +extension EthTransactionsViewController { + private func isOutgoing(_ transaction: EthTransaction) -> Bool { + return transaction.from.lowercased() == ethAddress.lowercased() + } +} diff --git a/Adamant/Wallets/Ethereum/EthTransferViewController.swift b/Adamant/Wallets/Ethereum/EthTransferViewController.swift new file mode 100644 index 000000000..fb8574d31 --- /dev/null +++ b/Adamant/Wallets/Ethereum/EthTransferViewController.swift @@ -0,0 +1,230 @@ +// +// EthTransferViewController.swift +// Adamant +// +// Created by Anokhov Pavel on 23.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import Eureka + +class EthTransferViewController: TransferViewControllerBase { + + // MARK: Dependencies + + var chatsProvider: ChatsProvider! + + + // MARK: Properties + + override var balanceFormatter: NumberFormatter { + if let service = service { + return AdamantBalanceFormat.currencyFormatter(for: .full, currencySymbol: type(of: service).currencySymbol) + } else { + return AdamantBalanceFormat.currencyFormatterFull + } + } + + private var skipValueChange: Bool = false + + static let invalidCharacters: CharacterSet = { + CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789").inverted + }() + + + // MARK: Send + + override func sendFunds() { + let comments = "" // TODO: + + guard let service = service as? EthWalletService, let recipient = recipient, let amount = amount else { + return + } + + guard let dialogService = dialogService else { + return + } + + dialogService.showProgress(withMessage: String.adamantLocalized.transfer.transferProcessingMessage, userInteractionEnable: false) + + service.createTransaction(recipient: recipient, amount: amount, comments: comments) { [weak self] result in + switch result { + case .success(let transaction): + // MARK: 1. Send adm report + if let reportRecipient = self?.admReportRecipient, let hash = transaction.txhash { + let payload = RichMessageTransfer(type: EthWalletService.richMessageType, amount: amount, hash: hash, comments: comments) + let message = AdamantMessage.richMessage(payload: payload) + + self?.chatsProvider.sendMessage(message, recipientId: reportRecipient) { result in + if case .failure(let error) = result { + self?.dialogService.showRichError(error: error) + } + } + } + + // MARK: 2. Send eth transaction + service.sendTransaction(transaction) { result in + switch result { + case .success(_): + service.update() + + guard let vc = self else { + break + } + + vc.dialogService.showSuccess(withMessage: String.adamantLocalized.transfer.transferSuccess) + vc.delegate?.transferViewControllerDidFinishTransfer(vc) + + case .failure(let error): + self?.dialogService.showRichError(error: error) + } + } + + case .failure(let error): + guard let dialogService = self?.dialogService else { + break + } + + dialogService.dismissProgress() + dialogService.showRichError(error: error) + } + } + } + + + // MARK: Overrides + + private var _recipient: String? + + override var recipient: String? { + set { + if let recipient = newValue, let first = recipient.first, first != "0" { + _recipient = "0x\(recipient)" + } else { + _recipient = newValue + } + + if let row: TextRow = form.rowBy(tag: BaseRows.address.tag) { + row.value = _recipient + row.updateCell() + } + } + get { + return _recipient + } + } + + override func validateRecipient(_ address: String) -> Bool { + guard let service = service else { + return false + } + + let fixedAddress: String + if let first = address.first, first != "0" { + fixedAddress = "0x\(address)" + } else { + fixedAddress = address + } + + switch service.validate(address: fixedAddress) { + case .valid: + return true + + case .invalid, .system: + return true + } + } + + override func recipientRow() -> BaseRow { + let row = TextRow() { + $0.tag = BaseRows.address.tag + $0.cell.textField.placeholder = String.adamantLocalized.newChat.addressPlaceholder + $0.cell.textField.keyboardType = UIKeyboardType.namePhonePad + + if let recipient = recipient { + let trimmed = recipient.components(separatedBy: EthTransferViewController.invalidCharacters).joined() + $0.value = trimmed + } + + let prefix = UILabel() + prefix.text = "0x" + prefix.sizeToFit() + let view = UIView() + view.addSubview(prefix) + view.frame = prefix.frame + $0.cell.textField.leftView = view + $0.cell.textField.leftViewMode = .always + + if recipientIsReadonly { + $0.disabled = true + prefix.isEnabled = false + } + }.cellUpdate { (cell, row) in + if let text = cell.textField.text { + cell.textField.text = text.components(separatedBy: EthTransferViewController.invalidCharacters).joined() + } + }.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: EthTransferViewController.invalidCharacters).joined() + if trimmed.starts(with: "0x") { + let i = trimmed.index(trimmed.startIndex, offsetBy: 2) + trimmed = String(trimmed[i...]) + } + + if text != trimmed { + self?.skipValueChange = true + + DispatchQueue.main.async { + row.value = trimmed + row.updateCell() + } + } + } + + self?.validateForm() + } + + return row + } + + override func handleRawAddress(_ address: String) -> Bool { + guard let service = service else { + return false + } + + switch service.validate(address: address) { + case .valid: + if let row: TextRow = form.rowBy(tag: BaseRows.address.tag) { + row.value = address + row.updateCell() + } + + return true + + default: + return false + } + } + + override func reportTransferTo(admAddress: String, transferRecipient: String, amount: Decimal, comments: String, hash: String) { + let payload = RichMessageTransfer(type: EthWalletService.richMessageType, amount: amount, hash: hash, comments: comments) + + let message = AdamantMessage.richMessage(payload: payload) + + chatsProvider.sendMessage(message, recipientId: admAddress) { [weak self] result in + switch result { + case .success: + break + + case .failure(let error): + self?.dialogService.showRichError(error: error) + } + } + } +} diff --git a/Adamant/Wallets/Ethereum/EthWallet.swift b/Adamant/Wallets/Ethereum/EthWallet.swift new file mode 100644 index 000000000..3770f8d2e --- /dev/null +++ b/Adamant/Wallets/Ethereum/EthWallet.swift @@ -0,0 +1,25 @@ +// +// EthWallet.swift +// Adamant +// +// Created by Anokhov Pavel on 03.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import web3swift + +class EthWallet: WalletAccount { + let address: String + let ethAddress: EthereumAddress + let keystore: BIP32Keystore + + var balance: Decimal = 0 + var notifications: Int = 0 + + init(address: String, ethAddress: EthereumAddress, keystore: BIP32Keystore) { + self.address = address + self.ethAddress = ethAddress + self.keystore = keystore + } +} diff --git a/Adamant/Wallets/Ethereum/EthWalletRoutes.swift b/Adamant/Wallets/Ethereum/EthWalletRoutes.swift new file mode 100644 index 000000000..aa35c96ba --- /dev/null +++ b/Adamant/Wallets/Ethereum/EthWalletRoutes.swift @@ -0,0 +1,45 @@ +// +// EthWalletRoutes.swift +// Adamant +// +// Created by Anokhov Pavel on 28.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation + +extension AdamantScene.Wallets { + struct Ethereum { + + /// Wallet preview + static let wallet = AdamantScene(identifier: "EthWalletViewController") { r in + let c = EthWalletViewController(nibName: "WalletViewControllerBase", bundle: nil) + c.dialogService = r.resolve(DialogService.self) + return c + } + + /// Send money + static let transfer = AdamantScene(identifier: "EthTransferViewController") { r in + let c = EthTransferViewController() + c.dialogService = r.resolve(DialogService.self) + c.chatsProvider = r.resolve(ChatsProvider.self) + c.accountService = r.resolve(AccountService.self) + return c + } + + /// List of Ethereum transactions + static let transactionsList = AdamantScene(identifier: "EthTransactionsViewController") { r in + let c = EthTransactionsViewController(nibName: "TransactionsListViewControllerBase", bundle: nil) + c.dialogService = r.resolve(DialogService.self) + c.router = r.resolve(Router.self) + return c + } + + /// Ethereum transaction details + static let transactionDetails = AdamantScene(identifier: "TransactionDetailsViewControllerBase") { r in + let c = EthTransactionDetailsViewController() + c.dialogService = r.resolve(DialogService.self) + return c + } + } +} diff --git a/Adamant/Wallets/Ethereum/EthWalletService+RichMessageProvider.swift b/Adamant/Wallets/Ethereum/EthWalletService+RichMessageProvider.swift new file mode 100644 index 000000000..9838257e5 --- /dev/null +++ b/Adamant/Wallets/Ethereum/EthWalletService+RichMessageProvider.swift @@ -0,0 +1,97 @@ +// +// EthWalletService+RichMessageProvider.swift +// Adamant +// +// Created by Anokhov Pavel on 08.09.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import MessageKit + +extension EthWalletService: RichMessageProvider { + + // MARK: Events + + func richMessageTapped(for transaction: RichMessageTransaction, at indexPath: IndexPath, in chat: ChatViewController) { + guard let richContent = transaction.richContent, let hash = richContent[RichContentKeys.transfer.hash] else { + return + } + + guard let dialogService = dialogService else { + return + } + + dialogService.showProgress(withMessage: nil, userInteractionEnable: false) + + getTransaction(by: hash) { [weak self] result in + dialogService.dismissProgress() + + switch result { + case .success(let transaction): + guard let vc = self?.router.get(scene: AdamantScene.Wallets.Ethereum.transactionDetails) as? EthTransactionDetailsViewController else { + return + } + + vc.transaction = transaction + DispatchQueue.main.async { + chat.navigationController?.pushViewController(vc, animated: true) + } + + case .failure(let error): + self?.dialogService.showRichError(error: error) + } + } + + } + + // MARK: Cells + + func cellSizeCalculator(for messagesCollectionViewFlowLayout: MessagesCollectionViewFlowLayout) -> CellSizeCalculator { + let calculator = TransferMessageSizeCalculator(layout: messagesCollectionViewFlowLayout) + calculator.font = UIFont.systemFont(ofSize: 24) + return calculator + } + + func cell(for message: MessageType, isFromCurrentSender: Bool, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell { + guard case .custom(let raw) = message.kind, let transfer = raw as? RichMessageTransfer else { + fatalError("ETH service tried to render wrong message kind: \(message.kind)") + } + + let cellIdentifier = isFromCurrentSender ? cellIdentifierSent : cellIdentifierReceived + guard let cell = messagesCollectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as? TransferCollectionViewCell else { + fatalError("Can't dequeue \(cellIdentifier) cell") + } + + cell.currencyLogoImageView.image = EthWalletService.currencyLogo + cell.currencySymbolLabel.text = EthWalletService.currencySymbol + + cell.amountLabel.text = transfer.amount + cell.dateLabel.text = message.sentDate.humanizedDateTime(withWeekday: false) + cell.transactionStatus = (message as? RichMessageTransaction)?.transactionStatus + + if cell.isAlignedRight != isFromCurrentSender { + cell.isAlignedRight = isFromCurrentSender + } + + return cell + } + + // MARK: Short description + + private static var formatter: NumberFormatter = { + return AdamantBalanceFormat.currencyFormatter(for: .full, currencySymbol: currencySymbol) + }() + + func shortDescription(for transaction: RichMessageTransaction) -> String { + guard let amount = transaction.richContent?[RichContentKeys.transfer.amount] else { + return "" + } + + if transaction.isOutgoing { + return String.localizedStringWithFormat(String.adamantLocalized.chatList.sentMessagePrefix, " ⬅️ \(amount) \(EthWalletService.currencySymbol)") + } else { + return "➡️ \(amount) \(EthWalletService.currencySymbol)" + } + } +} diff --git a/Adamant/Wallets/Ethereum/EthWalletService+RichMessageProviderWithStatusCheck.swift b/Adamant/Wallets/Ethereum/EthWalletService+RichMessageProviderWithStatusCheck.swift new file mode 100644 index 000000000..017c66591 --- /dev/null +++ b/Adamant/Wallets/Ethereum/EthWalletService+RichMessageProviderWithStatusCheck.swift @@ -0,0 +1,37 @@ +// +// EthWalletService+RichMessageProviderWithStatusCheck.swift +// Adamant +// +// Created by Anokhov Pavel on 06.10.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import web3swift + +extension EthWalletService: RichMessageProviderWithStatusCheck { + func statusForTransactionBy(hash: String, completion: @escaping (WalletServiceResult) -> Void) { + switch web3.eth.getTransactionReceipt(hash) { + case .success(let receipt): + completion(.success(result: receipt.status.asTransactionStatus())) + + case .failure(let error): + completion(.failure(error: error.asWalletServiceError())) + } + } +} + +extension TransactionReceipt.TXStatus { + func asTransactionStatus() -> TransactionStatus { + switch self { + case .ok: + return .success + + case .failed: + return .failed + + case .notYetProcessed: + return .pending + } + } +} diff --git a/Adamant/Wallets/Ethereum/EthWalletService+Send.swift b/Adamant/Wallets/Ethereum/EthWalletService+Send.swift new file mode 100644 index 000000000..3530829cd --- /dev/null +++ b/Adamant/Wallets/Ethereum/EthWalletService+Send.swift @@ -0,0 +1,115 @@ +// +// EthWalletService+Send.swift +// Adamant +// +// Created by Anokhov Pavel on 21.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import web3swift +import struct BigInt.BigUInt +import PromiseKit + +extension EthereumTransaction: RawTransaction { + var txHash: String? { + return txhash + } +} + +extension EthWalletService: WalletServiceTwoStepSend { + typealias T = EthereumTransaction + + func transferViewController() -> UIViewController { + guard let vc = router.get(scene: AdamantScene.Wallets.Ethereum.transfer) as? EthTransferViewController else { + fatalError("Can't get EthTransferViewController") + } + + vc.service = self + return vc + } + + + // MARK: Create & Send + func createTransaction(recipient: String, amount: Decimal, comments: String, completion: @escaping (WalletServiceResult) -> Void) { + // MARK: 1. Prepare + guard let ethWallet = ethWallet else { + completion(.failure(error: .notLogged)) + return + } + + guard let ethRecipient = EthereumAddress(recipient) else { + completion(.failure(error: .accountNotFound)) + return + } + + guard let bigUIntAmount = Web3.Utils.parseToBigUInt(String(format: "%.18f", amount.doubleValue), units: .eth) else { + completion(.failure(error: .invalidAmount(amount))) + return + } + + guard let keystoreManager = web3.provider.attachedKeystoreManager else { + completion(.failure(error: .internalError(message: "Failed to get web3.provider.KeystoreManager", error: nil))) + return + } + + // MARK: Go background + defaultDispatchQueue.async { + // MARK: 2. Create contract + + var options = Web3Options.defaultOptions() + options.from = ethWallet.ethAddress + options.value = bigUIntAmount + + guard let contract = self.web3.contract(Web3.Utils.coldWalletABI, at: ethRecipient) else { + completion(.failure(error: .internalError(message: "ETH Wallet: Send - contract loading error", error: nil))) + return + } + + guard let estimatedGas = contract.method(options: options)?.estimateGas(options: nil).value else { + completion(.failure(error: .internalError(message: "ETH Wallet: Send - retrieving estimated gas error", error: nil))) + return + } + + options.gasLimit = estimatedGas + + guard let gasPrice = self.web3.eth.getGasPrice().value else { + completion(.failure(error: .internalError(message: "ETH Wallet: Send - retrieving gas price error", error: nil))) + return + } + + options.gasPrice = gasPrice + + guard let intermediate = contract.method(options: options) else { + completion(.failure(error: .internalError(message: "ETH Wallet: Send - create transaction issue", error: nil))) + return + } + + do { + let transaction = try intermediate.assemblePromise().then { transaction throws -> Promise in + var trs = transaction + try Web3Signer.signTX(transaction: &trs, keystore: keystoreManager, account: ethWallet.ethAddress, password: "") + let promise = Promise.pending() + promise.resolver.fulfill(trs) + return promise.promise + }.wait() + + completion(.success(result: transaction)) + } catch { + completion(.failure(error: WalletServiceError.internalError(message: "Transaction sign error", error: error))) + } + } + } + + func sendTransaction(_ transaction: EthereumTransaction, completion: @escaping (WalletServiceResult) -> Void) { + defaultDispatchQueue.async { + switch self.web3.eth.sendRawTransaction(transaction) { + case .success(let result): + completion(.success(result: result.hash)) + + case .failure(let error): + completion(.failure(error: error.asWalletServiceError())) + } + } + } +} diff --git a/Adamant/Wallets/Ethereum/EthWalletService+Transfers.swift b/Adamant/Wallets/Ethereum/EthWalletService+Transfers.swift new file mode 100644 index 000000000..f64a5feda --- /dev/null +++ b/Adamant/Wallets/Ethereum/EthWalletService+Transfers.swift @@ -0,0 +1,20 @@ +// +// EthWalletService+Transfers.swift +// Adamant +// +// Created by Anokhov Pavel on 21.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit + +extension EthWalletService: WalletServiceWithTransfers { + func transferListViewController() -> UIViewController { + guard let vc = router.get(scene: AdamantScene.Wallets.Ethereum.transactionsList) as? EthTransactionsViewController else { + fatalError("Can't get EthTransactionsViewController") + } + + vc.ethWalletService = self + return vc + } +} diff --git a/Adamant/Wallets/Ethereum/EthWalletService.swift b/Adamant/Wallets/Ethereum/EthWalletService.swift new file mode 100644 index 000000000..7bb6731df --- /dev/null +++ b/Adamant/Wallets/Ethereum/EthWalletService.swift @@ -0,0 +1,485 @@ +// +// EthWalletService.swift +// Adamant +// +// Created by Anokhov Pavel on 03.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import UIKit +import web3swift +import Swinject +import Alamofire +import BigInt + +extension Web3Error { + func asWalletServiceError() -> WalletServiceError { + switch self { + case .connectionError: + return .networkError + + case .nodeError(let message): + return .remoteServiceError(message: message) + + case .generalError(let 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: + return .internalError(message: "Unknown error", error: nil) + } + } +} + +class EthWalletService: WalletService { + // MARK: - Constants + let addressRegex = try! NSRegularExpression(pattern: "^0x[a-fA-F0-9]{40}$") + + static let currencySymbol = "ETH" + static let currencyLogo = #imageLiteral(resourceName: "wallet_eth") + static let currencyExponent = -18 + + private (set) var transactionFee: Decimal = 0.0 + + static let transferGas: Decimal = 21000 + static let defaultGasPrice = 20000000000 // 20 Gwei + static let kvsAddress = "eth:address" + + static let walletPath = "m/44'/60'/3'/1" + + + // MARK: - Dependencies + weak var accountService: AccountService! + var apiService: ApiService! + var dialogService: DialogService! + var router: Router! + + + // MARK: - Notifications + let walletUpdatedNotification = Notification.Name("adamant.ethWallet.walletUpdated") + let serviceEnabledChanged = Notification.Name("adamant.ethWallet.enabledChanged") + let transactionFeeUpdated = Notification.Name("adamant.ethWallet.feeUpdated") + + + // MARK: RichMessageProvider properties + static let richMessageType = "eth_transaction" + let cellIdentifierSent = "ethTransferSent" + let cellIdentifierReceived = "ethTransferReceived" + let cellSource: CellSource? = CellSource.nib(nib: UINib(nibName: "TransferCollectionViewCell", bundle: nil)) + + + // MARK: - Properties + + let web3: web3 + private let baseUrl: String + let defaultDispatchQueue = DispatchQueue(label: "im.adamant.ethWalletService", qos: .utility, attributes: [.concurrent]) + private (set) var enabled = true + + let stateSemaphore = DispatchSemaphore(value: 1) + + var walletViewController: WalletViewController { + guard let vc = router.get(scene: AdamantScene.Wallets.Ethereum.wallet) as? EthWalletViewController else { + fatalError("Can't get EthWalletViewController") + } + + vc.service = self + return vc + } + + // MARK: - State + private (set) var state: WalletServiceState = .notInitiated + private (set) var ethWallet: EthWallet? = nil + + var wallet: WalletAccount? { return ethWallet } + + + // MARK: - Logic + init(apiUrl: String) throws { + // Init network + guard let url = URL(string: apiUrl), let web3 = Web3.new(url) else { + throw WalletServiceError.networkError + } + + self.web3 = web3 + self.baseUrl = EthWalletService.buildBaseUrl(for: web3.provider.network) + + // Notifications + NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userLoggedIn, object: nil, queue: nil) { [weak self] _ in + self?.update() + } + + NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.accountDataUpdated, object: nil, queue: nil) { [weak self] _ in + self?.update() + } + + NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userLoggedOut, object: nil, queue: nil) { [weak self] _ in + self?.ethWallet = nil + } + } + + func update() { + guard let wallet = ethWallet else { + return + } + + defer { stateSemaphore.signal() } + stateSemaphore.wait() + + switch state { + case .notInitiated, .updating: + return + + case .initiated, .updated: + break + } + + state = .updating + + getBalance(forAddress: wallet.ethAddress) { result in + defer { + self.stateSemaphore.signal() + } + self.stateSemaphore.wait() + self.state = .updated + + switch result { + case .success(let balance): + if wallet.balance != balance { + wallet.balance = balance + NotificationCenter.default.post(name: self.walletUpdatedNotification, object: self, userInfo: [AdamantUserInfoKey.WalletService.wallet: wallet]) + } + + case .failure(let error): + self.dialogService.showRichError(error: error) + } + } + + getGasPrices { [weak self] result in + switch result { + case .success(let price): + guard let fee = self?.transactionFee else { + return + } + + let newFee = price * EthWalletService.transferGas + + if fee != newFee { + self?.transactionFee = newFee + + if let notification = self?.transactionFeeUpdated { + NotificationCenter.default.post(name: notification, object: self, userInfo: nil) + } + } + + case .failure: + break + } + } + } + + // MARK: - Tools + + func validate(address: String) -> AddressValidationResult { + return addressRegex.perfectMatch(with: address) ? .valid : .invalid + } + + func getGasPrices(completion: @escaping (WalletServiceResult) -> Void) { + switch web3.eth.getGasPrice() { + case .success(let price): + completion(.success(result: price.asDecimal(exponent: EthWalletService.currencyExponent))) + + case .failure(let error): + completion(.failure(error: error.asWalletServiceError())) + } + } + + private static func buildBaseUrl(for network: Networks?) -> String { + let suffix: String + + guard let network = network else { + return "https://api.etherscan.io/api" + } + + switch network { + case .Mainnet: + suffix = "" + + default: + suffix = "-\(network)" + } + + return "https://api\(suffix).etherscan.io/api" + } + + private func buildUrl(queryItems: [URLQueryItem]? = nil) throws -> URL { + guard let url = URL(string: baseUrl), var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + throw AdamantApiService.InternalError.endpointBuildFailed + } + + components.queryItems = queryItems + + return try components.asURL() + } +} + + +// MARK: - WalletInitiatedWithPassphrase +extension EthWalletService: InitiatedWithPassphraseService { + func initWallet(withPassphrase passphrase: String, completion: @escaping (WalletServiceResult) -> Void) { + // MARK: 1. Prepare + stateSemaphore.wait() + + state = .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: "", mnemonicsPassword: "", language: .english, prefixPath: EthWalletService.walletPath) else { + completion(.failure(error: .internalError(message: "ETH Wallet: failed to create Keystore", error: nil))) + stateSemaphore.signal() + return + } + + keystore = store + } catch { + completion(.failure(error: .internalError(message: "ETH Wallet: failed to create Keystore", error: error))) + stateSemaphore.signal() + return + } + + web3.addKeystoreManager(KeystoreManager([keystore])) + + guard let ethAddress = keystore.addresses?.first else { + completion(.failure(error: .internalError(message: "ETH Wallet: failed to create Keystore", error: nil))) + stateSemaphore.signal() + return + } + + // MARK: 3. Update + ethWallet = EthWallet(address: ethAddress.address, ethAddress: ethAddress, keystore: keystore) + state = .initiated + + if !enabled { + enabled = true + NotificationCenter.default.post(name: serviceEnabledChanged, object: self) + } + + stateSemaphore.signal() + + // MARK: 4. Save into KVS + save(ethAddress: ethAddress.address) { [weak self] result in + switch result { + case .success: + break + + case .failure(let error): + self?.dialogService.showRichError(error: error) + } + } + + // MARK: 5. Initiate update + update() + } +} + + +// MARK: - Dependencies +extension EthWalletService: SwinjectDependentService { + func injectDependencies(from container: Container) { + accountService = container.resolve(AccountService.self) + apiService = container.resolve(ApiService.self) + dialogService = container.resolve(DialogService.self) + router = container.resolve(Router.self) + } +} + + +// MARK: - Balances & addresses +extension EthWalletService { + func getBalance(forAddress address: EthereumAddress, completion: @escaping (WalletServiceResult) -> Void) { + DispatchQueue.global(qos: .utility).async { [weak self] in + guard let web3 = self?.web3 else { + print("Can't get web3 service") + return + } + + let result = web3.eth.getBalance(address: address) + + switch result { + case .success(let balance): + completion(.success(result: balance.asDecimal(exponent: EthWalletService.currencyExponent))) + + case .failure(let error): + completion(.failure(error: error.asWalletServiceError())) + } + } + } + + + func getWalletAddress(byAdamantAddress address: String, completion: @escaping (WalletServiceResult) -> Void) { + apiService.get(key: EthWalletService.kvsAddress, sender: address) { (result) in + switch result { + case .success(let value): + if let address = value { + completion(.success(result: address)) + } else { + completion(.failure(error: .walletNotInitiated)) + } + + case .failure(let error): + completion(.failure(error: .internalError(message: "ETH Wallet: fail to get address from KVS", error: error))) + } + } + } +} + + +// MARK: - KVS +extension EthWalletService { + /// - Parameters: + /// - ethAddress: Ethereum address to save into KVS + /// - adamantAddress: Owner of Ethereum address + /// - completion: success + private func save(ethAddress: String, completion: @escaping (WalletServiceSimpleResult) -> Void) { + guard let adamant = accountService.account, let keypair = accountService.keypair else { + completion(.failure(error: .notLogged)) + return + } + + let api = apiService + + getWalletAddress(byAdamantAddress: adamant.address) { result in + switch result { + case .success(let address): + guard address == ethAddress else { + // ETH already saved + completion(.success) + return + } + + guard adamant.balance >= AdamantApiService.KvsFee else { + completion(.failure(error: .notEnoughtMoney)) + return + } + + api?.store(key: EthWalletService.kvsAddress, value: ethAddress, type: .keyValue, sender: adamant.address, keypair: keypair) { result in + switch result { + case .success: + completion(.success) + + case .failure(let error): + completion(.failure(error: .apiError(error))) + } + } + + case .failure(let error): + completion(.failure(error: error)) + } + } + } +} + + +// MARK: - Transactions +extension EthWalletService { + func getTransactionsHistory(address: String, page: Int = 1, size: Int = 50, completion: @escaping (WalletServiceResult<[EthTransaction]>) -> Void) { + let queryItems: [URLQueryItem] = [URLQueryItem(name: "module", value: "account"), + URLQueryItem(name: "action", value: "txlist"), + URLQueryItem(name: "address", value: address), + URLQueryItem(name: "page", value: "\(page)"), + URLQueryItem(name: "offset", value: "\(size)"), + URLQueryItem(name: "sort", value: "desc") + // ,URLQueryItem(name: "apikey", value: "YourApiKeyToken") + ] + + let endpoint: URL + do { + endpoint = try buildUrl(queryItems: queryItems) + } catch { + let err = AdamantApiService.InternalError.endpointBuildFailed.apiServiceErrorWith(error: error) + completion(.failure(error: WalletServiceError.apiError(err))) + return + } + + Alamofire.request(endpoint).responseData(queue: defaultDispatchQueue) { response in + switch response.result { + case .success(let data): + do { + let model: EthResponse = try JSONDecoder().decode(EthResponse.self, from: data) + + if model.status == 1 { + var transactions = model.result + + for index in 0..) -> Void) { + let sender = wallet?.address + let eth = web3.eth + + DispatchQueue.global(qos: .utility).async { + do { + // MARK: 1. Transaction's details and receipt + let details = try eth.getTransactionDetailsPromise(hash).wait() + let receipt = try eth.getTransactionReceiptPromise(hash).wait() + + // MARK: 2. Determine if transaction is outcome or income + let isOutgoing: Bool + if let sender = sender { + isOutgoing = details.transaction.to.address != sender + } else { + isOutgoing = false + } + + // MARK: 3. Check if transaction is delivered + guard receipt.status == .ok, let blockNumber = details.blockNumber else { + let transaction = details.transaction.asEthTransaction(date: nil, gasUsed: receipt.gasUsed, blockNumber: nil, confirmations: nil, receiptStatus: receipt.status, isOutgoing: isOutgoing) + completion(.success(result: transaction)) + return + } + + // MARK: 4. Block timestamp & confirmations + let currentBlock = try eth.getBlockNumberPromise().wait() + let block = try eth.getBlockByNumberPromise(blockNumber).wait() + let confirmations = currentBlock - blockNumber + + let transaction = details.transaction.asEthTransaction(date: block.timestamp, gasUsed: receipt.gasUsed, blockNumber: String(blockNumber), confirmations: String(confirmations), receiptStatus: receipt.status, isOutgoing: isOutgoing) + + completion(.success(result: transaction)) + + } catch let error as Web3Error { + completion(.failure(error: error.asWalletServiceError())) + } catch { + completion(.failure(error: WalletServiceError.internalError(message: "Failed to get transaction", error: error))) + } + } + } +} diff --git a/Adamant/Wallets/Ethereum/EthWalletViewController.swift b/Adamant/Wallets/Ethereum/EthWalletViewController.swift new file mode 100644 index 000000000..8bdbe0512 --- /dev/null +++ b/Adamant/Wallets/Ethereum/EthWalletViewController.swift @@ -0,0 +1,23 @@ +// +// EthWalletViewController.swift +// Adamant +// +// Created by Anokhov Pavel on 12.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit + +extension String.adamantLocalized.wallets { + static let ethereum = NSLocalizedString("AccountTab.Wallets.ethereum_wallet", comment: "Account tab: Ethereum wallet") +} + +class EthWalletViewController: WalletViewControllerBase { + // MARK: Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + walletTitleLabel.text = String.adamantLocalized.wallets.ethereum + } +} diff --git a/Adamant/Wallets/Lisk/LskTransactionsViewController.swift b/Adamant/Wallets/Lisk/LskTransactionsViewController.swift new file mode 100644 index 000000000..f549219d4 --- /dev/null +++ b/Adamant/Wallets/Lisk/LskTransactionsViewController.swift @@ -0,0 +1,147 @@ +// +// LskTransactionsViewController +// Adamant +// +// Created by Anton Boyarkin on 17/07/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import Lisk +import web3swift +import BigInt + +class LskTransactionsViewController: TransactionsListViewControllerBase { + + // MARK: - Dependencies + var lskApiService: LskApiService! + var dialogService: DialogService! + var router: Router! + + // MARK: - Properties + var transactions: [Transactions.TransactionModel] = [] + + override func viewDidLoad() { + super.viewDidLoad() + + self.refreshControl.beginRefreshing() + + handleRefresh(self.refreshControl) + } + + override func handleRefresh(_ refreshControl: UIRefreshControl) { + self.lskApiService.getTransactions({ (result) in + switch result { + case .success(let transactions): + self.transactions = transactions + DispatchQueue.main.async { + self.tableView.reloadData() + } + break + case .failure(let error): + if case .internalError(let message, _ ) = error { + let localizedErrorMessage = NSLocalizedString(message, comment: "TransactionList: 'Transactions not found' message.") + self.dialogService.showWarning(withMessage: localizedErrorMessage) + } else { + self.dialogService.showError(withMessage: String.adamantLocalized.transactionList.notFound, error: error) + } + break + } + DispatchQueue.main.async { + self.refreshControl.endRefreshing() + } + }) + } + + + // MARK: - UITableView + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return transactions.count + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + +// let transaction = transactions[indexPath.row] + + guard let controller = router.get(scene: AdamantScene.Wallets.Ethereum.transactionDetails) as? TransactionDetailsViewControllerBase else { + return + } + +// controller.transaction = transaction + navigationController?.pushViewController(controller, animated: true) + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifierCompact, for: indexPath) as? TransactionTableViewCell else { + // TODO: Display & Log error + return UITableViewCell(style: .default, reuseIdentifier: "cell") + } + +// let transaction = transactions[indexPath.row] + + cell.accessoryType = .disclosureIndicator + +// configureCell(cell, for: transaction) + return cell + } +} + +//extension Transactions.TransactionModel: TransactionDetails { +// var senderAddress: String { +// return self.senderId +// } +// +// var recipientAddress: String { +// return self.recipientId ?? "" +// } +// +// var sentDate: Date { +// return Date(timeIntervalSince1970: TimeInterval(self.timestamp) + Constants.Time.epochSeconds) +// } +// +// var amountValue: Double { +// guard let string = Web3.Utils.formatToPrecision(BigUInt(self.amount) ?? BigUInt(0), numberDecimals: 8, formattingDecimals: 8, decimalSeparator: ".", fallbackToScientific: false), let value = Double(string) else { +// return 0 +// } +// +// return value +// } +// +// var feeValue: Double { +// guard let string = Web3.Utils.formatToPrecision(BigUInt(self.fee) ?? BigUInt(0), numberDecimals: 8, formattingDecimals: 8, decimalSeparator: ".", fallbackToScientific: false), let value = Double(string) else { +// return 0 +// } +// +// return value +// } +// +// var confirmationsValue: String { +// return "\(self.confirmations)" +// } +// +// var block: String { +// return self.blockId +// } +// +// var showGoToExplorer: Bool { +// return true +// } +// +// var explorerUrl: URL? { +// return URL(string: "https://testnet-explorer.lisk.io/tx/\(id)") +// } +// +// var showGoToChat: Bool { +// return false +// } +// +// var chatroom: Chatroom? { +// return nil +// } +// +// var currencyCode: String { +// return "LSK" +// } +//} diff --git a/Adamant/Wallets/Lisk/LskWallet.swift b/Adamant/Wallets/Lisk/LskWallet.swift new file mode 100644 index 000000000..6c8c4b27e --- /dev/null +++ b/Adamant/Wallets/Lisk/LskWallet.swift @@ -0,0 +1,19 @@ +// +// LskWallet.swift +// Adamant +// +// Created by Anokhov Pavel on 03.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation + +class LskWallet: WalletAccount { + let address: String + var balance: Decimal = 0.0 + var notifications: Int = 0 + + init(address: String) { + self.address = address + } +} diff --git a/Adamant/Wallets/Lisk/LskWalletService.swift b/Adamant/Wallets/Lisk/LskWalletService.swift new file mode 100644 index 000000000..1947b9658 --- /dev/null +++ b/Adamant/Wallets/Lisk/LskWalletService.swift @@ -0,0 +1,52 @@ +// +// LskWalletService.swift +// Adamant +// +// Created by Anokhov Pavel on 03.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import UIKit + +class LskWalletService: WalletService { + var walletViewController: WalletViewController { fatalError() } + + let walletUpdatedNotification = Notification.Name("lsk.update") + let serviceEnabledChanged = Notification.Name("lsk.enabledChanged") + + // MARK: - Constants + let addressRegex = try! NSRegularExpression(pattern: "^([0-9]{2,22})L$", options: []) + let transactionFee: Decimal = 0.1 + let enabled: Bool = true + + static var currencySymbol = "LSK" + static var currencyLogo = #imageLiteral(resourceName: "wallet_lsk") + + + // MARK: - Properties + let transferAvailable: Bool = true + + + // MARK: - State + private (set) var state: WalletServiceState = .notInitiated + private (set) var wallet: WalletAccount? = nil + + + // MARK: - Logic + func update() { + + } + + + // MARK: - Tools + func validate(address: String) -> AddressValidationResult { + let value = address.replacingOccurrences(of: "L", with: "") + + return addressRegex.perfectMatch(with: value) ? .valid : .invalid + } + + func getWalletAddress(byAdamantAddress address: String, completion: @escaping (WalletServiceResult) -> Void) { + completion(.failure(error: .internalError(message: "Not implemented", error: nil))) + } +} diff --git a/Adamant/Wallets/TransactionDetails.swift b/Adamant/Wallets/TransactionDetails.swift new file mode 100644 index 000000000..9b01abde3 --- /dev/null +++ b/Adamant/Wallets/TransactionDetails.swift @@ -0,0 +1,60 @@ +// +// 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 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 } + + /// The block of the transaction. + var blockValue: String? { get } + + var isOutgoing: Bool { get } + + var transactionStatus: TransactionStatus? { get } +} + +extension TransactionDetails { +// 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/Wallets/TransactionDetailsViewControllerBase.swift b/Adamant/Wallets/TransactionDetailsViewControllerBase.swift new file mode 100644 index 000000000..031387704 --- /dev/null +++ b/Adamant/Wallets/TransactionDetailsViewControllerBase.swift @@ -0,0 +1,418 @@ +// +// TransactionDetailsViewControllerBase.swift +// Adamant +// +// Created by Anton Boyarkin on 25/06/2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import Eureka +import SafariServices + +// MARK: - Localization +extension String.adamantLocalized { + struct transactionDetails { + static let title = NSLocalizedString("TransactionDetailsScene.Title", comment: "Transaction details: scene title") + static let yourAddress = NSLocalizedString("TransactionDetailsScene.YourAddress", comment: "Transaction details: 'Your address' flag.") + static let requestingDataProgressMessage = NSLocalizedString("TransactionDetailsScene.RequestingData", comment: "Transaction details: 'Requesting Data' progress message.") + } +} + +extension String.adamantLocalized.alert { + static let exportUrlButton = NSLocalizedString("TransactionDetailsScene.Share.URL", comment: "Export transaction: 'Share transaction URL' button") + static let exportSummaryButton = NSLocalizedString("TransactionDetailsScene.Share.Summary", comment: "Export transaction: 'Share transaction summary' button") +} + +class TransactionDetailsViewControllerBase: FormViewController { + // MARK: - Rows + enum Rows { + case transactionNumber + case from + case to + case date + case amount + case fee + case confirmations + case block + case status + case openInExplorer + case openChat + + var tag: String { + switch self { + case .transactionNumber: return "id" + case .from: return "from" + case .to: return "to" + case .date: return "date" + case .amount: return "amount" + case .fee: return "fee" + case .confirmations: return "confirmations" + case .block: return "block" + case .status: return "status" + case .openInExplorer: return "openInExplorer" + case .openChat: return "openChat" + } + } + + var localized: String { + switch self { + case .transactionNumber: return NSLocalizedString("TransactionDetailsScene.Row.Id", comment: "Transaction details: Id row.") + case .from: return NSLocalizedString("TransactionDetailsScene.Row.From", comment: "Transaction details: sender row.") + case .to: return NSLocalizedString("TransactionDetailsScene.Row.To", comment: "Transaction details: recipient row.") + case .date: return NSLocalizedString("TransactionDetailsScene.Row.Date", comment: "Transaction details: date row.") + case .amount: return NSLocalizedString("TransactionDetailsScene.Row.Amount", comment: "Transaction details: amount row.") + case .fee: return NSLocalizedString("TransactionDetailsScene.Row.Fee", comment: "Transaction details: fee row.") + case .confirmations: return NSLocalizedString("TransactionDetailsScene.Row.Confirmations", comment: "Transaction details: confirmations row.") + case .block: return NSLocalizedString("TransactionDetailsScene.Row.Block", comment: "Transaction details: Block id row.") + case .status: return NSLocalizedString("TransactionDetailsScene.Row.Status", comment: "Transaction details: Transaction delivery status.") + case .openInExplorer: return NSLocalizedString("TransactionDetailsScene.Row.Explorer", comment: "Transaction details: 'Open transaction in explorer' row.") + case .openChat: return "" + } + } + + var image: UIImage? { + switch self { + case .openInExplorer: return #imageLiteral(resourceName: "row_explorer") + case .openChat: return #imageLiteral(resourceName: "row_chat") + + default: return nil + } + } + } + + // MARK: - Dependencies + var dialogService: DialogService! + + // MARK: - Properties + + var transaction: TransactionDetails? = nil { + didSet { + tableView?.reloadData() + } + } + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + if #available(iOS 11.0, *) { + navigationController?.navigationBar.prefersLargeTitles = true + } + + navigationItem.title = String.adamantLocalized.transactionDetails.title + navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(share)) + navigationAccessoryView.tintColor = UIColor.adamant.primary + + // MARK: - Transfer section + let section = Section() + + // MARK: Transaction number + let idRow = LabelRow() { + $0.disabled = true + $0.tag = Rows.transactionNumber.tag + $0.title = Rows.transactionNumber.localized + $0.value = transaction?.id + }.cellSetup { (cell, _) in + cell.selectionStyle = .gray + }.onCellSelection { (_, row) in + if let text = row.value { + self.shareValue(text) + } + }.cellUpdate { (cell, _) in + cell.textLabel?.textColor = .black + } + + section.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 = transaction { + if let senderName = self?.senderName { + $0.value = DoubleDetail(first: senderName, second: transaction.senderAddress) + } else { + $0.value = DoubleDetail(first: transaction.senderAddress, second: nil) + } + } else { + $0.value = nil + } + }.cellSetup { [weak self] (cell, _) in + cell.selectionStyle = .gray + cell.height = { + if self?.senderName != nil { + return DoubleDetailsTableViewCell.fullHeight + } else { + return DoubleDetailsTableViewCell.compactHeight + } + } + }.onCellSelection { (_, row) in + guard let value = row.value else { + return + } + + let text: String + if let name = value.second { + text = "\(name) (\(value.first))" + } else { + text = value.first + } + + self.shareValue(text) + }.cellUpdate { (cell, _) in + cell.textLabel?.textColor = .black + } + + section.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 = transaction { + if let recipientName = self?.recipientName { + $0.value = DoubleDetail(first: recipientName, second: transaction.recipientAddress) + } else { + $0.value = DoubleDetail(first: transaction.recipientAddress, second: nil) + } + } else { + $0.value = nil + } + }.cellSetup { [weak self] (cell, _) in + cell.selectionStyle = .gray + cell.height = { + if self?.recipientName != nil { + return DoubleDetailsTableViewCell.fullHeight + } else { + return DoubleDetailsTableViewCell.compactHeight + } + } + }.onCellSelection { (_, row) in + guard let value = row.value else { + return + } + + let text: String + if let name = value.second { + text = "\(name) (\(value.first))" + } else { + text = value.first + } + + self.shareValue(text) + }.cellUpdate { (cell, _) in + cell.textLabel?.textColor = .black + } + + section.append(recipientRow) + + // MARK: Date + let dateRow = DateRow() { + $0.disabled = true + $0.tag = Rows.date.tag + $0.title = Rows.date.localized + $0.value = transaction?.dateValue + }.cellSetup { (cell, _) in + cell.selectionStyle = .gray + }.onCellSelection { [weak self] (_, row) in + if let value = row.value { + let text = value.humanizedDateTimeFull() + self?.shareValue(text) + } + }.cellUpdate { (cell, _) in + cell.textLabel?.textColor = .black + } + + section.append(dateRow) + + // MARK: Amount + let amountRow = DecimalRow() { + $0.disabled = true + $0.tag = Rows.amount.tag + $0.title = Rows.amount.localized + $0.formatter = AdamantBalanceFormat.currencyFormatter(for: .full, currencySymbol: currencySymbol) + $0.value = transaction?.amountValue.doubleValue + }.cellSetup { (cell, _) in + cell.selectionStyle = .gray + }.onCellSelection { [weak self] (_, row) in + if let value = row.value { + let text = AdamantBalanceFormat.full.format(value, withCurrencySymbol: self?.currencySymbol ?? nil) + self?.shareValue(text) + } + }.cellUpdate { (cell, _) in + cell.textLabel?.textColor = .black + } + + section.append(amountRow) + + // MARK: Fee + let feeRow = DecimalRow() { + $0.disabled = true + $0.tag = Rows.fee.tag + $0.title = Rows.fee.localized + $0.formatter = AdamantBalanceFormat.currencyFormatter(for: .full, currencySymbol: currencySymbol) + $0.value = transaction?.feeValue.doubleValue + }.cellSetup { (cell, _) in + cell.selectionStyle = .gray + }.onCellSelection { [weak self] (_, row) in + if let value = row.value { + let text = AdamantBalanceFormat.full.format(value, withCurrencySymbol: self?.currencySymbol ?? nil) + self?.shareValue(text) + } + }.cellUpdate { (cell, _) in + cell.textLabel?.textColor = .black + } + + section.append(feeRow) + + // MARK: Confirmations + let confirmationsRow = LabelRow() { + $0.disabled = true + $0.tag = Rows.confirmations.tag + $0.title = Rows.confirmations.localized + $0.value = transaction?.confirmationsValue + }.cellSetup { (cell, _) in + cell.selectionStyle = .gray + }.onCellSelection { [weak self] (_, row) in + if let text = row.value { + self?.shareValue(text) + } + }.cellUpdate { (cell, _) in + cell.textLabel?.textColor = .black + } + + section.append(confirmationsRow) + + // MARK: Block + let blockRow = LabelRow() { + $0.disabled = true + $0.tag = Rows.block.tag + $0.title = Rows.block.localized + $0.value = transaction?.blockValue + }.cellSetup { (cell, _) in + cell.selectionStyle = .gray + }.onCellSelection { [weak self] (_, row) in + if let text = row.value { + self?.shareValue(text) + } + }.cellUpdate { (cell, _) in + cell.textLabel?.textColor = .black + } + + section.append(blockRow) + + // MARK: Status + if let status = transaction?.transactionStatus { + let statusRow = LabelRow() { + $0.tag = Rows.status.tag + $0.title = Rows.status.localized + $0.value = status.localized + }.cellSetup { (cell, _) in + cell.selectionStyle = .gray + }.onCellSelection { [weak self] (_, row) in + if let text = row.value { + self?.shareValue(text) + } + }.cellUpdate { (cell, _) in + cell.textLabel?.textColor = .black + } + + section.append(statusRow) + } + + // MARK: Open in explorer + let explorerRow = LabelRow() { + $0.hidden = Condition.function([], { [weak self] _ -> Bool in + if let transaction = self?.transaction { + return self?.explorerUrl(for: transaction) == nil + } else { + return true + } + }) + + $0.tag = Rows.openInExplorer.tag + $0.title = Rows.openInExplorer.localized + $0.cell.imageView?.image = Rows.openInExplorer.image + }.cellSetup { (cell, _) in + cell.selectionStyle = .gray + }.cellUpdate { (cell, _) in + cell.accessoryType = .disclosureIndicator + }.onCellSelection { [weak self] (_, _) in + guard let transaction = self?.transaction, let url = self?.explorerUrl(for: transaction) else { + return + } + + let safari = SFSafariViewController(url: url) + safari.preferredControlTintColor = UIColor.adamant.primary + self?.present(safari, animated: true, completion: nil) + } + + section.append(explorerRow) + + form.append(section) + } + + // MARK: - Actions + + @objc func share(_ sender: Any) { + guard let transaction = transaction else { + return + } + + let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + alert.addAction(UIAlertAction(title: String.adamantLocalized.alert.cancel, style: .cancel, handler: nil)) + + if let url = explorerUrl(for: transaction) { + // URL + alert.addAction(UIAlertAction(title: String.adamantLocalized.alert.exportUrlButton, style: .default) { [weak self] _ in + let alert = UIActivityViewController(activityItems: [url], applicationActivities: nil) + self?.present(alert, animated: true, completion: nil) + }) + } + + // Description + if let summary = summary(for: transaction) { + alert.addAction(UIAlertAction(title: String.adamantLocalized.alert.exportSummaryButton, style: .default) { [weak self] _ in + let text = summary + let alert = UIActivityViewController(activityItems: [text], applicationActivities: nil) + self?.present(alert, animated: true, completion: nil) + }) + } + + present(alert, animated: true, completion: nil) + } + + // MARK: - Tools + + func shareValue(_ value: String) { + dialogService.presentShareAlertFor(string: value, types: [.copyToPasteboard, .share], excludedActivityTypes: nil, animated: true) { [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? = nil + var senderName: String? = nil + var recipientName: String? = nil + + func explorerUrl(for transaction: TransactionDetails) -> URL? { + return nil + } + + func summary(for transaction: TransactionDetails) -> String? { + return AdamantFormattingTools.summaryFor(transaction: transaction, url: explorerUrl(for: transaction)) + } +} diff --git a/Adamant/Stories/Transactions/TransactionTableViewCell.swift b/Adamant/Wallets/TransactionTableViewCell.swift similarity index 90% rename from Adamant/Stories/Transactions/TransactionTableViewCell.swift rename to Adamant/Wallets/TransactionTableViewCell.swift index 82b65d4fb..229b75aaf 100644 --- a/Adamant/Stories/Transactions/TransactionTableViewCell.swift +++ b/Adamant/Wallets/TransactionTableViewCell.swift @@ -34,11 +34,19 @@ class TransactionTableViewCell: UITableViewCell { } } + + // MARK: - Constants + + static let cellHeightCompact: CGFloat = 90.0 + static let cellHeightFull: CGFloat = 100.0 + + // MARK: - IBOutlets @IBOutlet weak var topImageView: UIImageView! @IBOutlet weak var bottomImageView: UIImageView! @IBOutlet weak var accountLabel: UILabel! + @IBOutlet weak var addressLabel: UILabel! @IBOutlet weak var ammountLabel: UILabel! @IBOutlet weak var dateLabel: UILabel! diff --git a/Adamant/Stories/Transactions/TransactionTableViewCell.xib b/Adamant/Wallets/TransactionTableViewCell.xib similarity index 78% rename from Adamant/Stories/Transactions/TransactionTableViewCell.xib rename to Adamant/Wallets/TransactionTableViewCell.xib index 4e6ded282..2ff199bf3 100644 --- a/Adamant/Stories/Transactions/TransactionTableViewCell.xib +++ b/Adamant/Wallets/TransactionTableViewCell.xib @@ -12,40 +12,46 @@ - - + + - + - + - + - + - - - + + diff --git a/Adamant/Wallets/TransactionsListViewControllerBase.swift b/Adamant/Wallets/TransactionsListViewControllerBase.swift new file mode 100644 index 000000000..151588a49 --- /dev/null +++ b/Adamant/Wallets/TransactionsListViewControllerBase.swift @@ -0,0 +1,162 @@ +// +// TransactionsListViewControllerBase.swift +// Adamant +// +// Created by Anokhov Pavel on 08.01.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import CoreData + +extension String.adamantLocalized { + struct transactionList { + static let title = NSLocalizedString("TransactionListScene.Title", comment: "TransactionList: scene title") + static let toChat = NSLocalizedString("TransactionListScene.ToChat", comment: "TransactionList: To Chat button") + static let startChat = NSLocalizedString("TransactionListScene.StartChat", comment: "TransactionList: Start Chat button") + static let notFound = NSLocalizedString("TransactionListScene.Error.NotFound", comment: "TransactionList: 'Transactions not found' message.") + } +} + +// Extensions for a generic classes is limited, so delegates implemented right in class declaration +class TransactionsListViewControllerBase: UIViewController { + let cellIdentifierFull = "cf" + let cellIdentifierCompact = "cc" + + internal lazy var refreshControl: UIRefreshControl = { + let refreshControl = UIRefreshControl() + refreshControl.addTarget(self, action: + #selector(self.handleRefresh(_:)), + for: UIControl.Event.valueChanged) + refreshControl.tintColor = UIColor.adamant.primary + + return refreshControl + }() + + + // MARK: - IBOutlets + @IBOutlet weak var tableView: UITableView! + + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + if #available(iOS 11.0, *) { + navigationController?.navigationBar.prefersLargeTitles = false + } + + navigationItem.title = String.adamantLocalized.transactionList.title + + // MARK: Configure tableView + let nib = UINib.init(nibName: "TransactionTableViewCell", bundle: nil) + tableView.register(nib, forCellReuseIdentifier: cellIdentifierFull) + tableView.register(nib, forCellReuseIdentifier: cellIdentifierCompact) + tableView.dataSource = self + tableView.delegate = self + tableView.refreshControl = refreshControl + + // MARK: Notifications + NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userLoggedIn, object: nil, queue: nil) { [weak self] notification in + self?.reloadData() + } + + NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAccountService.userLoggedOut, object: nil, queue: nil) { [weak self] _ in + self?.reloadData() + } + + NotificationCenter.default.addObserver(forName: Notification.Name.AdamantAddressBookService.addressBookUpdated, object: nil, queue: OperationQueue.main) { [weak self] _ in + self?.reloadData() + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + if let indexPath = tableView.indexPathForSelectedRow { + tableView.deselectRow(at: indexPath, animated: animated) + } + + if tableView.isEditing { + tableView.setEditing(false, animated: false) + } + } + + + // MARK: - To override + + func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + return UIView() + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return TransactionTableViewCell.cellHeightCompact + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + return UITableViewCell(style: .default, reuseIdentifier: "cell") + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return 0 + } + + @objc internal func handleRefresh(_ refreshControl: UIRefreshControl) { + + } + + func reloadData() { + + } + + var currencySymbol: String? = nil +} + +// MARK: - UITableViewDataSource, UITableViewDelegate +extension TransactionsListViewControllerBase: UITableViewDataSource, UITableViewDelegate { + func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + // MARK: Cells + + func configureCell(_ cell: TransactionTableViewCell, + isOutgoing: Bool, + partnerId: String, + partnerName: String?, + amount: Decimal, + date: Date?) { + cell.accountLabel.tintColor = UIColor.adamant.primary + cell.ammountLabel.tintColor = UIColor.adamant.primary + cell.dateLabel.tintColor = UIColor.adamant.secondary + + if isOutgoing { + cell.transactionType = .outcome + } else { + cell.transactionType = .income + } + + if let partnerName = partnerName { + cell.accountLabel.text = partnerName + cell.addressLabel.text = partnerId + + if cell.addressLabel.isHidden { + cell.addressLabel.isHidden = false + } + } else { + cell.accountLabel.text = partnerId + + if !cell.addressLabel.isHidden { + cell.addressLabel.isHidden = true + } + } + + cell.ammountLabel.text = AdamantBalanceFormat.full.format(amount, withCurrencySymbol: currencySymbol) + + if let date = date { + cell.dateLabel.text = date.humanizedDateTime() + } else { + cell.dateLabel.text = nil + } + } +} diff --git a/Adamant/Stories/Transactions/TransactionsViewController.xib b/Adamant/Wallets/TransactionsListViewControllerBase.xib similarity index 88% rename from Adamant/Stories/Transactions/TransactionsViewController.xib rename to Adamant/Wallets/TransactionsListViewControllerBase.xib index 08b854797..0bb149dd9 100644 --- a/Adamant/Stories/Transactions/TransactionsViewController.xib +++ b/Adamant/Wallets/TransactionsListViewControllerBase.xib @@ -1,16 +1,16 @@ - + - + - + diff --git a/Adamant/Wallets/TransferViewControllerBase+Alert.swift b/Adamant/Wallets/TransferViewControllerBase+Alert.swift new file mode 100644 index 000000000..baf5b34cf --- /dev/null +++ b/Adamant/Wallets/TransferViewControllerBase+Alert.swift @@ -0,0 +1,137 @@ +// +// TransferViewControllerBase+Alert.swift +// Adamant +// +// Created by Anokhov Pavel on 04.09.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +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) + self.view.constrainToEdges(view) + + let indicator = UIActivityIndicatorView(style: .whiteLarge) + view.addSubview(indicator) + view.constrainCentered(indicator) + indicator.startAnimating() + + if animated { + if Thread.isMainThread { + view.alpha = 0 + UIView.animate(withDuration: 0.2) { + view.alpha = 1 + } + } else { + DispatchQueue.main.async { + view.alpha = 0 + UIView.animate(withDuration: 0.2) { + view.alpha = 1 + } + } + } + } + } + + func hideProgress(animated: Bool) { + guard let progressView = progressView else { + return + } + + hideView(progressView, animated: animated) + } + + + // MARK: - Alert view + + func showAlertView(title: String?, message: String, animated: Bool) { + if let progressView = progressView { + hideView(progressView, animated: animated) + } + + if let alertView = alertView { + hideView(alertView, animated: animated) + } + + let callback = { + guard let alert = UINib(nibName: "FullscreenAlertView", bundle: nil).instantiate(withOwner: nil).first as? FullscreenAlertView else { + fatalError("Can't get FullscreenAlertView") + } + + alert.title = title + alert.message = message + + self.view.addSubview(alert) + self.view.constrainToEdges(alert) + + 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) + } + } + } + + if Thread.isMainThread { + callback() + } else { + DispatchQueue.main.async { + 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: () -> Void + + if animated { + callback = { + UIView.animate(withDuration: 0.2, animations: { + view.backgroundColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0) + }, completion: { success in + view.removeFromSuperview() + }) + } + } else { + callback = { + view.removeFromSuperview() + } + } + + if Thread.isMainThread { + callback() + } else { + DispatchQueue.main.async { + callback() + } + } + } +} diff --git a/Adamant/Wallets/TransferViewControllerBase+QR.swift b/Adamant/Wallets/TransferViewControllerBase+QR.swift new file mode 100644 index 000000000..afa6ea121 --- /dev/null +++ b/Adamant/Wallets/TransferViewControllerBase+QR.swift @@ -0,0 +1,145 @@ +// +// TransferViewControllerBase+QR.swift +// Adamant +// +// Created by Anokhov Pavel on 29.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import QRCodeReader +import EFQRCode +import AVFoundation +import Photos + + +// MARK: - QR +extension TransferViewControllerBase { + func scanQr() { + switch AVCaptureDevice.authorizationStatus(for: .video) { + case .authorized: + present(qrReader, animated: true, completion: nil) + + case .notDetermined: + AVCaptureDevice.requestAccess(for: .video) { [weak self] (granted: Bool) in + if granted, let qrReader = self?.qrReader { + if Thread.isMainThread { + self?.present(qrReader, animated: true, completion: nil) + } else { + DispatchQueue.main.async { + self?.present(qrReader, animated: true, completion: nil) + } + } + } else { + return + } + } + + case .restricted: + let alert = UIAlertController(title: nil, message: String.adamantLocalized.login.cameraNotSupported, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: String.adamantLocalized.alert.ok, style: .cancel, handler: nil)) + present(alert, animated: true, completion: nil) + + case .denied: + let alert = UIAlertController(title: nil, message: String.adamantLocalized.login.cameraNotAuthorized, preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: String.adamantLocalized.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.adamantLocalized.alert.cancel, style: .cancel, handler: nil)) + + present(alert, animated: true, completion: nil) + } + } + + func loadQr() { + let presenter: () -> Void = { [weak self] in + let picker = UIImagePickerController() + picker.delegate = self + picker.allowsEditing = false + picker.sourceType = .photoLibrary + self?.present(picker, animated: true, completion: nil) + } + + if #available(iOS 11.0, *) { + presenter() + } else { + switch PHPhotoLibrary.authorizationStatus() { + case .authorized: + presenter() + + case .notDetermined: + PHPhotoLibrary.requestAuthorization { status in + if status == .authorized { + presenter() + } + } + + case .restricted, .denied: + dialogService.presentGoToSettingsAlert(title: nil, message: String.adamantLocalized.login.photolibraryNotAuthorized) + } + } + } +} + +// MARK: - ButtonsStripeViewDelegate +extension TransferViewControllerBase: ButtonsStripeViewDelegate { + func buttonsStripe(_ stripe: ButtonsStripeView, didTapButton button: StripeButtonType) { + switch button { + case .qrCameraReader: + scanQr() + + case .qrPhotoReader: + loadQr() + + default: + return + } + } +} + +// MARK: - UIImagePickerControllerDelegate +extension TransferViewControllerBase: UINavigationControllerDelegate, UIImagePickerControllerDelegate { + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + dismiss(animated: true, completion: nil) + + guard let image = info[.originalImage] as? UIImage else { + return + } + + if let cgImage = image.toCGImage(), let codes = EFQRCode.recognize(image: cgImage), codes.count > 0 { + for aCode in codes { + if handleRawAddress(aCode) { + return + } + } + + dialogService.showWarning(withMessage: String.adamantLocalized.newChat.wrongQrError) + } else { + dialogService.showWarning(withMessage: String.adamantLocalized.login.noQrError) + } + } +} + +// MARK: - QRCodeReaderViewControllerDelegate +extension TransferViewControllerBase: QRCodeReaderViewControllerDelegate { + func reader(_ reader: QRCodeReaderViewController, didScanResult result: QRCodeReaderResult) { + if handleRawAddress(result.value) { + dismiss(animated: true, completion: nil) + } else { + dialogService.showWarning(withMessage: String.adamantLocalized.newChat.wrongQrError) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + reader.startScanning() + } + } + } + + func readerDidCancel(_ reader: QRCodeReaderViewController) { + reader.dismiss(animated: true, completion: nil) + } +} diff --git a/Adamant/Wallets/TransferViewControllerBase.swift b/Adamant/Wallets/TransferViewControllerBase.swift new file mode 100644 index 000000000..10ed009a7 --- /dev/null +++ b/Adamant/Wallets/TransferViewControllerBase.swift @@ -0,0 +1,721 @@ +// +// TransferViewControllerBase.swift +// Adamant +// +// Created by Anokhov Pavel on 09.01.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import Eureka +import QRCodeReader + + +// MARK: - Transfer Delegate Protocol + +protocol TransferViewControllerDelegate: class { + func transferViewControllerDidFinishTransfer(_ viewController: TransferViewControllerBase) +} + + +// MARK: - Localization +extension String.adamantLocalized { + struct transfer { + static let addressPlaceholder = NSLocalizedString("TransferScene.Recipient.Placeholder", comment: "Transfer: recipient address placeholder") + static let amountPlaceholder = NSLocalizedString("TransferScene.Amount.Placeholder", comment: "Transfer: transfer amount placeholder") + + static let addressValidationError = NSLocalizedString("TransferScene.Error.InvalidAddress", comment: "Transfer: Address validation error") + static let amountZeroError = NSLocalizedString("TransferScene.Error.TooLittleMoney", comment: "Transfer: Amount is zero, or even negative notification") + static let amountTooHigh = NSLocalizedString("TransferScene.Error.NotEnoughtMoney", comment: "Transfer: Amount is hiegher that user's total money notification") + static let accountNotFound = NSLocalizedString("TransferScene.Error.AddressNotFound", comment: "Transfer: Address not found error") + + static let transferProcessingMessage = NSLocalizedString("TransferScene.SendingFundsProgress", comment: "Transfer: Processing message") + static let transferSuccess = NSLocalizedString("TransferScene.TransferSuccessMessage", comment: "Transfer: Tokens transfered successfully message") + + static let send = NSLocalizedString("TransferScene.Send", comment: "Transfer: Send button") + + static let cantUndo = NSLocalizedString("TransferScene.CantUndo", comment: "Transfer: Send button") + + private init() { } + } +} + +fileprivate extension String.adamantLocalized.alert { + static let confirmSendMessageFormat = NSLocalizedString("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 { + return String.localizedStringWithFormat(String.adamantLocalized.alert.confirmSendMessageFormat, "\(amount)", recipient) + } + static let send = NSLocalizedString("TransferScene.Send", comment: "Transfer: Confirm transfer alert: Send tokens button") +} + + + +// MARK: - +class TransferViewControllerBase: FormViewController { + + // MARK: - Rows + + enum BaseRows { + case balance + case amount + case maxToTransfer + case address + case fee + case total + case comments + case sendButton + + var tag: String { + switch self { + case .balance: return "balance" + case .amount: return "amount" + case .maxToTransfer: return "max" + case .address: return "recipient" + case .fee: return "fee" + case .total: return "total" + case .comments: return "comments" + case .sendButton: return "send" + } + } + + var localized: String { + switch self { + case .balance: return NSLocalizedString("TransferScene.Row.Balance", comment: "Transfer: logged user balance.") + case .amount: return NSLocalizedString("TransferScene.Row.Amount", comment: "Transfer: amount of adamant to transfer.") + case .maxToTransfer: return NSLocalizedString("TransferScene.Row.MaxToTransfer", comment: "Transfer: maximum amount to transfer: available account money substracting transfer fee.") + case .address: return NSLocalizedString("TransferScene.Row.Recipient", comment: "Transfer: recipient address") + case .fee: return NSLocalizedString("TransferScene.Row.TransactionFee", comment: "Transfer: transfer fee") + case .total: return NSLocalizedString("TransferScene.Row.Total", comment: "Transfer: total amount of transaction: money to transfer adding fee") + case .comments: return NSLocalizedString("TransferScene.Row.Comments", comment: "Transfer: transfer comment") + case .sendButton: return String.adamantLocalized.transfer.send + } + } + } + + enum Sections { + case wallet + case recipient + case transferInfo + + var tag: String { + switch self { + case .wallet: return "wlt" + case .recipient: return "rcp" + case .transferInfo: return "trsfr" + } + } + + var localized: String { + switch self { + case .wallet: return NSLocalizedString("TransferScene.Section.YourWallet", comment: "Transfer: 'Your wallet' section") + + case .recipient: return "Получатель" +// NSLocalizedString("TransferScene.Section.Recipient", comment: "Transfer: 'Recipient info' section") + + + case .transferInfo: return NSLocalizedString("TransferScene.Section.TransferInfo", comment: "Transfer: 'Transfer info' section") + } + } + } + + + // MARK: - Dependencies + + var accountService: AccountService! + var dialogService: DialogService! + + + // MARK: - Properties + + var service: WalletServiceWithSend? { + didSet { + if let prev = oldValue { + NotificationCenter.default.removeObserver(self, name: prev.transactionFeeUpdated, object: prev) + } + + if let new = service { + NotificationCenter.default.addObserver(forName: new.transactionFeeUpdated, object: new, queue: OperationQueue.main) { [weak self] _ in + guard let fee = self?.service?.transactionFee, let form = self?.form else { + return + } + + if let row: DecimalRow = form.rowBy(tag: BaseRows.fee.tag) { + row.value = fee.doubleValue + row.updateCell() + } + + if let row: DecimalRow = form.rowBy(tag: BaseRows.maxToTransfer.tag) { + row.updateCell() + } + + self?.validateForm() + } + } + } + } + + + weak var delegate: TransferViewControllerDelegate? + + var recipient: String? = nil { + didSet { + if let row: RowOf = form.rowBy(tag: BaseRows.address.tag) { + row.value = recipient + row.updateCell() + } + } + } + + var admReportRecipient: String? = nil + var amount: Decimal? = nil + + var recipientIsReadonly = false + + var maxToTransfer: Decimal { + guard let service = service, let balance = service.wallet?.balance else { + return 0 + } + + let max = balance - service.transactionFee + + if max >= 0 { + return max + } else { + return 0 + } + } + + + // MARK: - QR Reader + + lazy var qrReader: QRCodeReaderViewController = { + let builder = QRCodeReaderViewControllerBuilder { + $0.reader = QRCodeReader(metadataObjectTypes: [.qr ], captureDevicePosition: .back) + $0.cancelButtonTitle = String.adamantLocalized.alert.cancel + $0.showSwitchCameraButton = false + } + + let vc = QRCodeReaderViewController(builder: builder) + vc.delegate = self + return vc + }() + + + // MARK: - Alert + var progressView: UIView? = nil + var alertView: UIView? = nil + + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + // MARK: UI + navigationAccessoryView.tintColor = UIColor.adamant.primary + + // MARK: Sections + form.append(walletSection()) + form.append(recipientSection()) + form.append(transactionInfoSection()) + + // MARK: - Button section + form +++ Section() + <<< ButtonRow() { [weak self] in + $0.title = BaseRows.sendButton.localized + $0.tag = BaseRows.sendButton.tag + + $0.disabled = Condition.function([BaseRows.address.tag, BaseRows.amount.tag]) { [weak self] form -> Bool in + guard let service = self?.service, let wallet = service.wallet, wallet.balance > service.transactionFee else { + return true + } + + guard let isValid = self?.formIsValid() else { + return true + } + + return !isValid + } + }.onCellSelection { [weak self] (cell, row) in + self?.confirmSendFunds() + } + } + + // 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.append(defaultRowFor(baseRow: BaseRows.address)) + + if !recipientIsReadonly, let stripe = recipientStripe() { + var footer = HeaderFooterView(.callback { + 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: .fee)) + section.append(defaultRowFor(baseRow: .total)) + + return section + } + + +/* + private func createLSKForm() { + if let account = lskApiService.account, let balanceString = account.balanceString, let balance = Double(balanceString) { + + maxToTransfer = balance +// defaultFee = AdamantLskApiService.defaultFee + + let currencyFormatter = NumberFormatter() + currencyFormatter.numberStyle = .decimal + currencyFormatter.roundingMode = .floor + currencyFormatter.positiveFormat = "#.######## LSK" + + form +++ Section(Sections.wallet.localized) + <<< DecimalRow() { + $0.title = Row.balance.localized + $0.value = balance + $0.tag = Row.balance.tag + $0.disabled = true + $0.formatter = currencyFormatter + } + + // MARK: - Transfer section + form +++ Section(Sections.transferInfo.localized) + + <<< TextRow() { + $0.title = Row.address.localized + $0.placeholder = String.adamantLocalized.transfer.addressPlaceholder + $0.tag = Row.address.tag +// $0.value = toAddress + $0.add(rule: RuleClosure(closure: { value -> ValidationError? in + guard let value = value?.uppercased() else { + return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) + } + switch AdamantLskApiService.validateAddress(address: value) { + case .valid: + return nil + + case .system, .invalid: + return ValidationError(msg: String.adamantLocalized.transfer.addressValidationError) + } + })) + $0.validationOptions = .validatesOnBlur + }.cellUpdate({ (cell, row) in + cell.titleLabel?.textColor = row.isValid ? .black : .red + }) + <<< DecimalRow() { + $0.title = Row.amount.localized + $0.placeholder = String.adamantLocalized.transfer.amountPlaceholder + $0.tag = Row.amount.tag + $0.formatter = currencyFormatter + $0.add(rule: RuleSmallerOrEqualThan(max: maxToTransfer)) + $0.validationOptions = .validatesOnChange + }.onChange(ethAmountChanged) + <<< DecimalRow() { + $0.title = Row.fee.localized +// $0.value = defaultFee + $0.tag = Row.fee.tag + $0.disabled = true + $0.formatter = currencyFormatter + } + <<< DecimalRow() { + $0.title = Row.total.localized + $0.value = nil + $0.tag = Row.total.tag + $0.disabled = true + $0.formatter = currencyFormatter + } + } + } +*/ + + // MARK: - Tools + + func validateForm() { + guard let service = service, let wallet = service.wallet else { + return + } + + if let row: DecimalRow = form.rowBy(tag: BaseRows.maxToTransfer.tag) { + markRow(row, valid: wallet.balance > service.transactionFee) + } + + if let row: TextRow = form.rowBy(tag: BaseRows.address.tag) { + if let address = row.value, validateRecipient(address) { + recipient = address + markRow(row, valid: true) + } else { + recipient = nil + markRow(row, valid: false) + } + } else { + recipient = nil + } + + if let row: DecimalRow = form.rowBy(tag: BaseRows.amount.tag) { + if let raw = row.value { + let amount = Decimal(raw) + self.amount = amount + + markRow(row, valid: validateAmount(amount)) + } else { + amount = nil + markRow(row, valid: true) + } + } else { + amount = nil + } + + if let row: DecimalRow = form.rowBy(tag: BaseRows.total.tag) { + if let amount = amount { + row.value = (amount + service.transactionFee).doubleValue + row.updateCell() + markRow(row, valid: validateAmount(amount)) + } else { + row.value = nil + markRow(row, valid: true) + row.updateCell() + } + } + + if let row: ButtonRow = form.rowBy(tag: BaseRows.sendButton.tag) { + row.evaluateDisabled() + } + } + + func markRow(_ row: BaseRowType, valid: Bool) { + row.baseCell.textLabel?.textColor = valid ? UIColor.black : UIColor.red + } + + + // MARK: - Send Actions + + private func confirmSendFunds() { + guard let recipient = recipient, let amount = amount else { + return + } + + guard validateRecipient(recipient) else { + dialogService.showWarning(withMessage: String.adamantLocalized.transfer.addressValidationError) + return + } + + guard amount <= maxToTransfer else { + dialogService.showWarning(withMessage: String.adamantLocalized.transfer.amountTooHigh) + return + } + + if admReportRecipient != nil, let account = accountService.account, account.balance < 0.001 { + dialogService.showWarning(withMessage: "Not enought money to send report") + return + } + + let formattedAmount = balanceFormatter.string(from: amount as NSDecimalNumber)! + let title = String.adamantLocalized.alert.confirmSendMessage(formattedAmount: formattedAmount, recipient: recipient) + + let alert = UIAlertController(title: title, message: String.adamantLocalized.transfer.cantUndo, preferredStyle: .alert) + let cancelAction = UIAlertAction(title: String.adamantLocalized.alert.cancel , style: .cancel, handler: nil) + let sendAction = UIAlertAction(title: String.adamantLocalized.alert.send, style: .default) { [weak self] _ in + self?.sendFunds() + } + + alert.addAction(cancelAction) + alert.addAction(sendAction) + + present(alert, animated: true, completion: nil) + } + + + + // MARK: - 'Virtual' methods with basic implementation + + /// Override this to provide custom balance formatter + var balanceFormatter: NumberFormatter { + return AdamantBalanceFormat.full.defaultFormatter + } + + /// 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 service = service, let balance = service.wallet?.balance else { + return false + } + + let total = withFee ? amount + service.transactionFee : amount + + return balance > total + } + + func formIsValid() -> Bool { + if let recipient = recipient, validateRecipient(recipient), let amount = amount, validateAmount(amount) { + return true + } else { + return false + } + } + + /// 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 reportTransferTo(admAddress: String, transferRecipient: String, amount: Decimal, comments: String, hash: String) { + + } + + /// 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") + } + + + /// MARK: - Abstract + + /// User loaded address from QR (camera or library) + /// You must override this method + /// + /// - Parameter address: raw readed address + /// - Returns: string was successfully handled + func handleRawAddress(_ address: String) -> Bool { + fatalError("You must implement raw address handling") + } + + + /// Build recipient address row + /// You must override this method + func recipientRow() -> BaseRow { + fatalError("You must implement recipient row") + } + + + /// Validate recipient's address + /// You must override this method + func validateRecipient(_ address: String) -> Bool { + fatalError("You must implement recipient addres validation logic") + } + + /* + func sendLSKFunds() { + guard let recipientRow = form.rowBy(tag: Row.address.tag) as? TextRow, + let recipient = recipientRow.value, + let amountRow = form.rowBy(tag: Row.amount.tag) as? DecimalRow, + let amount = amountRow.value else { + return + } + + guard recipientRow.isValid else { + dialogService.showWarning(withMessage: (recipientRow.validationErrors.first?.msg) ?? "Invalid Address") + return + } + + guard let totalAmount = totalAmount, totalAmount <= maxToTransfer else { + dialogService.showWarning(withMessage: String.adamantLocalized.transfer.amountTooHigh) + return + } + + let alert = UIAlertController(title: String.localizedStringWithFormat(String.adamantLocalized.alert.confirmSendMessageFormat, "\(amount) LSK", recipient), message: String.adamantLocalized.transfer.cantUndo, preferredStyle: .alert) + let cancelAction = UIAlertAction(title: String.adamantLocalized.alert.cancel , style: .cancel, handler: nil) + let sendAction = UIAlertAction(title: String.adamantLocalized.alert.send, style: .default, handler: { _ in + self.sendLsk(to: recipient, amount: amount) + }) + + alert.addAction(cancelAction) + alert.addAction(sendAction) + + present(alert, animated: true, completion: nil) + } +*/ + + // MARK: - Private + /* + + + private func sendLsk(to recipient: String, amount: Double) { + self.dialogService.showProgress(withMessage: String.adamantLocalized.transfer.transferProcessingMessage, userInteractionEnable: false) + + self.lskApiService.createTransaction(toAddress: recipient, amount: amount) { (result) in + switch result { + case .success(let transaction): + if let id = transaction.id { + var message = ["type": "lsk_transaction", "amount": "\(amount)", "hash": id, "comments":""] + + if let commentsRow = self.form.rowBy(tag: Row.comments.tag) as? TextAreaRow, + let comments = commentsRow.value { + message["comments"] = comments + } + + do { + let data = try JSONEncoder().encode(message) + guard let raw = String(data: data, encoding: String.Encoding.utf8) else { + return + } + print("Payload: \(raw)") + self.delegate?.transferFinished(with: raw) + + self.lskApiService.sendTransaction(transaction: transaction, completion: { (result) in + switch result { + case .success(let hash): + print("Hash: \(hash)") + self.dialogService.showSuccess(withMessage: String.adamantLocalized.transfer.transferSuccess) + self.close() + case .failure(let error): + self.dialogService.showError(withMessage: "Transrer issue", error: error) + } + }) + } catch { + self.dialogService.showError(withMessage: "Transrer issue", error: nil) + } + } else { + self.dialogService.showError(withMessage: "Transrer issue", error: nil) + } + + break + case .failure(let error): + self.dialogService.showError(withMessage: "Transrer issue", error: error) + break + } + } + } +*/ +} + + +// MARK: - Default rows +extension TransferViewControllerBase { + func defaultRowFor(baseRow: BaseRows) -> BaseRow { + switch baseRow { + case .balance: + return DecimalRow() { [weak self] in + $0.title = BaseRows.balance.localized + $0.tag = BaseRows.balance.tag + $0.disabled = true + $0.formatter = self?.balanceFormatter + + if let wallet = self?.service?.wallet { + $0.value = wallet.balance.doubleValue + } else { + $0.value = 0 + } + } + + case .address: + return recipientRow() + + case .maxToTransfer: + return DecimalRow() { [weak self] in + $0.title = BaseRows.maxToTransfer.localized + $0.tag = BaseRows.maxToTransfer.tag + $0.disabled = true + $0.formatter = self?.balanceFormatter + + if let maxToTransfer = self?.maxToTransfer { + $0.value = maxToTransfer.doubleValue + } + } + + case .amount: + return DecimalRow { [weak self] in + $0.title = BaseRows.amount.localized + $0.placeholder = String.adamantLocalized.transfer.amountPlaceholder + $0.tag = BaseRows.amount.tag + $0.formatter = self?.balanceFormatter + + if let amount = self?.amount { + $0.value = amount.doubleValue + } + }.onChange { [weak self] (row) in + self?.validateForm() + } + + case .fee: + return DecimalRow() { [weak self] in + $0.tag = BaseRows.fee.tag + $0.title = BaseRows.fee.localized + $0.disabled = true + $0.formatter = self?.balanceFormatter + + if let fee = self?.service?.transactionFee { + $0.value = fee.doubleValue + } else { + $0.value = 0 + } + } + + case .total: + return DecimalRow() { [weak self] in + $0.tag = BaseRows.total.tag + $0.title = BaseRows.total.localized + $0.value = nil + $0.disabled = true + $0.formatter = self?.balanceFormatter + + if let balance = self?.service?.wallet?.balance { + $0.add(rule: RuleSmallerOrEqualThan(max: balance.doubleValue)) + } + } + + case .comments: + fatalError() + + case .sendButton: + return ButtonRow() { [weak self] in + $0.title = BaseRows.sendButton.localized + $0.tag = BaseRows.sendButton.tag + + $0.disabled = Condition.function([BaseRows.address.tag, BaseRows.amount.tag]) { [weak self] form -> Bool in + guard let service = self?.service, let wallet = service.wallet, wallet.balance > service.transactionFee else { + return true + } + + guard let isValid = self?.formIsValid() else { + return true + } + + return !isValid + } + }.onCellSelection { [weak self] (cell, row) in + self?.confirmSendFunds() + } + } + } +} diff --git a/Adamant/Wallets/WalletAccount.swift b/Adamant/Wallets/WalletAccount.swift new file mode 100644 index 000000000..3184eb6ff --- /dev/null +++ b/Adamant/Wallets/WalletAccount.swift @@ -0,0 +1,18 @@ +// +// WalletAccount.swift +// Adamant +// +// Created by Anokhov Pavel on 03.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation + +// MARK: - Wallet Account +protocol WalletAccount { + // MARK: Account + var address: String { get } + var balance: Decimal { get } + + var notifications: Int { get } +} diff --git a/Adamant/Wallets/WalletService.swift b/Adamant/Wallets/WalletService.swift new file mode 100644 index 000000000..427440b38 --- /dev/null +++ b/Adamant/Wallets/WalletService.swift @@ -0,0 +1,214 @@ +// +// Wallet.swift +// Adamant +// +// Created by Anokhov Pavel on 03.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation +import UIKit +import Swinject + +enum WalletServiceState { + case notInitiated, initiated, updated, updating +} + +enum WalletServiceSimpleResult { + case success + case failure(error: WalletServiceError) +} + +enum WalletServiceResult { + case success(result: T) + case failure(error: WalletServiceError) +} + +// MARK: - Errors + +enum WalletServiceError: Error { + case notLogged + case notEnoughtMoney + case networkError + case accountNotFound + case walletNotInitiated + case invalidAmount(Decimal) + case remoteServiceError(message: String) + case apiError(ApiServiceError) + case internalError(message: String, error: Error?) + case transactionNotFound(reason: String) +} + +extension WalletServiceError: RichError { + var message: String { + switch self { + case .notLogged: + return String.adamantLocalized.sharedErrors.userNotLogged + + case .notEnoughtMoney: + return NSLocalizedString("WalletServices.SharedErrors.NotEnoughtMoney", comment: "Wallet Services: Shared error, user do not have enought money.") + + case .networkError: + return String.adamantLocalized.sharedErrors.networkError + + case .accountNotFound: + return String.adamantLocalized.transfer.accountNotFound + + case .walletNotInitiated: + return "Кошелёк ещё не создан для этого аккаунта" + + case .remoteServiceError(let message): + return String.adamantLocalized.sharedErrors.remoteServerError(message: message) + + case .apiError(let error): + return error.localized + + case .internalError(let message, _): + return String.adamantLocalized.sharedErrors.internalError(message: message) + + case .invalidAmount(let amount): + return "Неверное количество для перевода: \(amount)" + + case .transactionNotFound: + return "Не удалось найти транзакцию" + } + } + + var internalError: Error? { + switch self { + case .internalError(_, let error): return error + default: return nil + } + } + + var level: ErrorLevel { + switch self { + case .notLogged, .notEnoughtMoney, .networkError, .accountNotFound, .invalidAmount, .walletNotInitiated, .transactionNotFound: + return .warning + + case .remoteServiceError, .internalError: + return .error + + case .apiError(let error): + switch error { + case .accountNotFound, .notLogged, .networkError: + return .warning + + case .serverError, .internalError: + return .error + } + } + } +} + +extension ApiServiceError { + func asWalletServiceError() -> WalletServiceError { + switch self { + case .accountNotFound: + return .accountNotFound + + case .networkError: + return .networkError + + case .notLogged: + return .notLogged + + case .serverError, .internalError: + return .apiError(self) + } + } +} + + +// MARK: - Notifications +extension AdamantUserInfoKey { + struct WalletService { + static let wallet = "Adamant.WalletService.wallet" + + private init() {} + } +} + + +// MARK: - UI +extension Notification.Name { + struct WalletViewController { + static let heightUpdated = Notification.Name("adamant.walletViewController") + + private init() {} + } +} + +protocol WalletViewController { + var viewController: UIViewController { get } + var height: CGFloat { get } + var service: WalletService? { get } +} + + +// MARK: - Wallet Service +protocol WalletService: class { + // MARK: Currency + static var currencySymbol: String { get } + static var currencyLogo: UIImage { 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 } + + // MARK: State + var wallet: WalletAccount? { get } + var state: WalletServiceState { get } + var enabled: Bool { get } + + // MARK: Logic + func update() + + // MARK: Account UI + var walletViewController: WalletViewController { get } + + // MARK: Tools + func validate(address: String) -> AddressValidationResult + func getWalletAddress(byAdamantAddress address: String, completion: @escaping (WalletServiceResult) -> Void) +} + +protocol SwinjectDependentService: WalletService { + func injectDependencies(from container: Container) +} + +protocol InitiatedWithPassphraseService: WalletService { + func initWallet(withPassphrase: String, completion: @escaping (WalletServiceResult) -> Void) +} + +protocol WalletServiceWithTransfers: WalletService { + func transferListViewController() -> UIViewController +} + +// MARK: Send + +protocol WalletServiceWithSend: WalletService { + var transactionFeeUpdated: Notification.Name { get } + + var transactionFee : Decimal { get } + func transferViewController() -> UIViewController +} + +protocol WalletServiceSimpleSend: WalletServiceWithSend { + func sendMoney(recipient: String, amount: Decimal, comments: String, completion: @escaping (WalletServiceSimpleResult) -> Void) +} + +protocol WalletServiceTwoStepSend: WalletServiceWithSend { + associatedtype T: RawTransaction + + func createTransaction(recipient: String, amount: Decimal, comments: String, completion: @escaping (WalletServiceResult) -> Void) + func sendTransaction(_ transaction: T, completion: @escaping (WalletServiceResult) -> Void) +} + +protocol RawTransaction { + var txHash: String? { get } +} diff --git a/Adamant/Wallets/WalletViewControllerBase.swift b/Adamant/Wallets/WalletViewControllerBase.swift new file mode 100644 index 000000000..a72ae8740 --- /dev/null +++ b/Adamant/Wallets/WalletViewControllerBase.swift @@ -0,0 +1,234 @@ +// +// WalletViewControllerBase.swift +// Adamant +// +// Created by Anokhov Pavel on 12.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import UIKit +import Eureka + +extension String.adamantLocalized { + struct wallets { + + private init() {} + } +} + +class WalletViewControllerBase: FormViewController, WalletViewController { + // MARK: - Rows + enum BaseRows { + case address, balance, send + + var tag: String { + switch self { + case .address: return "a" + case .balance: return "b" + case .send: return "s" + } + } + + var localized: String { + switch self { + case .address: return NSLocalizedString("AccountTab.Row.Address", comment: "Account tab: 'Address' row") + case . balance: return NSLocalizedString("AccountTab.Row.Balance", comment: "Account tab: Balance row title") + case .send: return NSLocalizedString("AccountTab.Row.SendTokens", comment: "Account tab: 'Send tokens' button") + } + } + } + + private let cellIdentifier = "cell" + + + // MARK: - Dependencies + + var dialogService: DialogService! + + + // MARK: - Properties, WalletViewController + + var viewController: UIViewController { return self } + var height: CGFloat { return tableView.frame.origin.y + tableView.contentSize.height } + + var service: WalletService? + + // MARK: - IBOutlets + + @IBOutlet weak var walletTitleLabel: UILabel! + + + // MARK: - Lifecycle + + override func viewDidLoad() { + super.viewDidLoad() + + let section = Section() + + // MARK: Address + let addressRow = LabelRow() { + $0.tag = BaseRows.address.tag + $0.title = BaseRows.address.localized + $0.cell.selectionStyle = .gray + + if let wallet = service?.wallet { + $0.value = wallet.address + } + }.cellUpdate { (cell, _) in + cell.accessoryType = .disclosureIndicator + }.onCellSelection { [weak self] (_, _) in + let completion = { [weak self] in + guard let tableView = self?.tableView, let indexPath = tableView.indexPathForSelectedRow else { + return + } + + tableView.deselectRow(at: indexPath, animated: true) + } + + if let address = self?.service?.wallet?.address { + + let contentType = ShareContentType.address + self?.dialogService.presentShareAlertFor(string: address, + types: contentType.shareTypes(sharingTip: address), + excludedActivityTypes: contentType.excludedActivityTypes, + animated: true, + completion: completion) + } + } + + section.append(addressRow) + + // MARK: Balance + let balanceRow = AlertLabelRow() { [weak self] in + $0.tag = BaseRows.balance.tag + $0.title = BaseRows.balance.localized + + if let alertLabel = $0.cell.alertLabel { + alertLabel.backgroundColor = UIColor.adamant.primary + alertLabel.textColor = UIColor.white + alertLabel.clipsToBounds = true + alertLabel.textInsets = UIEdgeInsets(top: 1, left: 5, bottom: 1, right: 5) + + if let count = self?.service?.wallet?.notifications, count > 0 { + alertLabel.text = String(count) + } else { + alertLabel.isHidden = true + } + } + + if let service = self?.service, let wallet = service.wallet { + let symbol = type(of: service).currencySymbol + $0.value = AdamantBalanceFormat.full.format(wallet.balance, withCurrencySymbol: symbol) + } else { + $0.value = "0" + } + } + + if service is WalletServiceWithTransfers { + balanceRow.cell.selectionStyle = .gray + balanceRow.cellUpdate { (cell, _) in + cell.accessoryType = .disclosureIndicator + }.onCellSelection { [weak self] (_, _) in + guard let service = self?.service as? WalletServiceWithTransfers else { + return + } + + self?.navigationController?.pushViewController(service.transferListViewController(), animated: true ) + } + } + + section.append(balanceRow) + + // MARK: Send + if service is WalletServiceWithSend { + let sendRow = LabelRow() { + $0.tag = BaseRows.send.tag + $0.title = BaseRows.send.localized + $0.cell.selectionStyle = .gray + }.cellUpdate { (cell, _) in + cell.accessoryType = .disclosureIndicator + }.onCellSelection { [weak self] (_, _) in + guard let service = self?.service as? WalletServiceWithSend else { + return + } + + let vc = service.transferViewController() + if let v = vc as? TransferViewControllerBase { + v.delegate = self + } + + if let nav = self?.navigationController { + nav.pushViewController(vc, animated: true) + } else { + self?.present(vc, animated: true) + } + } + + section.append(sendRow) + } + + form.append(section) + + // MARK: Notification + if let service = service { + let callback = { [weak self] (notification: Notification) in + guard let wallet = notification.userInfo?[AdamantUserInfoKey.WalletService.wallet] as? WalletAccount else { + return + } + + if let row: AlertLabelRow = self?.form.rowBy(tag: BaseRows.balance.tag) { + let symbol = type(of: service).currencySymbol + row.value = AdamantBalanceFormat.full.format(wallet.balance, withCurrencySymbol: symbol) + + if wallet.notifications > 0 { + row.cell.alertLabel.text = String(wallet.notifications) + + if row.cell.alertLabel.isHidden { + row.cell.alertLabel.isHidden = false + } + } else { + row.cell.alertLabel.isHidden = true + } + + row.updateCell() + } + } + + NotificationCenter.default.addObserver(forName: service.walletUpdatedNotification, + object: service, + queue: OperationQueue.main, + using: callback) + } + } + + 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) + } + + override func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + return UIView() + } +} + + +extension WalletViewControllerBase: TransferViewControllerDelegate { + func transferViewControllerDidFinishTransfer(_ viewController: TransferViewControllerBase) { + if let nav = navigationController, nav.topViewController == viewController { + DispatchQueue.main.async { + nav.popViewController(animated: true) + } + } else if presentedViewController == viewController { + DispatchQueue.main.async { [weak self] in + self?.dismiss(animated: true, completion: nil) + } + } + } +} diff --git a/Adamant/Wallets/WalletViewControllerBase.xib b/Adamant/Wallets/WalletViewControllerBase.xib new file mode 100644 index 000000000..546122305 --- /dev/null +++ b/Adamant/Wallets/WalletViewControllerBase.xib @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Adamant/Wallets/WalletsRoutes.swift b/Adamant/Wallets/WalletsRoutes.swift new file mode 100644 index 000000000..f204d3ba2 --- /dev/null +++ b/Adamant/Wallets/WalletsRoutes.swift @@ -0,0 +1,16 @@ +// +// WalletsRoutes.swift +// Adamant +// +// Created by Anokhov Pavel on 14.08.2018. +// Copyright © 2018 Adamant. All rights reserved. +// + +import Foundation + +extension AdamantScene { + struct Wallets { + + private init() { } + } +} diff --git a/Adamant/Helpers/JSModels.swift b/AdamantTests/Core/JSModels.swift similarity index 99% rename from Adamant/Helpers/JSModels.swift rename to AdamantTests/Core/JSModels.swift index 6007364f1..ba3b3afce 100644 --- a/Adamant/Helpers/JSModels.swift +++ b/AdamantTests/Core/JSModels.swift @@ -8,7 +8,7 @@ import Foundation import JavaScriptCore - +@testable import Adamant // MARK: Keypair diff --git a/AdamantTests/Parsing/ParsingModelsTests.swift b/AdamantTests/Parsing/ParsingModelsTests.swift index 478a50ce9..b7e20ef6b 100644 --- a/AdamantTests/Parsing/ParsingModelsTests.swift +++ b/AdamantTests/Parsing/ParsingModelsTests.swift @@ -57,7 +57,7 @@ class ParsingModelsTests: XCTestCase { } func testAccount() { - let t: Account = TestTools.LoadJsonAndDecode(filename: "Account") + let t: AdamantAccount = TestTools.LoadJsonAndDecode(filename: "Account") XCTAssertEqual(t.address, "U2279741505997340299") XCTAssertEqual(t.unconfirmedBalance, Decimal(0.345)) diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..70566f2d0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ +GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/Podfile b/Podfile index e41527905..987fd4c2f 100644 --- a/Podfile +++ b/Podfile @@ -10,14 +10,16 @@ target 'Adamant' do pod 'ReachabilitySwift' # Network status pod 'Haring' # Markdown parser pod 'DateToolsSwift' # Date formatter tools + pod 'ProcedureKit' # Async programming tools # UI pod 'FreakingSimpleRoundImageView' # Round avatars pod 'FTIndicator' # Notifications and activity indicator pod 'Eureka' # Forms - pod 'MessageKit' # Chat UI + pod 'MessageKit', :git => 'https://github.com/RealBonus/MessageKit', :branch => 'temp/customMessageKind_swift42' # Chat UI, swift 4.2 pod 'MyLittlePinpad' # Pinpad pod 'PMAlertController' # Custom alert controller + pod 'Parchment' # Paging menu # QR pod 'EFQRCode' # QR generator @@ -27,6 +29,8 @@ target 'Adamant' do pod 'RNCryptor' # Cryptor pod 'CryptoSwift' # MD5 hash pod 'libsodium' # Sodium crypto library + pod 'web3swift' # ETH Web3 Swift Port + pod 'Lisk', :git => 'https://github.com/boyarkin-anton/lisk-swift.git', :branch => 'dev' # LSK # Utility pod 'ByteBackpacker' # Utility to pack value types into a Byte array diff --git a/Podfile.lock b/Podfile.lock index d68448e72..356d827b4 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,11 +1,15 @@ PODS: - Alamofire (4.7.3) + - BigInt (3.1.0): + - SipHash (~> 1.2) - ByteBackpacker (1.2.1) - - CryptoSwift (0.10.0) + - CryptoSwift (0.12.0) - DateToolsSwift (4.0.0) - - EFQRCode (4.2.2) - - Eureka (4.2.0) - - FreakingSimpleRoundImageView (1.2.3) + - Ed25519 (1.1.0): + - libCEd25519 (~> 1.1.0) + - EFQRCode (4.3.0) + - Eureka (4.3.0) + - FreakingSimpleRoundImageView (1.3) - FTIndicator (1.2.9): - FTIndicator/FTNotificationIndicator (= 1.2.9) - FTIndicator/FTProgressIndicator (= 1.2.9) @@ -13,16 +17,46 @@ PODS: - FTIndicator/FTNotificationIndicator (1.2.9) - FTIndicator/FTProgressIndicator (1.2.9) - FTIndicator/FTToastIndicator (1.2.9) - - Haring (2.0.8) + - Haring (2.1) - KeychainAccess (3.1.1) + - libCEd25519 (1.1.0) - libsodium (1.0.12) - - MessageKit (1.0.0) - - MyLittlePinpad (0.2.5) - - PMAlertController (3.4.0) - - QRCodeReader.swift (8.2.0) - - ReachabilitySwift (4.1.0) + - Lisk (1.0.0): + - Ed25519 (~> 1.1.0) + - MessageInputBar/Core (0.4.0) + - MessageKit (1.0.0): + - MessageInputBar/Core + - MyLittlePinpad (0.3) + - Parchment (1.4.1) + - PMAlertController (3.5.0) + - ProcedureKit (4.5.0): + - ProcedureKit/Standard (= 4.5.0) + - ProcedureKit/Standard (4.5.0) + - PromiseKit (6.4.1): + - PromiseKit/CorePromise (= 6.4.1) + - PromiseKit/Foundation (= 6.4.1) + - PromiseKit/UIKit (= 6.4.1) + - PromiseKit/CorePromise (6.4.1) + - PromiseKit/Foundation (6.4.1): + - PromiseKit/CorePromise + - PromiseKit/UIKit (6.4.1): + - PromiseKit/CorePromise + - QRCodeReader.swift (9.0.1) + - ReachabilitySwift (4.3.0) + - Result (4.0.0) - RNCryptor (5.0.3) - - Swinject (2.4.1) + - scrypt (2.0): + - CryptoSwift (~> 0.11) + - secp256k1_ios (0.1.3) + - SipHash (1.2.2) + - Swinject (2.5.0) + - web3swift (1.1.9): + - BigInt (~> 3.1) + - CryptoSwift (~> 0.11) + - PromiseKit (~> 6.3) + - Result (~> 4.0) + - scrypt (~> 2.0) + - secp256k1_ios (~> 0.1) DEPENDENCIES: - Alamofire @@ -36,55 +70,99 @@ DEPENDENCIES: - Haring - KeychainAccess - libsodium - - MessageKit + - Lisk (from `https://github.com/boyarkin-anton/lisk-swift.git`, branch `dev`) + - MessageKit (from `https://github.com/RealBonus/MessageKit`, branch `temp/customMessageKind_swift42`) - MyLittlePinpad + - Parchment - PMAlertController + - ProcedureKit - QRCodeReader.swift - ReachabilitySwift - RNCryptor - Swinject + - web3swift SPEC REPOS: https://github.com/cocoapods/specs.git: - Alamofire + - BigInt - ByteBackpacker - CryptoSwift - DateToolsSwift + - Ed25519 - EFQRCode - Eureka - FreakingSimpleRoundImageView - FTIndicator - Haring - KeychainAccess + - libCEd25519 - libsodium - - MessageKit + - MessageInputBar - MyLittlePinpad + - Parchment - PMAlertController + - ProcedureKit + - PromiseKit - QRCodeReader.swift - ReachabilitySwift + - Result - RNCryptor + - scrypt + - secp256k1_ios + - SipHash - Swinject + - web3swift + +EXTERNAL SOURCES: + Lisk: + :branch: dev + :git: https://github.com/boyarkin-anton/lisk-swift.git + MessageKit: + :branch: temp/customMessageKind_swift42 + :git: https://github.com/RealBonus/MessageKit + +CHECKOUT OPTIONS: + Lisk: + :commit: 952fea02d6076668f68eb252ec5bd8bf4e656960 + :git: https://github.com/boyarkin-anton/lisk-swift.git + MessageKit: + :commit: 4409d10a7dcf390abbb696d280ec02025e89f4e7 + :git: https://github.com/RealBonus/MessageKit SPEC CHECKSUMS: Alamofire: c7287b6e5d7da964a70935e5db17046b7fde6568 + BigInt: 76b5dfdfa3e2e478d4ffdf161aeede5502e2742f ByteBackpacker: df001da117faacdbf09b69a116ec480f6651c9ec - CryptoSwift: 6c778d69282bed3b4e975ff97a79d074f20bb011 + CryptoSwift: 1c07ca50843dd48bc54e6ea53d7a4dba3b645716 DateToolsSwift: 875d97ff9e3a5d54abdd67a269b3f51c757b71ab - EFQRCode: 203846c7b135b5f7c1d18bb7923bc32a74c8b363 - Eureka: 0748c7bbb2560130e43c7bfa83e99c98854a1f42 - FreakingSimpleRoundImageView: 66a0f0fa1722faf63b662ba61f8441b09436a428 + Ed25519: d84c847d8ffbb2f6fec71479abbb8fa3dfe12578 + EFQRCode: 51b67bd10952da9ef619ae0a396abb746b94263f + Eureka: 6d711cb30ca333b4bc893110285a722ae3840114 + FreakingSimpleRoundImageView: 0d687cb05da8684e85c4c2ae9945bafcbe89d2a2 FTIndicator: f7f071fd159e5befa1d040a9ef2e3ab53fa9322c - Haring: d2a4cfc00dfb63836dffc93e45919369f850e134 + Haring: b3a8495c073a016f03296d67debc609a7ecf7710 KeychainAccess: 7bd430028059754a3debab3cfc0bd1fc7fb85df3 + libCEd25519: 45de00a0944cb2d51e64fcef67bd9989c42d20ca libsodium: 9a8faa5ef2fa0d2d57bd7f7d79bf8fb7c1a9f0ea - MessageKit: 16160036b16476b04bd0e9f2c7ce67e662e503e4 - MyLittlePinpad: ec1d9990d9715eab48205ecc08f4ff359ead521b - PMAlertController: efb781925d741d50e0200018a00c53cecb8b4910 - QRCodeReader.swift: 003eb32f18a5a675b936ec82ba0ff368cddbff45 - ReachabilitySwift: 6849231cd4e06559f3b9ef4a97a0a0f96d41e09f + Lisk: a900424110c208c425102ee8b6fd60d9b6b878c3 + MessageInputBar: d46ecc5b355d13433b8367dcd200e05a6564f0ed + MessageKit: 0aaaa6115a9b4497a65fcdfed5dad270fbc8f2b2 + MyLittlePinpad: 5d57cb5a092ddbeb5c4e6fa0b46df83bd3e88b0c + Parchment: e6c2a3a0063739382dd207e21d48fe5e81f3324a + PMAlertController: 06dab8160066fc4ce991c880e3722cd403e2926a + ProcedureKit: 023e4600e921fe0b8d94f2d19ed680df83aa28ba + PromiseKit: 4c76a6506638034e3d7bede97b2ff7743f7bd2dc + QRCodeReader.swift: 96292a5612fbc2fd9a0b26f93fa5164c8d02f59d + ReachabilitySwift: 408477d1b6ed9779dba301953171e017c31241f3 + Result: 7645bb3f50c2ce726dd0ff2fa7b6f42bbe6c3713 RNCryptor: c93d19029dcf7ff160aca0f24d6c9e7b0d82f664 - Swinject: f7f15a9672e99328c1f07f277091087917215700 + scrypt: 3fe5b1a3b0976f97cd87488673a8f7c65708cc84 + secp256k1_ios: ac9ef04e761f43c58012b28548afa91493761f17 + SipHash: fad90a4683e420c52ef28063063dbbce248ea6d4 + Swinject: 82cdb851f63f91bba974e3eca1d69780f2f7677e + web3swift: 4895c765c9858eb4737ef222eb8643bb5fc7fdb3 -PODFILE CHECKSUM: 218a26e444ddb4fecbf6765ec36ab3f38049a1a7 +PODFILE CHECKSUM: 17981ca7dbf7440ef8c4e90c4b889b8aa510e6ad COCOAPODS: 1.6.0.beta.1