Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Chainalysis sanctioned address check #6071

Merged
merged 1 commit into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading