Skip to content

Commit

Permalink
ERC 20 Token support. Coin manager sync (#141)
Browse files Browse the repository at this point in the history
- Add syncer for update erc20 coins from ipfs
  • Loading branch information
ant013 committed Feb 5, 2019
1 parent d8f4d5e commit 3f98cbd
Show file tree
Hide file tree
Showing 16 changed files with 249 additions and 13 deletions.
12 changes: 12 additions & 0 deletions BankWallet/BankWallet.xcodeproj/project.pbxproj
Expand Up @@ -525,6 +525,7 @@
58AAA05AF7F58059E50F601A /* DataProviderSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAB2A7397DE3BBC456E96 /* DataProviderSettingsViewController.swift */; };
58AAA087BFA7A2609522E861 /* DataProviderSettingsRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA56AA98F7B0DB2E97971 /* DataProviderSettingsRouter.swift */; };
58AAA09CAEB6AF43CA020636 /* FullTransactionInfoPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA823D090E4B3E0D03A91 /* FullTransactionInfoPresenter.swift */; };
58AAA0A3411206AAE7312512 /* TokenSyncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA84935922407464F5624 /* TokenSyncer.swift */; };
58AAA0A54A64F5548C65F5F1 /* FullTransactionInfoInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA687FC0253AD175BA756 /* FullTransactionInfoInteractor.swift */; };
58AAA0B04B8E3BD80EC3085E /* FullTransactionInfoModule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA79D129F7B3E64598E5F /* FullTransactionInfoModule.swift */; };
58AAA0E8388C917EB1D4E022 /* FullTransactionInfoProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAC944A9DCACFAF1DDA15 /* FullTransactionInfoProtocols.swift */; };
Expand All @@ -534,8 +535,10 @@
58AAA160B00C018EBE97CAFD /* RequestErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAF286BB3532AF94E4EB8 /* RequestErrorView.swift */; };
58AAA1A770E5DD3B87EDD1A7 /* EtherscanProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA07E7147F10E59DC1CED /* EtherscanProvider.swift */; };
58AAA1AB2FD9FBA7801F830D /* PaymentRequestAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA7A94D25C20240FD75C6 /* PaymentRequestAddress.swift */; };
58AAA1CBB01C5CD2D53BDE14 /* TokenSyncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA84935922407464F5624 /* TokenSyncer.swift */; };
58AAA1EE015E354E285FCCD3 /* FullTransactionInfoProviderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA72A3BACDB574838A8CC /* FullTransactionInfoProviderFactory.swift */; };
58AAA1EEC400E3FE695E6C0B /* TransactionInfoDescriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAC9716B68916B4EA0B9B /* TransactionInfoDescriptionView.swift */; };
58AAA2171F75CCDDC287A550 /* TokenSyncerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAF290870CC9456A81C2C /* TokenSyncerTests.swift */; };
58AAA2363892ED5743CBF2EB /* EthereumBaseAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA656A81B2C12F618FB44 /* EthereumBaseAdapter.swift */; };
58AAA23E15CBC7EBB94086E2 /* FullTransactionDataProviderManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAAC5854D58969ECE57A1 /* FullTransactionDataProviderManagerTests.swift */; };
58AAA2761F98B944428578B8 /* DataProviderSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAB2A7397DE3BBC456E96 /* DataProviderSettingsViewController.swift */; };
Expand Down Expand Up @@ -613,6 +616,7 @@
58AAAB2024A5634646E9EFD5 /* RequestErrorTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA452B368DF573BDB2E41 /* RequestErrorTheme.swift */; };
58AAAB45621FAF77A7C091C3 /* FullTransactionInfoProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAAC944A9DCACFAF1DDA15 /* FullTransactionInfoProtocols.swift */; };
58AAABB4991882979199CA1F /* FullTransactionInfoProviderFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA72A3BACDB574838A8CC /* FullTransactionInfoProviderFactory.swift */; };
58AAABC3FB85309D31E2F893 /* TokenSyncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA84935922407464F5624 /* TokenSyncer.swift */; };
58AAABE8E8374ED4211F610C /* Erc20Adapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA312DD0792117182B64E /* Erc20Adapter.swift */; };
58AAAC1C5B2E8569B5E8386F /* FullTransactionLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA407F55E644523FC8EF8 /* FullTransactionLinkView.swift */; };
58AAAC1F6C9D7F092ECFF261 /* FullTransactionInfoRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58AAA842D6734B75D88E578C /* FullTransactionInfoRouter.swift */; };
Expand Down Expand Up @@ -1428,6 +1432,7 @@
58AAA823D090E4B3E0D03A91 /* FullTransactionInfoPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullTransactionInfoPresenter.swift; sourceTree = "<group>"; };
58AAA839E98781BFF8AEB2B3 /* BlockChairProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockChairProvider.swift; sourceTree = "<group>"; };
58AAA842D6734B75D88E578C /* FullTransactionInfoRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullTransactionInfoRouter.swift; sourceTree = "<group>"; };
58AAA84935922407464F5624 /* TokenSyncer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenSyncer.swift; sourceTree = "<group>"; };
58AAA90F7BDC0FD487A0C495 /* FullTransactionInfoState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullTransactionInfoState.swift; sourceTree = "<group>"; };
58AAA976A6B67FD47D5958E8 /* HorSysProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HorSysProvider.swift; sourceTree = "<group>"; };
58AAA98833B2412BBE860CA5 /* TransactionInfoDescriptionTheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionInfoDescriptionTheme.swift; sourceTree = "<group>"; };
Expand All @@ -1444,6 +1449,7 @@
58AAAEA9AB2118A46863AEB5 /* FullTransactionInfoPresenterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullTransactionInfoPresenterTests.swift; sourceTree = "<group>"; };
58AAAEB2610B22FF5C9E9A41 /* BlockExplorerProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlockExplorerProvider.swift; sourceTree = "<group>"; };
58AAAF286BB3532AF94E4EB8 /* RequestErrorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestErrorView.swift; sourceTree = "<group>"; };
58AAAF290870CC9456A81C2C /* TokenSyncerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenSyncerTests.swift; sourceTree = "<group>"; };
754FC632F3964D9EF1172D85 /* Pods_Bank.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Bank.framework; sourceTree = BUILT_PRODUCTS_DIR; };
7B87DD80F1B5F49E1A17D48A /* Pods-Bank.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Bank.release.xcconfig"; path = "../Pods/Target Support Files/Pods-Bank/Pods-Bank.release.xcconfig"; sourceTree = "<group>"; };
A79CC9D3E472AD6CDB9E17EC /* Pods-Bank.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Bank.debug.xcconfig"; path = "../Pods/Target Support Files/Pods-Bank/Pods-Bank.debug.xcconfig"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1704,6 +1710,7 @@
58AAA7F8DA30F2F95B9DC758 /* FullTransactionDataProviderManager.swift */,
58AAA3CCBFC02D7F6438DE3E /* PingManager.swift */,
58AAAC5B00009B199A687EF3 /* EthereumKitManager.swift */,
58AAA84935922407464F5624 /* TokenSyncer.swift */,
);
path = Managers;
sourceTree = "<group>";
Expand Down Expand Up @@ -1923,6 +1930,7 @@
1A564A19508B4730BC016037 /* LockoutManagerTests.swift */,
58AAAAC5854D58969ECE57A1 /* FullTransactionDataProviderManagerTests.swift */,
1A564A1732F105ABE241E908 /* CoinManagerTests.swift */,
58AAAF290870CC9456A81C2C /* TokenSyncerTests.swift */,
);
path = Managers;
sourceTree = "<group>";
Expand Down Expand Up @@ -3420,6 +3428,7 @@
58AAA6941C8B5140B0B5DDAE /* Erc20Adapter.swift in Sources */,
58AAA2363892ED5743CBF2EB /* EthereumBaseAdapter.swift in Sources */,
1A56451041F9A9AB607C3A73 /* Decimal.swift in Sources */,
58AAABC3FB85309D31E2F893 /* TokenSyncer.swift in Sources */,
11B35D3A74237CE0B77295B6 /* CoinIconImageView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -3467,6 +3476,7 @@
58AAA23E15CBC7EBB94086E2 /* FullTransactionDataProviderManagerTests.swift in Sources */,
58AAA70DA6DB5AE33CA2DBD9 /* FullTransactionInfoProviderFactoryTests.swift in Sources */,
1A56429A1D6B9D172E140E1C /* CoinManagerTests.swift in Sources */,
58AAA2171F75CCDDC287A550 /* TokenSyncerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -3785,6 +3795,7 @@
58AAABE8E8374ED4211F610C /* Erc20Adapter.swift in Sources */,
58AAA4A377F356194AE08055 /* EthereumBaseAdapter.swift in Sources */,
1A56415A4BB89B9156C6442D /* Decimal.swift in Sources */,
58AAA0A3411206AAE7312512 /* TokenSyncer.swift in Sources */,
11B3580BECA360DE35E89286 /* CoinIconImageView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -4104,6 +4115,7 @@
58AAA82745E47084A2B18F95 /* Erc20Adapter.swift in Sources */,
58AAAF011B2E9CDF8455CA7B /* EthereumBaseAdapter.swift in Sources */,
1A56491DC545ED4F8A6E6D40 /* Decimal.swift in Sources */,
58AAA1CBB01C5CD2D53BDE14 /* TokenSyncer.swift in Sources */,
11B3590CC5D7FB68ED00F7B7 /* CoinIconImageView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
2 changes: 2 additions & 0 deletions BankWallet/BankWallet/Core/App.swift
Expand Up @@ -21,6 +21,7 @@ class App {
let reachabilityManager: IReachabilityManager

let grdbStorage: GrdbStorage
let tokenSyncer: ITokenSyncer

let pinManager: IPinManager
let coinManager: ICoinManager
Expand Down Expand Up @@ -62,6 +63,7 @@ class App {
reachabilityManager = ReachabilityManager(appConfigProvider: appConfigProvider)

grdbStorage = GrdbStorage()
tokenSyncer = TokenSyncer(tokenNetworkManager: networkManager, storage: grdbStorage)

pinManager = PinManager(secureStorage: secureStorage)
coinManager = CoinManager(appConfigProvider: appConfigProvider, storage: grdbStorage)
Expand Down
3 changes: 1 addition & 2 deletions BankWallet/BankWallet/Core/Managers/AppConfigProvider.swift
Expand Up @@ -5,7 +5,7 @@ class AppConfigProvider: IAppConfigProvider {
let maxDecimal: Int = 8

let reachabilityHost = "ipfs.horizontalsystems.xyz"
let ratesApiUrl = "https://ipfs.horizontalsystems.xyz/ipns/Qmd4Gv2YVPqs6dmSy1XEq7pQRSgLihqYKL2JjK7DMUFPVz/io-hs/data/xrates"
let apiUrl = "https://ipfs.horizontalsystems.xyz/ipns/Qmd4Gv2YVPqs6dmSy1XEq7pQRSgLihqYKL2JjK7DMUFPVz/io-hs/data"

var testMode: Bool {
return Bundle.main.object(forInfoDictionaryKey: "TestMode") as? String == "true"
Expand Down Expand Up @@ -52,7 +52,6 @@ class AppConfigProvider: IAppConfigProvider {
Coin(title: "Bitcoin", code: "BTC\(suffix)", type: .bitcoin),
Coin(title: "Bitcoin Cash", code: "BCH\(suffix)", type: .bitcoinCash),
Coin(title: "Ethereum", code: "ETH\(suffix)", type: .ethereum),
Coin(title: "Pundi X Token", code: "NPXS\(suffix)", type: .erc20(address: "0xA15C7Ebe1f07CaF6bFF097D8a589fb8AC49Ae5B3", decimal: 18)),
]
}

Expand Down
29 changes: 25 additions & 4 deletions BankWallet/BankWallet/Core/Managers/NetworkManager.swift
Expand Up @@ -35,7 +35,7 @@ class NetworkManager {
private let ipfsMinuteFormatter = DateFormatter()

required init(appConfigProvider: IAppConfigProvider) {
self.apiUrl = appConfigProvider.ratesApiUrl
self.apiUrl = appConfigProvider.apiUrl

ipfsHourFormatter.timeZone = TimeZone(abbreviation: "UTC")
ipfsHourFormatter.dateFormat = "yyyy/MM/dd/HH"
Expand Down Expand Up @@ -148,7 +148,7 @@ extension NetworkManager: IRateNetworkManager {
coin.removeLast()
}

return observable(forRequest: request(withMethod: .get, path: "\(coin)/\(currencyCode)/index.json"))
return observable(forRequest: request(withMethod: .get, path: "xrates/\(coin)/\(currencyCode)/index.json"))
}

func getRate(coinCode: String, currencyCode: String, date: Date) -> Observable<Decimal> {
Expand All @@ -161,8 +161,8 @@ extension NetworkManager: IRateNetworkManager {
let hourPath = ipfsHourFormatter.string(from: date)
let minuteString = ipfsMinuteFormatter.string(from: date)

let hourObservable: Observable<[String: Double]> = observable(forRequest: request(withMethod: .get, path: "\(coin)/\(currencyCode)/\(hourPath)/index.json"))
let dayObservable: Observable<Double> = observable(forRequest: request(withMethod: .get, path: "\(coin)/\(currencyCode)/\(dayPath)/index.json"))
let hourObservable: Observable<[String: Double]> = observable(forRequest: request(withMethod: .get, path: "xrates/\(coin)/\(currencyCode)/\(hourPath)/index.json"))
let dayObservable: Observable<Double> = observable(forRequest: request(withMethod: .get, path: "xrates/\(coin)/\(currencyCode)/\(dayPath)/index.json"))

return hourObservable
.flatMap { rates -> Observable<Decimal> in
Expand All @@ -180,6 +180,27 @@ extension NetworkManager: IRateNetworkManager {
}

}
extension NetworkManager: ITokenNetworkManager {

func getTokens() -> Observable<[Coin]> {
let tokenObservable: Observable<[[String: Any]]> = observable(forRequest: request(withMethod: .get, path: "blockchain/ETH/erc20/index.json"))
return tokenObservable
.map { tokens -> [Coin] in
var coins = [Coin]()
for token in tokens {
if let code = token["code"] as? String,
let name = token["name"] as? String,
let contract = token["contract"] as? String,
let decimal = token["decimal"] as? Int {
coins.append(Coin(title: name, code: code, type: .erc20(address: contract, decimal: decimal)))
}
}
return coins
}
}

}


extension NetworkManager: IJSONApiManager {

Expand Down
40 changes: 40 additions & 0 deletions BankWallet/BankWallet/Core/Managers/TokenSyncer.swift
@@ -0,0 +1,40 @@
import RxSwift

class TokenSyncer: ITokenSyncer {
private let disposeBag = DisposeBag()

private let tokenNetworkManager: ITokenNetworkManager
private let storage: ICoinStorage
private let async: Bool

init(tokenNetworkManager: ITokenNetworkManager, storage: ICoinStorage, async: Bool = true) {
self.tokenNetworkManager = tokenNetworkManager
self.storage = storage
self.async = async
}

func sync() {
var observable = Observable.zip(tokenNetworkManager.getTokens(), storage.enabledCoinsObservable().take(1), storage.allCoinsObservable().take(1))

if async {
observable = observable.subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background)).observeOn(MainScheduler.instance)
}

observable.subscribe(onNext: { [weak self] (newCoins, enabledCoins, allCoins) in
self?.update(newCoins: newCoins, enabledCoins: enabledCoins, allCoins: allCoins)
})
.disposed(by: disposeBag)
}

private func update(newCoins: [Coin], enabledCoins: [Coin], allCoins: [Coin]) {
let inserted = newCoins.filter { !allCoins.contains($0) }
let deleted = allCoins.filter { !(enabledCoins.contains($0) || newCoins.contains($0)) }

guard !inserted.isEmpty || !deleted.isEmpty else {
return
}

storage.update(inserted: inserted, deleted: deleted)
}

}
11 changes: 10 additions & 1 deletion BankWallet/BankWallet/Core/Protocols.swift
Expand Up @@ -184,7 +184,7 @@ protocol IAppConfigProvider {
var fiatDecimal: Int { get }
var maxDecimal: Int { get }
var reachabilityHost: String { get }
var ratesApiUrl: String { get }
var apiUrl: String { get }
var testMode: Bool { get }
var infuraKey: String { get }
var etherscanKey: String { get }
Expand All @@ -211,6 +211,14 @@ protocol IRateNetworkManager {
func getRate(coinCode: String, currencyCode: String, date: Date) -> Observable<Decimal>
}

protocol ITokenNetworkManager {
func getTokens() -> Observable<[Coin]>
}

protocol ITokenSyncer {
func sync()
}

protocol IRateStorage {
func nonExpiredLatestRateValueObservable(forCoinCode coinCode: CoinCode, currencyCode: String) -> Observable<Decimal>
func latestRateObservable(forCoinCode coinCode: CoinCode, currencyCode: String) -> Observable<Rate>
Expand All @@ -225,6 +233,7 @@ protocol ICoinStorage {
func enabledCoinsObservable() -> Observable<[Coin]>
func allCoinsObservable() -> Observable<[Coin]>
func save(enabledCoins: [Coin])
func update(inserted: [Coin], deleted: [Coin])
func clearCoins()
}

Expand Down
11 changes: 11 additions & 0 deletions BankWallet/BankWallet/Core/Storage/GrdbStorage.swift
Expand Up @@ -159,6 +159,17 @@ extension GrdbStorage: ICoinStorage {
}
}

func update(inserted: [Coin], deleted: [Coin]) {
_ = try? dbPool.write { db in
for coin in inserted {
let storableCoin = StorableCoin(coin: coin, enabled: false, order: nil)
try storableCoin.insert(db)
}
let deletedCoinCodes = deleted.map { $0.code }
try StorableCoin.filter(deletedCoinCodes.contains(StorableCoin.Columns.code)).deleteAll(db)
}
}

func clearCoins() {
_ = try? dbPool.write { db in
try StorableCoin.deleteAll(db)
Expand Down
8 changes: 7 additions & 1 deletion BankWallet/BankWallet/Models/Coin.swift
Expand Up @@ -55,7 +55,7 @@ class StorableCoin: Record {
var enabled: Bool
var order: Int?

init(coin: Coin, enabled: Bool, order: Int) {
init(coin: Coin, enabled: Bool, order: Int?) {
self.coin = coin
self.enabled = enabled
self.order = order
Expand Down Expand Up @@ -100,6 +100,12 @@ extension Coin: Equatable {
}
}

extension Coin: Comparable {
public static func <(lhs: Coin, rhs: Coin) -> Bool {
return lhs.title < rhs.title
}
}

extension CoinType: Equatable {
public static func ==(lhs: CoinType, rhs: CoinType) -> Bool {
switch (lhs, rhs) {
Expand Down
Expand Up @@ -6,11 +6,13 @@ class ManageCoinsInteractor {
private let disposeBag = DisposeBag()

private let coinManager: ICoinManager
private let tokenSyncer: ITokenSyncer
private let storage: ICoinStorage
private let async: Bool

init(coinManager: ICoinManager, storage: ICoinStorage, async: Bool) {
init(coinManager: ICoinManager, tokenSyncer: ITokenSyncer, storage: ICoinStorage, async: Bool) {
self.coinManager = coinManager
self.tokenSyncer = tokenSyncer
self.storage = storage
self.async = async
}
Expand All @@ -19,6 +21,10 @@ class ManageCoinsInteractor {

extension ManageCoinsInteractor: IManageCoinsInteractor {

func syncCoins() {
tokenSyncer.sync()
}

func loadCoins() {
var allCoinsObservable = coinManager.allCoinsObservable
var enabledCoinsObservable = storage.enabledCoinsObservable()
Expand Down
Expand Up @@ -17,6 +17,7 @@ protocol IManageCoinsViewDelegate {
}

protocol IManageCoinsInteractor {
func syncCoins()
func loadCoins()
func save(enabledCoins: [Coin])
}
Expand Down
Expand Up @@ -38,6 +38,7 @@ extension ManageCoinsPresenter: IManageCoinsInteractorDelegate {
extension ManageCoinsPresenter: IManageCoinsViewDelegate {

func viewDidLoad() {
interactor.syncCoins()
interactor.loadCoins()
}

Expand Down
Expand Up @@ -16,7 +16,7 @@ extension ManageCoinsRouter {

static func module() -> UIViewController {
let router = ManageCoinsRouter()
let interactor = ManageCoinsInteractor(coinManager: App.shared.coinManager, storage: App.shared.grdbStorage, async: true)
let interactor = ManageCoinsInteractor(coinManager: App.shared.coinManager, tokenSyncer: App.shared.tokenSyncer, storage: App.shared.grdbStorage, async: true)
let presenter = ManageCoinsPresenter(interactor: interactor, router: router, state: ManageCoinsPresenterState())
let viewController = ManageCoinsViewController(delegate: presenter)

Expand Down

0 comments on commit 3f98cbd

Please sign in to comment.