Skip to content

Commit

Permalink
Refactor address security issues and integrate to send flow
Browse files Browse the repository at this point in the history
  • Loading branch information
ealymbaev committed Jan 29, 2025
1 parent 61ed2ff commit d814519
Show file tree
Hide file tree
Showing 12 changed files with 217 additions and 135 deletions.
12 changes: 6 additions & 6 deletions UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2592,8 +2592,8 @@
D06A171C2BA1B1BC0081E312 /* FeeSettingsViewHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06A171A2BA1B1BC0081E312 /* FeeSettingsViewHelper.swift */; };
D06B302C2B6A120E0012A161 /* LegacyFeeSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06B302B2B6A120E0012A161 /* LegacyFeeSettingsViewModel.swift */; };
D06B302D2B6A120E0012A161 /* LegacyFeeSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06B302B2B6A120E0012A161 /* LegacyFeeSettingsViewModel.swift */; };
D06F60032D195FBC0033A288 /* AddressSecurityCheckerChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06F60012D195FBC0033A288 /* AddressSecurityCheckerChain.swift */; };
D06F60052D195FBC0033A288 /* AddressSecurityCheckerChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06F60012D195FBC0033A288 /* AddressSecurityCheckerChain.swift */; };
D06F60032D195FBC0033A288 /* AddressSecurityIssueType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06F60012D195FBC0033A288 /* AddressSecurityIssueType.swift */; };
D06F60052D195FBC0033A288 /* AddressSecurityIssueType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06F60012D195FBC0033A288 /* AddressSecurityIssueType.swift */; };
D06F60082D195FE90033A288 /* AddressSecurityCheckerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06F60072D195FE90033A288 /* AddressSecurityCheckerFactory.swift */; };
D06F60092D195FE90033A288 /* AddressSecurityCheckerFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D06F60072D195FE90033A288 /* AddressSecurityCheckerFactory.swift */; };
D07157DB2A2DD968006F141F /* SendTronModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = D07157DA2A2DD968006F141F /* SendTronModule.swift */; };
Expand Down Expand Up @@ -4568,7 +4568,7 @@
D066A45E2C6CC7E200074E35 /* WelcomeScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeScreenViewModel.swift; sourceTree = "<group>"; };
D06A171A2BA1B1BC0081E312 /* FeeSettingsViewHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeeSettingsViewHelper.swift; sourceTree = "<group>"; };
D06B302B2B6A120E0012A161 /* LegacyFeeSettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyFeeSettingsViewModel.swift; sourceTree = "<group>"; };
D06F60012D195FBC0033A288 /* AddressSecurityCheckerChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressSecurityCheckerChain.swift; sourceTree = "<group>"; };
D06F60012D195FBC0033A288 /* AddressSecurityIssueType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressSecurityIssueType.swift; sourceTree = "<group>"; };
D06F60072D195FE90033A288 /* AddressSecurityCheckerFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressSecurityCheckerFactory.swift; sourceTree = "<group>"; };
D07157DA2A2DD968006F141F /* SendTronModule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTronModule.swift; sourceTree = "<group>"; };
D07157DD2A2DDA09006F141F /* SendTronService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTronService.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -7125,7 +7125,7 @@
children = (
D05C8E892D22931A006EE778 /* ChainalysisAddressValidator.swift */,
D0F766CE2D1AD36200E409AD /* SpamAddressDetector.swift */,
D06F60012D195FBC0033A288 /* AddressSecurityCheckerChain.swift */,
D06F60012D195FBC0033A288 /* AddressSecurityIssueType.swift */,
58AAA7305EF2A12D3DC82B32 /* AddressParserChain.swift */,
58AAAAC62C5B05463D339BCC /* EvmAddressParserItem.swift */,
58AAA9A4761030BE9F60C85E /* UdnAddressParserItem.swift */,
Expand Down Expand Up @@ -9927,7 +9927,7 @@
D07157DC2A2DD968006F141F /* SendTronModule.swift in Sources */,
11B35A82532EC55909EFBAD8 /* LaunchScreen.swift in Sources */,
6BB14F762C01D04200E879B2 /* CheckBoxUiView.swift in Sources */,
D06F60052D195FBC0033A288 /* AddressSecurityCheckerChain.swift in Sources */,
D06F60052D195FBC0033A288 /* AddressSecurityIssueType.swift in Sources */,
6BCD53052A161F4100993F20 /* ICloudBackupTermsViewController.swift in Sources */,
11B354FA6F6BF59F64560590 /* CoinTreasuriesViewModel.swift in Sources */,
11B35F6092E0950714E277E4 /* PostCell.swift in Sources */,
Expand Down Expand Up @@ -11422,7 +11422,7 @@
D087627629815DAE00E6FFD4 /* ChooseWatchViewModel.swift in Sources */,
11B35ED9D5F95988E9335440 /* CoinAnalyticsModule.swift in Sources */,
D389BC4C2C0DDCF500724504 /* MarketAdvancedSearchBlockchainsView.swift in Sources */,
D06F60032D195FBC0033A288 /* AddressSecurityCheckerChain.swift in Sources */,
D06F60032D195FBC0033A288 /* AddressSecurityIssueType.swift in Sources */,
D0118E4B2B7CC63300D55CE6 /* ResendBitcoinViewController.swift in Sources */,
6BB14F752C01D04200E879B2 /* CheckBoxUiView.swift in Sources */,
11B3551E5E9A6D167F7BA078 /* LaunchScreenManager.swift in Sources */,
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import MarketKit

enum AddressSecurityIssueType: CaseIterable, Identifiable {
case phishing
case sanctioned

var id: Self {
self
}

var title: String {
switch self {
case .phishing: return "send.address.phishing_check".localized
case .sanctioned: return "send.address.blacklist_check".localized
}
}

var caution: CautionNew {
switch self {
case .phishing: return CautionNew(title: "send.address.phishing.title".localized, text: "send.address.phishing.description".localized, type: .error)
case .sanctioned: return CautionNew(title: "send.address.blacklist.title".localized, text: "send.address.blacklist.description".localized, type: .error)
}
}

func supports(blockchainType: BlockchainType) -> Bool {
switch self {
case .phishing: return EvmBlockchainManager.blockchainTypes.contains(blockchainType)
case .sanctioned: return true
}
}

static func issueTypes(blockchainType: BlockchainType) -> [Self] {
allCases.filter { $0.supports(blockchainType: blockchainType) }
}
}

struct ResolvedAddress: Hashable {
let address: String
let issueTypes: [AddressSecurityIssueType]
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,21 @@ import RxSwift

class ChainalysisAddressValidator {
private let baseUrl = "https://public.chainalysis.com/api/v1/address/"
private let networkManager: NetworkManager
private let networkManager = App.shared.networkManager
private let headers: HTTPHeaders

init(networkManager: NetworkManager) {
self.networkManager = networkManager

init() {
headers = HTTPHeaders([
HTTPHeader(name: "X-API-KEY", value: AppConfig.chainalysisApiKey),
HTTPHeader(name: "Accept", value: "application/json"),
])
}
}

extension ChainalysisAddressValidator: IAddressSecurityCheckerItem {
func handle(address: Address) -> Single<AddressSecurityCheckerChain.SecurityIssue?> {
let request = networkManager.session.request("\(baseUrl)\(address.raw)", headers: headers)
let response: Single<ChainalysisAddressValidatorResponse> = networkManager.single(request: request)

return response.map {
if $0.identifications.isEmpty {
return nil
}

return .sanctioned(description: "Sanctioned address. \($0.identifications.count) identifications found.")
}
extension ChainalysisAddressValidator: IAddressSecurityChecker {
func check(address: Address) async throws -> Bool {
let response: ChainalysisAddressValidatorResponse = try await networkManager.fetch(url: "\(baseUrl)\(address.raw)", headers: headers)
return !response.identifications.isEmpty
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import RxSwift

class SpamAddressDetector {
private let spamAddressManager: SpamAddressManager

Expand All @@ -8,15 +6,8 @@ class SpamAddressDetector {
}
}

extension SpamAddressDetector: IAddressSecurityCheckerItem {
func handle(address: Address) -> Single<AddressSecurityCheckerChain.SecurityIssue?> {
var result: AddressSecurityCheckerChain.SecurityIssue? = nil

let spamAddress = spamAddressManager.find(address: address.raw.uppercased())
if let spamAddress {
result = .spam(transactionHash: spamAddress.transactionHash.hs.hexString)
}

return Single.just(result)
extension SpamAddressDetector: IAddressSecurityChecker {
func check(address: Address) async throws -> Bool {
spamAddressManager.find(address: address.raw.uppercased()) != nil
}
}
Original file line number Diff line number Diff line change
@@ -1,32 +1,12 @@
import MarketKit
protocol IAddressSecurityChecker {
func check(address: Address) async throws -> Bool
}

enum AddressSecurityCheckerFactory {
static func securityCheckerChainHandlers(blockchainType: BlockchainType) -> [IAddressSecurityCheckerItem] {
switch blockchainType {
case .ethereum, .gnosis, .fantom, .polygon, .arbitrumOne, .avalanche, .optimism, .binanceSmartChain, .base:
let evmAddressSecurityCheckerItem = SpamAddressDetector()
let chainalysisAddressValidator = ChainalysisAddressValidator(networkManager: App.shared.networkManager)

var handlers = [IAddressSecurityCheckerItem]()
handlers.append(evmAddressSecurityCheckerItem)
handlers.append(chainalysisAddressValidator)

return handlers
default:
return []
static func addressSecurityChecker(type: AddressSecurityIssueType) -> IAddressSecurityChecker {
switch type {
case .phishing: return SpamAddressDetector()
case .sanctioned: return ChainalysisAddressValidator()
}
}

static func securityCheckerChain(blockchainType: BlockchainType?) -> AddressSecurityCheckerChain {
if let blockchainType {
return AddressSecurityCheckerChain().append(handlers: securityCheckerChainHandlers(blockchainType: blockchainType))
}

var handlers = [IAddressSecurityCheckerItem]()
for blockchainType in BlockchainType.supported {
handlers.append(contentsOf: securityCheckerChainHandlers(blockchainType: blockchainType))
}

return AddressSecurityCheckerChain().append(handlers: handlers)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,43 @@ struct AddressView: View {
text: $viewModel.address,
result: $viewModel.addressResult
)
.padding(.bottom, .margin12)

switch viewModel.state {
case .checking, .valid:
ListSection {
VStack(spacing: 0) {
ForEach(viewModel.issueTypes) { type in
checkView(title: type.title, state: viewModel.checkStates[type] ?? .notAvailable)
}
}
}
.themeListStyle(.bordered)

let cautions = viewModel.issueTypes.filter { viewModel.checkStates[$0] == .detected }.map { $0.caution }

if !cautions.isEmpty {
VStack(spacing: .margin12) {
ForEach(cautions.indices, id: \.self) { index in
HighlightedTextView(caution: cautions[index])
}
}
}
default:
EmptyView()
}
}
.padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin16, trailing: .margin16))
}
} bottomContent: {
let (title, disabled, showProgress) = buttonState()

Button(action: {
guard case let .valid(address) = viewModel.state else {
guard case let .valid(resolvedAddress) = viewModel.state else {
return
}

onFinish(ResolvedAddress(address: address))
onFinish(resolvedAddress)
}) {
HStack(spacing: .margin8) {
if showProgress {
Expand All @@ -51,16 +76,46 @@ struct AddressView: View {
}
}

@ViewBuilder private func checkView(title: String, state: AddressViewModel.CheckState) -> some View {
HStack(spacing: .margin8) {
HStack(spacing: 2) {
Text(title).textSubhead2()
Image("crown_20").themeIcon(color: .themeJacob)
}

Spacer()

switch state {
case .checking:
ProgressView()
case .clear:
Text("send.address.check.clear".localized).textSubhead2(color: .themeRemus)
case .detected:
Text("send.address.check.detected".localized).textSubhead2(color: .themeLucian)
case .notAvailable:
Text("n/a".localized).textSubhead2()
case .locked:
Image("lock_20").themeIcon()
}
}
.padding(.horizontal, .margin16)
.frame(minHeight: 40)
}

private func buttonState() -> (String, Bool, Bool) {
let title: String
var disabled = true
var showProgress = false

if case .empty = viewModel.state {
title = "send.enter_address".localized
} else if case .invalid = viewModel.state {
title = "send.invalid_address".localized
} else {
switch viewModel.state {
case .empty:
title = "send.address.enter_address".localized
case .invalid:
title = "send.address.invalid_address".localized
case .checking:
title = "send.address.checking".localized
showProgress = true
case .valid:
title = "send.next_button".localized
disabled = false
}
Expand Down
Loading

0 comments on commit d814519

Please sign in to comment.