Skip to content

Commit

Permalink
Merge pull request #863 from novasamatech/feature/swaps-confirm-base
Browse files Browse the repository at this point in the history
Swaps base interactor
  • Loading branch information
ERussel authored Oct 26, 2023
2 parents c8f88c9 + 7ef97ff commit d851e34
Show file tree
Hide file tree
Showing 25 changed files with 892 additions and 249 deletions.
80 changes: 78 additions & 2 deletions novawallet.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions novawallet/Assets.xcassets/iconForward.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "arrow-forward.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.
246 changes: 246 additions & 0 deletions novawallet/Modules/Swaps/Base/SwapBaseInteractor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import UIKit
import RobinHood
import BigInt

class SwapBaseInteractor: AnyCancellableCleaning, AnyProviderAutoCleaning, SwapBaseInteractorInputProtocol {
weak var basePresenter: SwapSetupInteractorOutputProtocol?
let assetConversionOperationFactory: AssetConversionOperationFactoryProtocol
let assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol
let runtimeService: RuntimeProviderProtocol
let feeProxy: ExtrinsicFeeProxyProtocol
let extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol
let priceLocalSubscriptionFactory: PriceProviderFactoryProtocol
let walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol
let currencyManager: CurrencyManagerProtocol
let selectedAccount: MetaAccountModel

private let operationQueue: OperationQueue
private var quoteCall: CancellableCall?
private var runtimeOperationCall: CancellableCall?
private var extrinsicService: ExtrinsicServiceProtocol?

private var priceProviders: [ChainAssetId: StreamableProvider<PriceData>] = [:]
private var assetBalanceProviders: [ChainAssetId: StreamableProvider<AssetBalance>] = [:]

init(
assetConversionOperationFactory: AssetConversionOperationFactoryProtocol,
assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol,
runtimeService: RuntimeProviderProtocol,
feeProxy: ExtrinsicFeeProxyProtocol,
extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol,
priceLocalSubscriptionFactory: PriceProviderFactoryProtocol,
walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol,
currencyManager: CurrencyManagerProtocol,
selectedAccount: MetaAccountModel,
operationQueue: OperationQueue
) {
self.assetConversionOperationFactory = assetConversionOperationFactory
self.assetConversionExtrinsicService = assetConversionExtrinsicService
self.runtimeService = runtimeService
self.feeProxy = feeProxy
self.extrinsicServiceFactory = extrinsicServiceFactory
self.priceLocalSubscriptionFactory = priceLocalSubscriptionFactory
self.walletLocalSubscriptionFactory = walletLocalSubscriptionFactory
self.currencyManager = currencyManager
self.selectedAccount = selectedAccount
self.operationQueue = operationQueue
}

func updateSubscriptions(activeChainAssets: Set<ChainAssetId>) {
priceProviders = clear(providers: priceProviders, activeChainAssets: activeChainAssets)
assetBalanceProviders = clear(providers: assetBalanceProviders, activeChainAssets: activeChainAssets)
}

func clear<T>(
providers: [ChainAssetId: StreamableProvider<T>],
activeChainAssets: Set<ChainAssetId>
) -> [ChainAssetId: StreamableProvider<T>] {
providers.reduce(into: [ChainAssetId: StreamableProvider<T>]()) {
if !activeChainAssets.contains($1.key) {
$1.value.removeObserver(self)
} else {
$0[$1.key] = $1.value
}
}
}

func priceSubscription(chainAsset: ChainAsset) -> StreamableProvider<PriceData>? {
guard let priceId = chainAsset.asset.priceId else {
return nil
}

return priceProviders[chainAsset.chainAssetId] ?? subscribeToPrice(
for: priceId,
currency: currencyManager.selectedCurrency
)
}

func assetBalanceSubscription(chainAsset: ChainAsset) -> StreamableProvider<AssetBalance>? {
guard let accountId = chainAccountResponse(for: chainAsset)?.accountId else {
return nil
}
let chainAssetId = chainAsset.chainAssetId
return assetBalanceProviders[chainAssetId] ?? subscribeToAssetBalanceProvider(
for: accountId,
chainId: chainAssetId.chainId,
assetId: chainAssetId.assetId
)
}

func quote(args: AssetConversion.QuoteArgs) {
clear(cancellable: &quoteCall)

let wrapper = assetConversionOperationFactory.quote(for: args)
wrapper.targetOperation.completionBlock = { [weak self, args] in
DispatchQueue.main.async {
guard self?.quoteCall === wrapper else {
return
}
do {
let result = try wrapper.targetOperation.extractNoCancellableResultData()

self?.basePresenter?.didReceive(quote: result, for: args)
} catch {
self?.basePresenter?.didReceive(error: .quote(error, args))
}
}
}

quoteCall = wrapper
operationQueue.addOperations(wrapper.allOperations, waitUntilFinished: false)
}

func fee(args: AssetConversion.CallArgs) {
clear(cancellable: &runtimeOperationCall)
guard let extrinsicService = extrinsicService else {
return
}

let runtimeCoderFactoryOperation = runtimeService.fetchCoderFactoryOperation()

runtimeCoderFactoryOperation.completionBlock = { [weak self] in
guard let self = self else {
return
}
do {
let runtimeCoderFactory = try runtimeCoderFactoryOperation.extractNoCancellableResultData()
let builder = self.assetConversionExtrinsicService.fetchExtrinsicBuilderClosure(
for: args,
codingFactory: runtimeCoderFactory
)
self.feeProxy.estimateFee(
using: extrinsicService,
reuseIdentifier: args.identifier,
setupBy: builder
)
} catch {
DispatchQueue.main.async {
self.basePresenter?.didReceive(error: .fetchFeeFailed(error, args.identifier))
}
}
}

runtimeOperationCall = runtimeCoderFactoryOperation
operationQueue.addOperation(runtimeCoderFactoryOperation)
}

func chainAccountResponse(for chainAsset: ChainAsset) -> ChainAccountResponse? {
let metaChainAccountResponse = selectedAccount.fetchMetaChainAccount(for: chainAsset.chain.accountRequest())
return metaChainAccountResponse?.chainAccount
}

func set(receiveChainAsset chainAsset: ChainAsset) {
priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset)
}

func set(payChainAsset chainAsset: ChainAsset) {
guard let chainAccount = chainAccountResponse(for: chainAsset) else {
extrinsicService = nil
basePresenter?.didReceive(payAccountId: nil)
return
}
priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset)
assetBalanceProviders[chainAsset.chainAssetId] = assetBalanceSubscription(chainAsset: chainAsset)

extrinsicService = extrinsicServiceFactory.createService(
account: chainAccount,
chain: chainAsset.chain
)
basePresenter?.didReceive(payAccountId: chainAccount.accountId)
}

func set(feeChainAsset chainAsset: ChainAsset) {
priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset)
assetBalanceProviders[chainAsset.chainAssetId] = assetBalanceSubscription(chainAsset: chainAsset)
}

// MARK: - SwapBaseInteractorInputProtocol

func setup() {
feeProxy.delegate = self
}

func calculateQuote(for args: AssetConversion.QuoteArgs) {
quote(args: args)
}

func calculateFee(
args: AssetConversion.CallArgs
) {
fee(args: args)
}

func remakePriceSubscription(for chainAsset: ChainAsset) {
priceProviders[chainAsset.chainAssetId] = priceSubscription(chainAsset: chainAsset)
}
}

extension SwapBaseInteractor: ExtrinsicFeeProxyDelegate {
func didReceiveFee(result: Result<RuntimeDispatchInfo, Error>, for transactionId: TransactionFeeId) {
DispatchQueue.main.async {
switch result {
case let .success(dispatchInfo):
let fee = BigUInt(dispatchInfo.fee)
self.basePresenter?.didReceive(fee: fee, transactionId: transactionId)
case let .failure(error):
self.basePresenter?.didReceive(error: .fetchFeeFailed(error, transactionId))
}
}
}
}

extension SwapBaseInteractor: PriceLocalStorageSubscriber, PriceLocalSubscriptionHandler {
func handlePrice(result: Result<PriceData?, Error>, priceId: AssetModel.PriceId) {
switch result {
case let .success(priceData):
basePresenter?.didReceive(price: priceData, priceId: priceId)
case let .failure(error):
basePresenter?.didReceive(error: .price(error, priceId))
}
}
}

extension SwapBaseInteractor: WalletLocalStorageSubscriber, WalletLocalSubscriptionHandler {
func handleAssetBalance(
result: Result<AssetBalance?, Error>,
accountId: AccountId,
chainId: ChainModel.Id,
assetId: AssetModel.Id
) {
let chainAssetId = ChainAssetId(chainId: chainId, assetId: assetId)
switch result {
case let .success(balance):
let balance = balance ?? .createZero(
for: .init(chainId: chainId, assetId: assetId),
accountId: accountId
)
basePresenter?.didReceive(
balance: balance,
for: chainAssetId,
accountId: accountId
)
case let .failure(error):
basePresenter?.didReceive(error: .assetBalance(error, chainAssetId, accountId))
}
}
}
16 changes: 16 additions & 0 deletions novawallet/Modules/Swaps/Base/SwapBaseProtocols.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import BigInt

protocol SwapBaseInteractorInputProtocol: AnyObject {
func setup()
func calculateQuote(for args: AssetConversion.QuoteArgs)
func calculateFee(args: AssetConversion.CallArgs)
func remakePriceSubscription(for chainAsset: ChainAsset)
}

protocol SwapBaseInteractorOutputProtocol: AnyObject {
func didReceive(quote: AssetConversion.Quote, for quoteArgs: AssetConversion.QuoteArgs)
func didReceive(fee: BigUInt?, transactionId: TransactionFeeId)
func didReceive(error: SwapSetupError)
func didReceive(price: PriceData?, priceId: AssetModel.PriceId)
func didReceive(payAccountId: AccountId?)
}
11 changes: 11 additions & 0 deletions novawallet/Modules/Swaps/Base/View/SwapNetworkFeeViewCell.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import SoraUI

final class SwapNetworkFeeViewCell: RowView<SwapNetworkFeeView>, StackTableViewCellProtocol {
var titleButton: RoundedButton { rowContentView.titleView }
var valueTopButton: RoundedButton { rowContentView.valueView.fView }
var valueBottomLabel: UILabel { rowContentView.valueView.sView }

func bind(loadableViewModel: LoadableViewModelState<SwapFeeViewModel>) {
rowContentView.bind(loadableViewModel: loadableViewModel)
}
}
10 changes: 10 additions & 0 deletions novawallet/Modules/Swaps/Base/View/SwapRateViewCell.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import SoraUI

final class SwapRateViewCell: RowView<SwapRateView>, StackTableViewCellProtocol {
var titleButton: RoundedButton { rowContentView.titleView }
var valueLabel: UILabel { rowContentView.valueView }

func bind(loadableViewModel: LoadableViewModelState<String>) {
rowContentView.bind(loadableViewModel: loadableViewModel)
}
}
54 changes: 54 additions & 0 deletions novawallet/Modules/Swaps/Confirm/SwapConfirmInteractor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import UIKit

final class SwapConfirmInteractor: SwapBaseInteractor {
weak var presenter: SwapConfirmInteractorOutputProtocol?
let payChainAsset: ChainAsset
let receiveChainAsset: ChainAsset
let feeChainAsset: ChainAsset
let slippage: BigRational

init(
payChainAsset: ChainAsset,
receiveChainAsset: ChainAsset,
feeChainAsset: ChainAsset,
slippage: BigRational,
assetConversionOperationFactory: AssetConversionOperationFactoryProtocol,
assetConversionExtrinsicService: AssetConversionExtrinsicServiceProtocol,
runtimeService: RuntimeProviderProtocol,
feeProxy: ExtrinsicFeeProxyProtocol,
extrinsicServiceFactory: ExtrinsicServiceFactoryProtocol,
priceLocalSubscriptionFactory: PriceProviderFactoryProtocol,
walletLocalSubscriptionFactory: WalletLocalSubscriptionFactoryProtocol,
currencyManager: CurrencyManagerProtocol,
selectedAccount: MetaAccountModel,
operationQueue: OperationQueue
) {
self.payChainAsset = payChainAsset
self.receiveChainAsset = receiveChainAsset
self.feeChainAsset = feeChainAsset
self.slippage = slippage

super.init(
assetConversionOperationFactory: assetConversionOperationFactory,
assetConversionExtrinsicService: assetConversionExtrinsicService,
runtimeService: runtimeService,
feeProxy: feeProxy,
extrinsicServiceFactory: extrinsicServiceFactory,
priceLocalSubscriptionFactory: priceLocalSubscriptionFactory,
walletLocalSubscriptionFactory: walletLocalSubscriptionFactory,
currencyManager: currencyManager,
selectedAccount: selectedAccount,
operationQueue: operationQueue
)
}

override func setup() {
super.setup()

set(payChainAsset: payChainAsset)
set(receiveChainAsset: receiveChainAsset)
set(feeChainAsset: feeChainAsset)
}
}

extension SwapConfirmInteractor: SwapConfirmInteractorInputProtocol {}
34 changes: 34 additions & 0 deletions novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Foundation
import BigInt

final class SwapConfirmPresenter {
weak var view: SwapConfirmViewProtocol?
let wireframe: SwapConfirmWireframeProtocol
let interactor: SwapConfirmInteractorInputProtocol

init(
interactor: SwapConfirmInteractorInputProtocol,
wireframe: SwapConfirmWireframeProtocol
) {
self.interactor = interactor
self.wireframe = wireframe
}
}

extension SwapConfirmPresenter: SwapConfirmPresenterProtocol {
func setup() {
interactor.setup()
}
}

extension SwapConfirmPresenter: SwapConfirmInteractorOutputProtocol {
func didReceive(quote _: AssetConversion.Quote, for _: AssetConversion.QuoteArgs) {}

func didReceive(fee _: BigUInt?, transactionId _: TransactionFeeId) {}

func didReceive(error _: SwapSetupError) {}

func didReceive(price _: PriceData?, priceId _: AssetModel.PriceId) {}

func didReceive(payAccountId _: AccountId?) {}
}
Loading

0 comments on commit d851e34

Please sign in to comment.