Skip to content

Commit

Permalink
Add Chainalysis sanctioned address check
Browse files Browse the repository at this point in the history
  • Loading branch information
esen committed Dec 30, 2024
1 parent 00a802c commit 2fa88c4
Show file tree
Hide file tree
Showing 14 changed files with 95 additions and 19 deletions.
1 change: 1 addition & 0 deletions .github/workflows/deploy_appstore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ jobs:
XCCONFIG_PROD_OPEN_SEA_API_KEY: ${{ secrets.XCCONFIG_PROD_OPEN_SEA_API_KEY }}
XCCONFIG_PROD_TRONGRID_API_KEY: ${{ secrets.XCCONFIG_PROD_TRONGRID_API_KEY }}
XCCONFIG_PROD_UNSTOPPABLE_DOMAINS_API_KEY: ${{ secrets.XCCONFIG_PROD_UNSTOPPABLE_DOMAINS_API_KEY }}
XCCONFIG_PROD_CHAINALYSIS_API_KEY: ${{ secrets.XCCONFIG_PROD_CHAINALYSIS_API_KEY }}
XCCONFIG_PROD_ONE_INCH_API_KEY: ${{ secrets.XCCONFIG_PROD_ONE_INCH_API_KEY }}
XCCONFIG_PROD_ONE_INCH_COMMISSION: ${{ secrets.XCCONFIG_PROD_ONE_INCH_COMMISSION }}
XCCONFIG_PROD_ONE_INCH_COMMISSION_ADDRESS: ${{ secrets.XCCONFIG_PROD_ONE_INCH_COMMISSION_ADDRESS }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/deploy_dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ jobs:
XCCONFIG_DEV_OPEN_SEA_API_KEY: ${{ secrets.XCCONFIG_DEV_OPEN_SEA_API_KEY }}
XCCONFIG_DEV_TRONGRID_API_KEY: ${{ secrets.XCCONFIG_DEV_TRONGRID_API_KEY }}
XCCONFIG_DEV_UNSTOPPABLE_DOMAINS_API_KEY: ${{ secrets.XCCONFIG_DEV_UNSTOPPABLE_DOMAINS_API_KEY }}
XCCONFIG_DEV_CHAINALYSIS_API_KEY: ${{ secrets.XCCONFIG_DEV_CHAINALYSIS_API_KEY }}
XCCONFIG_DEV_ONE_INCH_API_KEY: ${{ secrets.XCCONFIG_DEV_ONE_INCH_API_KEY }}
XCCONFIG_DEV_ONE_INCH_COMMISSION: ${{ secrets.XCCONFIG_DEV_ONE_INCH_COMMISSION }}
XCCONFIG_DEV_ONE_INCH_COMMISSION_ADDRESS: ${{ secrets.XCCONFIG_DEV_ONE_INCH_COMMISSION_ADDRESS }}
Expand Down
6 changes: 6 additions & 0 deletions UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2552,6 +2552,8 @@
D0532CC52B149E450015DF40 /* WatchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0532CC32B149E450015DF40 /* WatchService.swift */; };
D054DAE32BE5123F0040B7C9 /* InitialTransactionSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D054DAE22BE5123F0040B7C9 /* InitialTransactionSettings.swift */; };
D054DAE42BE5123F0040B7C9 /* InitialTransactionSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D054DAE22BE5123F0040B7C9 /* InitialTransactionSettings.swift */; };
D05C8E8A2D22931A006EE778 /* ChainalysisAddressValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05C8E892D22931A006EE778 /* ChainalysisAddressValidator.swift */; };
D05C8E8B2D22931A006EE778 /* ChainalysisAddressValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05C8E892D22931A006EE778 /* ChainalysisAddressValidator.swift */; };
D05E968D2A25D6C6002CCD71 /* Trc20Adapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05E968C2A25D6C6002CCD71 /* Trc20Adapter.swift */; };
D05E968E2A25D6C6002CCD71 /* Trc20Adapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05E968C2A25D6C6002CCD71 /* Trc20Adapter.swift */; };
D05E96902A261D82002CCD71 /* TronTransactionAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D05E968F2A261D82002CCD71 /* TronTransactionAdapter.swift */; };
Expand Down Expand Up @@ -4536,6 +4538,7 @@
D0532CC02B149E110015DF40 /* WatchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchViewController.swift; sourceTree = "<group>"; };
D0532CC32B149E450015DF40 /* WatchService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchService.swift; sourceTree = "<group>"; };
D054DAE22BE5123F0040B7C9 /* InitialTransactionSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InitialTransactionSettings.swift; sourceTree = "<group>"; };
D05C8E892D22931A006EE778 /* ChainalysisAddressValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChainalysisAddressValidator.swift; sourceTree = "<group>"; };
D05E968C2A25D6C6002CCD71 /* Trc20Adapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trc20Adapter.swift; sourceTree = "<group>"; };
D05E968F2A261D82002CCD71 /* TronTransactionAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TronTransactionAdapter.swift; sourceTree = "<group>"; };
D05E96922A261DC1002CCD71 /* TronTransactionConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TronTransactionConverter.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -7102,6 +7105,7 @@
58AAA0C31F14444C22ACF393 /* Address */ = {
isa = PBXGroup;
children = (
D05C8E892D22931A006EE778 /* ChainalysisAddressValidator.swift */,
D0F766CE2D1AD36200E409AD /* SpamAddressDetector.swift */,
D06F60012D195FBC0033A288 /* AddressSecurityCheckerChain.swift */,
58AAA7305EF2A12D3DC82B32 /* AddressParserChain.swift */,
Expand Down Expand Up @@ -9878,6 +9882,7 @@
D087627729815DAE00E6FFD4 /* ChooseWatchViewModel.swift in Sources */,
11B35FBC1AFDCF0DB8362C88 /* CoinAnalyticsModule.swift in Sources */,
D0118E4C2B7CC63300D55CE6 /* ResendBitcoinViewController.swift in Sources */,
D05C8E8A2D22931A006EE778 /* ChainalysisAddressValidator.swift in Sources */,
D389BC4D2C0DDCF500724504 /* MarketAdvancedSearchBlockchainsView.swift in Sources */,
11B3518BEA8865CADA5DA684 /* LaunchScreenManager.swift in Sources */,
D07157DC2A2DD968006F141F /* SendTronModule.swift in Sources */,
Expand Down Expand Up @@ -11367,6 +11372,7 @@
D36DE0E4272FD887000BC916 /* OneInchService.swift in Sources */,
D05E969A2A26278D002CCD71 /* TronApproveTransactionRecord.swift in Sources */,
D05E969D2A2627AF002CCD71 /* TronContractCallTransactionRecord.swift in Sources */,
D05C8E8B2D22931A006EE778 /* ChainalysisAddressValidator.swift in Sources */,
D36DE0C9272FD864000BC916 /* UniswapProvider.swift in Sources */,
D00DAE452B626C2900F48E1D /* GasPrice.swift in Sources */,
D087627629815DAE00E6FFD4 /* ChooseWatchViewModel.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ private_cloud_container_id = iCloud.io.horizontalsystems.bank-wallet.dev
open_sea_api_key =
unstoppable_domains_api_key =
one_inch_api_key =
chainalysis_api_key =
one_inch_commission =
one_inch_commission_address =
swap_enabled = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ shared_cloud_container_id = iCloud.io.horizontalsystems.bank-wallet.shared
private_cloud_container_id = iCloud.io.horizontalsystems.bank-wallet
open_sea_api_key =
unstoppable_domains_api_key =
chainalysis_api_key =
one_inch_api_key =
one_inch_commission =
one_inch_commission_address =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import RxRelay
import RxSwift

protocol IAddressSecurityCheckerItem: AnyObject {
func handle(address: Address) -> Single<AddressSecurityCheckerChain.SecurityCheckResult>
func handle(address: Address) -> Single<AddressSecurityCheckerChain.SecurityIssue?>
}

class AddressSecurityCheckerChain {
Expand All @@ -24,22 +24,21 @@ extension AddressSecurityCheckerChain {
return self
}

func handle(address: Address) -> Single<[SecurityCheckResult]> {
Single.zip(handlers.map { handler -> Single<SecurityCheckResult> in
func handle(address: Address) -> Single<[SecurityIssue]> {
Single.zip(handlers.map { handler -> Single<SecurityIssue?> in
handler.handle(address: address)
})
.map { $0.compactMap { $0 } }
}
}

extension AddressSecurityCheckerChain {
public enum SecurityCheckResult {
case valid
public enum SecurityIssue {
case spam(transactionHash: String)
case sanctioned(description: String)

public var description: String? {
switch self {
case .valid: return nil
case let .spam(transactionHash): return "Possibly phishing address. Transaction hash: \(transactionHash)"
case let .sanctioned(description): return description
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Alamofire
import Foundation
import HsToolKit
import ObjectMapper
import RxSwift

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

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

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.")
}
}
}

public struct ChainalysisAddressValidatorResponse: ImmutableMappable {
public let identifications: [Identification]

public init(map: Map) throws {
identifications = try map.value("identifications")
}

public struct Identification: ImmutableMappable {
public let category: String
public let name: String?
public let description: String?
public let url: String?

public init(map: Map) throws {
category = try map.value("category")
name = try map.value("name")
description = try map.value("description")
url = try map.value("url")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,12 @@ class SpamAddressDetector {
}

extension SpamAddressDetector: IAddressSecurityCheckerItem {
func handle(address: Address) -> Single<AddressSecurityCheckerChain.SecurityCheckResult> {
let result: AddressSecurityCheckerChain.SecurityCheckResult
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)
} else {
result = .valid
}

return Single.just(result)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ enum AddressSecurityCheckerFactory {
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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ enum AppConfig {
(Bundle.main.object(forInfoDictionaryKey: "OpenSeaApiKey") as? String) ?? ""
}

static var chainalysisApiKey: String {
(Bundle.main.object(forInfoDictionaryKey: "ChainalysisApiKey") as? String) ?? ""
}

static var swapEnabled: Bool {
Bundle.main.object(forInfoDictionaryKey: "SwapEnabled") as? String == "true"
}
Expand Down
2 changes: 2 additions & 0 deletions UnstoppableWallet/UnstoppableWallet/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@
<string>${swap_enabled}</string>
<key>TronGridApiKey</key>
<string>${trongrid_api_key}</string>
<key>ChainalysisApiKey</key>
<string>${chainalysis_api_key}</string>
<key>TwitterBearerToken</key>
<string>${twitter_bearer_token}</string>
<key>UIApplicationSceneManifest</key>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,17 +85,17 @@ class AddressViewModelNew: ObservableObject {
.handle(address: address)
.subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated))
.observeOn(MainScheduler.instance)
.flatMap { [weak self] parsedAddress -> Single<(Address?, [AddressSecurityCheckerChain.SecurityCheckResult])> in
.flatMap { [weak self] parsedAddress -> Single<(Address?, [AddressSecurityCheckerChain.SecurityIssue])> in
guard let _address = parsedAddress, let securityCheckerChain = self?.securityCheckerChain else {
return .just((parsedAddress, []))
}

return securityCheckerChain.handle(address: _address).map { (_address, $0) }
}
.subscribe(
onSuccess: { [weak self] parsedAddress, securityCheckResults in
print("securityCheckResults: \(securityCheckResults)")
self?.sync(parsedAddress, uri: uri, securityCheckResults: securityCheckResults)
onSuccess: { [weak self] parsedAddress, securityIssues in
print("securityIssues: \(securityIssues)")
self?.sync(parsedAddress, uri: uri, securityIssues: securityIssues)
},
onError: { [weak self] in self?.sync($0, text: text) }
)
Expand All @@ -113,13 +113,13 @@ class AddressViewModelNew: ObservableObject {
}
}

private func sync(_ address: Address?, uri: AddressUri?, securityCheckResults: [AddressSecurityCheckerChain.SecurityCheckResult]) {
private func sync(_ address: Address?, uri: AddressUri?, securityIssues: [AddressSecurityCheckerChain.SecurityIssue]) {
guard let address else {
result = .idle
return
}

result = .valid(.init(address: address, uri: uri, securityCheckResults: securityCheckResults))
result = .valid(.init(address: address, uri: uri, securityIssues: securityIssues))
}

private func sync(_ error: Error, text: String) {
Expand Down Expand Up @@ -191,7 +191,7 @@ enum AddressInput {
struct Success: Equatable {
let address: Address
let uri: AddressUri?
let securityCheckResults: [AddressSecurityCheckerChain.SecurityCheckResult]
let securityIssues: [AddressSecurityCheckerChain.SecurityIssue]

static func == (lhs: AddressInput.Success, rhs: AddressInput.Success) -> Bool {
lhs.address == rhs.address && lhs.uri == rhs.uri
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class AddressMultiSwapSettingsViewModel: ObservableObject, IMultiSwapSettingsFie

if let initialAddress {
address = initialAddress.title
addressResult = .valid(.init(address: initialAddress, uri: nil, securityCheckResults: []))
addressResult = .valid(.init(address: initialAddress, uri: nil, securityIssues: []))
}
}

Expand Down
6 changes: 5 additions & 1 deletion fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ XCCONFIG_DEV_OPEN_SEA_API_KEY = ENV["XCCONFIG_DEV_OPEN_SEA_API_KEY"]
XCCONFIG_DEV_TRONGRID_API_KEY = ENV["XCCONFIG_DEV_TRONGRID_API_KEY"]
XCCONFIG_DEV_UNSTOPPABLE_DOMAINS_API_KEY = ENV["XCCONFIG_DEV_UNSTOPPABLE_DOMAINS_API_KEY"]
XCCONFIG_DEV_ONE_INCH_API_KEY = ENV["XCCONFIG_DEV_ONE_INCH_API_KEY"]
XCCONFIG_DEV_CHAINALYSIS_API_KEY = ENV["XCCONFIG_DEV_CHAINALYSIS_API_KEY"]
XCCONFIG_DEV_ONE_INCH_COMMISSION = ENV["XCCONFIG_DEV_ONE_INCH_COMMISSION"]
XCCONFIG_DEV_ONE_INCH_COMMISSION_ADDRESS = ENV["XCCONFIG_DEV_ONE_INCH_COMMISSION_ADDRESS"]
XCCONFIG_DEV_REFERRAL_APP_SERVER_URL = ENV["XCCONFIG_DEV_REFERRAL_APP_SERVER_URL"]
Expand All @@ -47,6 +48,7 @@ XCCONFIG_PROD_OPEN_SEA_API_KEY = ENV["XCCONFIG_PROD_OPEN_SEA_API_KEY"]
XCCONFIG_PROD_TRONGRID_API_KEY = ENV["XCCONFIG_PROD_TRONGRID_API_KEY"]
XCCONFIG_PROD_UNSTOPPABLE_DOMAINS_API_KEY = ENV["XCCONFIG_PROD_UNSTOPPABLE_DOMAINS_API_KEY"]
XCCONFIG_PROD_ONE_INCH_API_KEY = ENV["XCCONFIG_PROD_ONE_INCH_API_KEY"]
XCCONFIG_PROD_CHAINALYSIS_API_KEY = ENV["XCCONFIG_PROD_CHAINALYSIS_API_KEY"]
XCCONFIG_PROD_ONE_INCH_COMMISSION = ENV["XCCONFIG_PROD_ONE_INCH_COMMISSION"]
XCCONFIG_PROD_ONE_INCH_COMMISSION_ADDRESS = ENV["XCCONFIG_PROD_ONE_INCH_COMMISSION_ADDRESS"]
XCCONFIG_PROD_REFERRAL_APP_SERVER_URL = ENV["XCCONFIG_PROD_REFERRAL_APP_SERVER_URL"]
Expand Down Expand Up @@ -130,6 +132,7 @@ def apply_dev_xcconfig
update_dev_xcconfig('trongrid_api_key', XCCONFIG_DEV_TRONGRID_API_KEY)
update_dev_xcconfig('unstoppable_domains_api_key', XCCONFIG_DEV_UNSTOPPABLE_DOMAINS_API_KEY)
update_dev_xcconfig('one_inch_api_key', XCCONFIG_DEV_ONE_INCH_API_KEY)
update_dev_xcconfig('chainalysis_api_key', XCCONFIG_DEV_CHAINALYSIS_API_KEY)
update_dev_xcconfig('one_inch_commission', XCCONFIG_DEV_ONE_INCH_COMMISSION)
update_dev_xcconfig('one_inch_commission_address', XCCONFIG_DEV_ONE_INCH_COMMISSION_ADDRESS)
update_dev_xcconfig('referral_app_server_url', XCCONFIG_DEV_REFERRAL_APP_SERVER_URL)
Expand All @@ -154,7 +157,8 @@ def apply_prod_xcconfig(swap_enabled, donate_enabled)
update_prod_xcconfig('open_sea_api_key', XCCONFIG_PROD_OPEN_SEA_API_KEY)
update_prod_xcconfig('trongrid_api_key', XCCONFIG_PROD_TRONGRID_API_KEY)
update_prod_xcconfig('unstoppable_domains_api_key', XCCONFIG_PROD_UNSTOPPABLE_DOMAINS_API_KEY)
update_prod_xcconfig('one_inch_api_key', XCCONFIG_PROD_ONE_INCH_API_KEY)
update_prod_xcconfig('one_inch_api_key', XCCONFIG_PROD_ONE_INCH_API_KEY)
update_prod_xcconfig('chainalysis_api_key', XCCONFIG_PROD_CHAINALYSIS_API_KEY)
update_prod_xcconfig('one_inch_commission', XCCONFIG_PROD_ONE_INCH_COMMISSION)
update_prod_xcconfig('one_inch_commission_address', XCCONFIG_PROD_ONE_INCH_COMMISSION_ADDRESS)
update_prod_xcconfig('referral_app_server_url', XCCONFIG_PROD_REFERRAL_APP_SERVER_URL)
Expand Down

0 comments on commit 2fa88c4

Please sign in to comment.