434 lines
17 KiB
Swift
434 lines
17 KiB
Swift
//
|
|
// SwapServiceMain.swift
|
|
// Wallet
|
|
//
|
|
// Created by Saveliy Stavitsky on 3/18/21.
|
|
// Copyright © 2021 AM. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import SwiftGraph
|
|
import OrderedCollections
|
|
|
|
extension Array where Element == Swap.Model.Pool {
|
|
func process(startToken: Network.Model.Token?, amount: Decimal, forward: Bool, reverse: Bool) -> Decimal {
|
|
var amount = amount
|
|
var token = startToken?.symbol//forward ? tokenGive : tokenGet
|
|
for pool in forward ? self : self.reversed() {
|
|
if pool.token1Light?.symbol == token {
|
|
amount = reverse
|
|
? pool.processReceive(amount: amount, forward: true)
|
|
: pool.processSend(amount: amount, forward: true)
|
|
if amount <= 0 { return 0 }
|
|
token = pool.token2Light?.symbol
|
|
} else {
|
|
amount = reverse
|
|
? pool.processReceive(amount: amount, forward: false)
|
|
: pool.processSend(amount: amount, forward: false)
|
|
if amount <= 0 { return 0 }
|
|
token = pool.token1Light?.symbol
|
|
}
|
|
}
|
|
return amount
|
|
}
|
|
}
|
|
|
|
extension Swap.Service {
|
|
final class Pools {
|
|
|
|
var didUpdate: (() -> Void)?
|
|
|
|
var balances: [Network.Model.Token] {
|
|
Wallet.Service.Tokens.shared.balances.collection.filter({ $0.swappableContracts == true })
|
|
}
|
|
|
|
private(set) var collection = [Swap.Model.Pool]() {
|
|
didSet {
|
|
if let preselectTokenGive = preselectTokenGive {
|
|
if collection.reduce([], { $0 + [$1.token1, $1.token2] }).compactMap({ $0 }).contains(preselectTokenGive) {
|
|
tokenGive = preselectTokenGive
|
|
} else {
|
|
showPreselectError = true
|
|
didUpdate?()
|
|
}
|
|
}
|
|
graph = UnweightedUniqueElementsGraph<Network.Model.Token>()
|
|
for swap in collection.filter({ $0.token1 != nil && $0.token2 != nil }) {
|
|
graph.addVertex(swap.token1!)
|
|
graph.addVertex(swap.token2!)
|
|
graph.addEdge(from: swap.token1!, to: swap.token2!)
|
|
}
|
|
}
|
|
}
|
|
|
|
var preselectTokenGive: Network.Model.Token?
|
|
var showPreselectError: Bool = false
|
|
|
|
var tokenGive: Network.Model.Token? {
|
|
didSet {
|
|
amountSend = ""
|
|
amountReceive = ""
|
|
amountMinimum = ""
|
|
allRouts = calcAllRouts()
|
|
if let route = allRouts.first {
|
|
selectedBestRoute = route
|
|
}
|
|
}
|
|
}
|
|
var tokenGet: Network.Model.Token? {
|
|
didSet {
|
|
amountSend = ""
|
|
amountReceive = ""
|
|
amountMinimum = ""
|
|
allRouts = calcAllRouts()
|
|
if let route = allRouts.first {
|
|
selectedBestRoute = route
|
|
}
|
|
}
|
|
}
|
|
|
|
var tokensFrom: [Network.Model.Token] {
|
|
graph.vertices
|
|
// .filter({ balances.contains($0) })
|
|
.compactMap({ balances.first($0) })
|
|
}
|
|
|
|
var tokensTo: [Network.Model.Token] {
|
|
if let from = tokenGive {
|
|
var tokens = Set<Network.Model.Token>()
|
|
let sdsffs = graph.findAllBfs(from: from, goalTest: {
|
|
print($0)
|
|
tokens.insert($0)
|
|
return true
|
|
})
|
|
print(sdsffs)
|
|
tokens.remove(from)
|
|
return Array(tokens)
|
|
// .compactMap({ balances.first($0) })
|
|
.map({
|
|
if let token = balances.first($0) {
|
|
return token
|
|
} else {
|
|
var token = $0
|
|
token.amount = 0
|
|
return token
|
|
}
|
|
})
|
|
.sorted(by: { $0.amount == $1.amount ? $0.symbol < $1.symbol : $0.amount > $1.amount })
|
|
.filter({ $0.swappableContracts == true })
|
|
} else {
|
|
return []
|
|
}
|
|
}
|
|
|
|
private var selectedBestRoute: [Swap.Model.Pool] = []
|
|
func bestRoute(startToken: Network.Model.Token?, amount: Decimal, forward: Bool, reverse: Bool) -> [Swap.Model.Pool] {
|
|
print(allRouts.count)
|
|
|
|
var bestRoute = allRouts.first ?? []
|
|
var bestValue = allRouts.first?.process(startToken: startToken, amount: amount, forward: forward, reverse: reverse) ?? 0
|
|
for route in allRouts {
|
|
let value = route.process(startToken: startToken, amount: amount, forward: forward, reverse: reverse)
|
|
if reverse {
|
|
if bestValue == 0 || (value < bestValue && value > 0) {
|
|
bestValue = value
|
|
bestRoute = route
|
|
}
|
|
} else {
|
|
if bestValue < value {
|
|
bestValue = value
|
|
bestRoute = route
|
|
}
|
|
}
|
|
}
|
|
|
|
return bestRoute
|
|
}
|
|
|
|
private var allRouts: [[Swap.Model.Pool]] = []
|
|
private func calcAllRouts() -> [[Swap.Model.Pool]] {
|
|
if let from = tokenGive, let to = tokenGet {
|
|
let currenciesList = Array(Set<String>(collection
|
|
.reduce([String](), { $0 + [$1.token1Light!.id, $1.token2Light!.id] })))
|
|
// let poolsList = collection.reduce([(String, Int)](), {
|
|
// $0 + [("\(currenciesList.firstIndex(of: $1.token1!.symbol)!)-\(currenciesList.firstIndex(of: $1.token2!.symbol)!)", $1.id)]
|
|
// })
|
|
|
|
let fromIndex = currenciesList.firstIndex(of: "\(from.symbol)#\(from.contract)")!
|
|
let toIndex = currenciesList.firstIndex(of: "\(to.symbol)#\(to.contract)")!
|
|
|
|
let sppedCollection = collection.map({
|
|
SpeedPool(id: $0.id,
|
|
token1Code: currenciesList.firstIndex(of: $0.token1Light!.id)!,
|
|
token2Code: currenciesList.firstIndex(of: $0.token2Light!.id)!
|
|
)
|
|
})
|
|
let allCollections = sppedCollection + sppedCollection.map({ $0.reversed() })
|
|
|
|
var outFinal = [OrderedSet<Int>]()
|
|
|
|
var currentFinal = [OrderedSet<Int>]()
|
|
|
|
allCollections
|
|
.filter({ $0.token1 == fromIndex })
|
|
.forEach({
|
|
if $0.token2 == toIndex {
|
|
outFinal.append(OrderedSet([$0.token1, $0.token2]))
|
|
} else {
|
|
currentFinal.append(OrderedSet([$0.token1, $0.token2]))
|
|
}
|
|
})
|
|
|
|
// for (iteration, _) in allCollections.enumerated() {
|
|
for iteration in 0..<4 {
|
|
print("\(iteration) = \(currentFinal.count) / \(outFinal.count)")
|
|
var newCurrentFinal = [OrderedSet<Int>]()
|
|
|
|
for checkPool in allCollections {
|
|
for chain in currentFinal {
|
|
if //!chain.contains(checkPool.token1Code) &&
|
|
!chain.contains(checkPool.token2) {
|
|
if chain.last == checkPool.token1 {
|
|
if checkPool.token2 == toIndex {
|
|
outFinal.append(OrderedSet(chain.elements + [checkPool.token1, checkPool.token2]))
|
|
} else {
|
|
newCurrentFinal.append(OrderedSet(chain.elements + [checkPool.token1, checkPool.token2]))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
currentFinal = newCurrentFinal
|
|
}
|
|
|
|
var allCollectionsDict = [Int: SpeedPool]()
|
|
for pool in allCollections {
|
|
allCollectionsDict[pool.token1 * 10000 + pool.token2] = pool
|
|
}
|
|
var allUsusalPools = [Int: Swap.Model.Pool]()
|
|
for pool in collection {
|
|
allUsusalPools[pool.id] = pool
|
|
}
|
|
|
|
return outFinal.map({ chain in
|
|
var newChain = [SpeedPool]()
|
|
for index in Array(0..<(chain.count - 1)) {
|
|
let token1 = chain.elements[index]
|
|
let token2 = chain.elements[index + 1]
|
|
newChain.append(allCollectionsDict[token1 * 10000 + token2]!)
|
|
}
|
|
return newChain.map({ speedPool in allUsusalPools[speedPool.id]! })
|
|
})
|
|
} else {
|
|
return []
|
|
}
|
|
}
|
|
|
|
func processRate(amount: Decimal) -> Decimal {
|
|
var newAmountReceive: Decimal
|
|
guard let tokenGive = tokenGive, let tokenGet = tokenGet else { return 0 }
|
|
|
|
newAmountReceive = selectedBestRoute.process(startToken: tokenGive,
|
|
amount: amount,
|
|
forward: true, reverse: false)
|
|
|
|
if newAmountReceive < 0 || newAmountReceive == Decimal.nan {
|
|
return 0
|
|
} else {
|
|
return newAmountReceive
|
|
}
|
|
}
|
|
|
|
var route: [Network.Model.Token] {
|
|
var tokens = [tokenGive]
|
|
for pool in selectedBestRoute {
|
|
if pool.token1 == tokens.last {
|
|
tokens.append(pool.token2)
|
|
} else {
|
|
tokens.append(pool.token1)
|
|
}
|
|
}
|
|
return tokens.compactMap({ $0 })
|
|
}
|
|
|
|
let minOtherAmount = "800".toDecimal()
|
|
|
|
private(set) var amountSend: String = ""
|
|
var amountSendError: String? {
|
|
if let tokenGive = self.tokenGive, let amountSend = amountSend.toOptionalDecimal() {
|
|
if (balances.first(tokenGive)?.amount ?? 0) < amountSend {
|
|
return L10n.Swap.Main.Give.error
|
|
} else if amountSend < Decimal(1.0) / pow(10, tokenGive.precision) * minOtherAmount {
|
|
return L10n.Swap.Main.Give.tooSmallOther
|
|
} else {
|
|
return nil
|
|
}
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private(set) var amountReceive: String = ""
|
|
var amountReceiveError: String? {
|
|
if let tokenGet = self.tokenGet, let amountReceive = amountReceive.toOptionalDecimal() {
|
|
if (route.last?.amount ?? 0) < amountReceive {
|
|
return L10n.Swap.Main.Get.error
|
|
} else {
|
|
return nil
|
|
}
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private(set) var amountMinimum: String = ""
|
|
// var amountMinimumShowError: Bool {
|
|
// if let amount = amountReceive {
|
|
// return amount * 0.95 < amountMinimum ?? 0
|
|
// } else {
|
|
// return false
|
|
// }
|
|
// }
|
|
|
|
func update(receive: String) {
|
|
guard let tokenGive = self.tokenGive, let tokenGet = self.tokenGet else { return }
|
|
|
|
if let amount = receive.toOptionalDecimal() {
|
|
amountReceive = receive.normalized(precision: tokenGet.precision)
|
|
amountMinimum = (amount * 0.99).toString(max: tokenGet.precision, group: false)
|
|
|
|
selectedBestRoute = bestRoute(startToken: tokenGet,
|
|
amount: amount, forward: false, reverse: true)
|
|
var newAmountSend = selectedBestRoute.process(startToken: tokenGet,
|
|
amount: amount, forward: false, reverse: true)
|
|
|
|
newAmountSend = newAmountSend > 0 ? newAmountSend : 0
|
|
amountSend = newAmountSend.toString(max: tokenGive.precision, group: false)
|
|
} else if receive.isEmpty {
|
|
amountSend = ""
|
|
amountReceive = ""
|
|
amountMinimum = ""
|
|
}
|
|
|
|
didUpdate?()
|
|
}
|
|
|
|
func update(send: String) {
|
|
guard let tokenGive = self.tokenGive, let tokenGet = self.tokenGet else { return }
|
|
|
|
if let amount = send.toOptionalDecimal() {
|
|
amountSend = send.normalized(precision: tokenGive.precision)
|
|
|
|
selectedBestRoute = bestRoute(startToken: tokenGive,
|
|
amount: amount, forward: true, reverse: false)
|
|
var newAmountReceive = selectedBestRoute.process(startToken: tokenGive,
|
|
amount: amount, forward: true, reverse: false)
|
|
|
|
newAmountReceive = newAmountReceive > 0 ? newAmountReceive : 0
|
|
amountReceive = newAmountReceive.toString(max: tokenGet.precision, group: false)
|
|
amountMinimum = (newAmountReceive * 0.99).toString(max: tokenGet.precision, group: false)
|
|
} else if send.isEmpty {
|
|
amountSend = ""
|
|
amountReceive = ""
|
|
amountMinimum = ""
|
|
}
|
|
|
|
didUpdate?()
|
|
}
|
|
|
|
func update(minimum: String) {
|
|
guard let tokenGet = self.tokenGet else { return }
|
|
if minimum.toOptionalDecimal() != nil {
|
|
amountMinimum = minimum.normalized(precision: tokenGet.precision)
|
|
} else if minimum.isEmpty {
|
|
amountMinimum = ""
|
|
}
|
|
didUpdate?()
|
|
}
|
|
|
|
private(set) var graph = UnweightedUniqueElementsGraph<Network.Model.Token>()
|
|
|
|
func fetch(_ completion: @escaping (NSError?) -> Void) {
|
|
Network.table.fetch(
|
|
code: ApplicationEnvironment.shared().current.contract(.swap),
|
|
table: "pools",
|
|
scope: ApplicationEnvironment.shared().current.contract(.swap),
|
|
limit: 1000,
|
|
type: Swap.Model.Pool.self
|
|
) { (result) in
|
|
self.collection = result
|
|
.filter({ ($0.token1?.amount ?? 0) > 0 || ($0.token2?.amount ?? 0) > 0 })
|
|
completion(nil)
|
|
|
|
self.didUpdate?()
|
|
}
|
|
}
|
|
|
|
func submit(privateKey: String, _ completion: @escaping (Result<String, Error>) -> Void) {
|
|
guard let tokenGive = self.tokenGive,
|
|
let tokenGet = self.tokenGet,
|
|
let amountSend = self.amountSend.toOptionalDecimal()
|
|
// let contract = ApplicationEnvironment.shared().current.contract(tokenGive.contract)
|
|
else { return }
|
|
var actions = [(contract: String, action: Network.Model.Blockchain.Action, data: Codable)]()
|
|
if !balances.contains(tokenGet) {
|
|
if tokenGet.contract == "pizzatotoken" {
|
|
actions.append(
|
|
(contract: tokenGet.contract,
|
|
action: .signup,
|
|
data: [
|
|
"owner": Accounts().current?.name ?? ""
|
|
])
|
|
)
|
|
} else {
|
|
actions.append(
|
|
(contract: tokenGet.contract,
|
|
action: .open,
|
|
data: [
|
|
"owner": Accounts().current?.name ?? "",
|
|
"symbol": "\(tokenGet.precision),\(tokenGet.symbol)",
|
|
"ram_payer": Accounts().current?.name ?? ""
|
|
])
|
|
)
|
|
}
|
|
}
|
|
let route = selectedBestRoute.map({ "\($0.id)" }).joined(separator: "-")
|
|
let min = amountMinimum
|
|
.toOptionalDecimal()?
|
|
.toString(precision: tokenGet)
|
|
.split(separator: " ").first?
|
|
.replacingOccurrences(of: ".", with: "")
|
|
.replacingOccurrences(of: ",", with: "") ?? ""
|
|
actions.append(
|
|
(contract: tokenGive.contract,
|
|
action: .transfer,
|
|
data: [
|
|
"from": Accounts().current?.name ?? "",
|
|
"to": ApplicationEnvironment.shared().current.contract(.swap),
|
|
"quantity": "\(amountSend.toString(precision: tokenGive))",
|
|
"memo": "swap:\(route)\(min.isEmpty ? "" : ";min:\(min)")"
|
|
]))
|
|
Network.Service.Blockchain.execute(actions: actions, privateKeys: [privateKey]) { (result) in
|
|
switch result {
|
|
case .success(let trxId):
|
|
delayed(4) {
|
|
Wallet.Service.Tokens.shared.balances.fetch { (_) in
|
|
completion(.success(trxId))
|
|
}
|
|
}
|
|
case .failure(let err):
|
|
completion(.failure(err))
|
|
}
|
|
}
|
|
}
|
|
|
|
func getToken(_ token: Network.Model.Token) -> Network.Model.Token? {
|
|
if let value = balances.filter({ $0.symbol == token.symbol }).first {
|
|
return value
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
}
|