Files
raspberry/iOS/Wallet/Sources/P2P/Service/P2PServiceOrder.swift
2022-05-27 20:21:01 +03:00

646 lines
26 KiB
Swift

//
// PaycashServiceOrder.swift
// PayCash
//
// Created by Igor Danich on 02.09.2020.
// Copyright © 2020 List. All rights reserved.
//
import Foundation
import Alamofire
import EosioSwiftEcc
import EosioSwiftAbieosSerializationProvider
import RealmSwift
import struct Resolver.Injected
extension P2P.Service {
class Order {
@Injected private var p2pBalancesService: P2PBalancesService
@Injected var p2pListsService: P2PListsService
var kind: P2P.Model.Kind
var order: P2POrder?
var contentType: P2P.Model.ContentType {
didSet {
paymentSystem = nil
tokenGet = nil
blockchain = nil
vcardNumber = ""
vcardName = ""
vcardExpiration = ""
vcardCVV = ""
country = nil
city = nil
bank = []
link = ""
account = ""
didReload?()
update()
}
}
var token: Network.Model.Token? {
didSet {
update()
}
}
var card = Cards.shared.default {
didSet {
update()
}
}
var paymentSystem: (id: Int, title: String, description: String)? {
didSet {
tokenGet = nil
update()
}
}
var tokenGet: (id: Int, title: String, description: String)? {
didSet {
rateToken1 = "1"
rateToken2 = "1"
amount = ""
limitMin = ""
limitMax = ""
blockchain = nil
update()
}
}
var blockchain: (id: Int, title: String, description: String)? {
didSet {
didReload?()
update()
}
}
var rateToken1: String = "1" {
didSet {
update()
}
}
var rateToken2: String = "1" {
didSet {
update()
}
}
var rate: Decimal {
(rateToken2.toOptionalDecimal() ?? 1.0) / ((rateToken1.toOptionalDecimal() ?? 1.0) > 0 ? (rateToken1.toOptionalDecimal() ?? 1.0) : 1.0)
}
var rateSend: String {
rateToken1 == "1" ? rateToken2 : rateToken1
}
var token1Precision: Int { 2 }
var amount: String = "" {
didSet {
limitMin = amount
limitMax = amount
didReload?()
update()
}
}
var token2Precision: Int {
p2pListsService.currencies.where({ $0.id == tokenGet?.id ?? -1 }).first?.precision ?? 0
}
var amountToken2: String {
if let amount = amount.toOptionalDecimal() {
return (amount * rate).toNormalizedString(precisionMax: token2Precision)
} else {
return ""
}
}
var limitMin = "" { didSet { update() } }
var limitMinToken2: String {
if let amountLimitMin = limitMin.toOptionalDecimal() {
return (amountLimitMin * rate).toNormalizedString(precisionMax: token2Precision)
} else {
return ""
}
}
var limitMax = "" { didSet { update() } }
var limitMaxToken2: String {
if let amountLimitMax = limitMax.toOptionalDecimal() {
return (amountLimitMax * rate).toNormalizedString(precisionMax: token2Precision)
} else {
return ""
}
}
var link = "" { didSet { update() } }
var vcardNumber = "" { didSet { update() } }
var vcardName = "" { didSet { update() } }
var vcardExpiration = "" { didSet { update() } }
var vcardCVV = "" { didSet { update() } }
var country: (id: Int, title: String, description: String)? {
didSet {
city = nil
bank = []
update()
}
}
var city: (id: Int, title: String, description: String)? {
didSet {
update()
}
}
var bank: [(id: Int, title: String, description: String)] = [] {
didSet {
update()
}
}
var account = "" { didSet { update() } }
var memo = "" { didSet { update() } }
var didUpdate: (() -> Void)?
var didReload: (() -> Void)?
// var rate: String {
// token != nil
// ? L10n.P2p.Order.Rate.text(token?.symbol ?? "", "\(token?.symbol.prefix(3) ?? "")")
// : " "
// }
var maxValue: String {
if let order = order {
return order.volume.decimalValue
.roundDown(to: token1Precision).toString(max: token1Precision, group: false)
}
let D = (token?.amount ?? 0).receive(precision: 5)
if D < 0.01 {
return "0"
}
if kind == .sell {
let V = (D/1.1).roundUp(to: 5)
let S = V.commission(percent: 0.1, precision: 5, ignoreMaxCommission: true)
return (D - S).roundDown(to: token1Precision).toString(max: token1Precision, group: false)
} else {
return (10*D).roundDown(to: token1Precision).toString(max: token1Precision, group: false)
}
}
var isFast = false
let uuid = UUID().uuidString
private var _limitMin: String { limitMin.isEmpty ? (limitMax.isEmpty ? amount : limitMax) : limitMin }
private var _limitMax: String { limitMax.isEmpty ? (limitMin.isEmpty ? amount : limitMin) : limitMax }
private var meta: String {
var info = [String:Any]()
info["uuid"] = uuid
info["type"] = contentType.rawValue
info["params"] = [String: Any]()
if let data = try? JSONSerialization.data(withJSONObject: info, options: .fragmentsAllowed) {
return String(data: data, encoding: .utf8) ?? "{}"
}
return "{}"
}
private var data: String {
return "{}"
}
var amountError: String? {
if token == nil { return nil }
if amount.toDecimal() > maxValue.toDecimal() { return L10n.P2p.Error.balance }
if let order = order {
if amount.toDecimal() > order.volume.decimalValue {
return L10n.P2p.Error.balance
}
}
return nil
}
var minError: String? {
if token == nil { return nil }
if amount.toDecimal() < _limitMin.toDecimal() { return L10n.P2p.Error.Limit.less }
return nil
}
var maxError: String? {
if token == nil { return nil }
if _limitMax.toDecimal() > amount.toDecimal() { return L10n.P2p.Error.Limit.more }
if _limitMin.toDecimal() > _limitMax.toDecimal() { return L10n.P2p.Error.Limit.less }
return nil
}
private(set) var isValidLimit = false
private(set) var isValid = false
init(kind: P2P.Model.Kind, contentType: P2P.Model.ContentType = .card,
token: Network.Model.Token? = nil, order: P2POrder? = nil) {
self.kind = kind
self.order = order
self.contentType = contentType
if let token = token {
self.token = P2P.Service.tokens.first(token)
}
}
var insurance: Decimal { amount.toDecimal().commission(percent: 0.1, precision: 5, ignoreMaxCommission: true) }
var commission: Decimal {
((kind == .buy ? Decimal(0) : amount.toDecimal()) + insurance)
.send(precision: 5).commission(precision: 5)
}
private var isVcardExpirationValid: Bool {
let month = vcardExpiration.components(separatedBy: "/").first ?? ""
let monthInt = (Int(vcardExpiration.components(separatedBy: "/").first ?? "") ?? 0)
let year = vcardExpiration.components(separatedBy: "/").last ?? ""
let yearInt = (Int(vcardExpiration.components(separatedBy: "/").last ?? "") ?? 0)
return vcardExpiration.count == 5 && month.count == 2 && year.count == 2
&& monthInt > 0 && monthInt <= 12
&& yearInt > 20
}
private func update() {
isValidLimit =
amount.toDecimal() > 0
&& rateToken1.toDecimal() > 0
&& rateToken2.toDecimal() > 0
&& limitMin.toDecimal() > 0
&& limitMax.toDecimal() > 0
&& amount.toDecimal() <= maxValue.toDecimal()
&& _limitMax.toDecimal() >= _limitMin.toDecimal()
&& _limitMax.toDecimal() <= amount.toDecimal()
&& amountToken2.toDecimal() > 0
switch contentType {
case .card:
isValid = isValidLimit
&& rateToken1.toOptionalDecimal() != nil
&& rateToken2.toOptionalDecimal() != nil
&& tokenGet != nil
&& (kind == .sell || (country != nil && !bank.isEmpty))
&& (kind == .buy || (!(card?.number ?? "").isEmpty && card?.countryId != nil && card?.bankId != nil))
case .url:
isValid = isValidLimit
&& (kind == .sell || rateToken1.toOptionalDecimal() != nil)
&& (kind == .sell || rateToken2.toOptionalDecimal() != nil)
&& tokenGet != nil && country != nil
&& (kind == .buy || !link.isEmpty)
case .cash:
isValid = isValidLimit
&& rateToken1.toOptionalDecimal() != nil
&& rateToken2.toOptionalDecimal() != nil
&& tokenGet != nil && country != nil && city != nil
case .vcard:
isValid = isValidLimit
&& rateToken1.toOptionalDecimal() != nil
&& rateToken2.toOptionalDecimal() != nil
&& tokenGet != nil && country != nil
&& (kind == .sell || (Cards.Model.cleaned(number: vcardNumber).count >= 12
&& Cards.Model.cleaned(number: vcardNumber).count <= 19
&& CCValidator.validate(creditCardNumber: Cards.Model.cleaned(number: vcardNumber), validatePrefix: false, validateLength: true, useLuhnAlgorithm: true)))
&& (kind == .sell || !vcardName.isEmpty)
&& (kind == .sell || isVcardExpirationValid)
&& (kind == .sell || vcardCVV.count == 3)
&& (kind == .sell || !bank.isEmpty)
case .emoney:
isValid = isValidLimit
&& rateToken1.toOptionalDecimal() != nil
&& rateToken2.toOptionalDecimal() != nil
&& tokenGet != nil && paymentSystem != nil
&& (kind == .buy || !account.isEmpty)
case .crypto:
isValid = isValidLimit
&& rateToken1.toOptionalDecimal() != nil
&& rateToken2.toOptionalDecimal() != nil
&& tokenGet != nil && blockchain != nil
&& (kind == .buy || !account.isEmpty)
}
if let order = order {
if amount.toDecimal() > order.volume.decimalValue {
isValid = false
}
}
didUpdate?()
}
private func prepareOrderData() -> String? {
let username = Accounts().current?.username ?? ""
let timeText = Date().toISODateString
let signature = Network.Service.Blockchain.signWithK1(privateKey: AccountViewAuthorize.getPrivateKey(), data: timeText.data(using: .utf8) ?? Data())
switch contentType {
case .card:
return """
{
"data_type": "\(contentType.rawValue)",
"signature": {
"username": "\(username)",
"text": "\(timeText)",
"signature": "\(signature)"
},
"uuid": "\(uuid)",
"payload": {
"currency_id": \(tokenGet?.id ?? -1),
"rate": "\(rateSend)",
"is_cash_rate": "\(rateToken1 == "1")",
\(kind == .buy ? "" : "\"country_id\": \(card?.countryId ?? -1),")
\(kind == .buy ? "" : "\"bank_ids\": \([card?.bankId ?? -1]),")
\(kind == .sell ? "" : "\"country_id\": \(country?.id ?? -1),")
\(kind == .sell ? "" : "\"bank_ids\": [\(bank.map(\.id).map({ "\($0)" }).joined(separator: ","))],")
\(kind == .buy ? "" : "\"card_number\": \"\(Cards.Model.cleaned(number: card?.number ?? ""))\",")
"memo": "\(escapedMemo)"
}
}
"""
case .url:
return """
{
"data_type": "\(contentType.rawValue)",
"signature": {
"username": "\(username)",
"text": "\(timeText)",
"signature": "\(signature)"
},
"uuid": "\(uuid)",
"payload": {
"currency_id": \(tokenGet?.id ?? -1),
"rate": "\(rateSend)",
"is_cash_rate": "\(rateToken1 == "1")",
"country_id": \(country?.id ?? -1),
\(kind == .buy ? "" : "\"payment_url\": \"\(link)\",")
"memo": "\(escapedMemo)"
}
}
"""
case .cash:
return """
{
"data_type": "\(contentType.rawValue)",
"signature": {
"username": "\(username)",
"text": "\(timeText)",
"signature": "\(signature)"
},
"uuid": "\(uuid)",
"payload": {
"currency_id": \(tokenGet?.id ?? -1),
"rate": "\(rateSend)",
"is_cash_rate": "\(rateToken1 == "1")",
"country_id": \(country?.id ?? -1),
"city_id": \(city?.id ?? -1),
"memo": "\(escapedMemo)"
}
}
"""
case .vcard:
return """
{
"data_type": "\(contentType.rawValue)",
"signature": {
"username": "\(username)",
"text": "\(timeText)",
"signature": "\(signature)"
},
"uuid": "\(uuid)",
"payload": {
"currency_id": \(tokenGet?.id ?? -1),
"rate": "\(rateSend)",
"is_cash_rate": "\(rateToken1 == "1")",
"country_id": \(country?.id ?? -1),
\(kind == .sell ? "" : "\"card_number\": \"\(Cards.Model.cleaned(number: vcardNumber))\",")
\(kind == .sell ? "" : "\"card_name\": \"\(vcardName)\",")
\(kind == .sell ? "" : "\"card_expiry\": \"\(vcardExpiration)\",")
\(kind == .sell ? "" : "\"cvv\": \"\(vcardCVV)\",")
\(kind == .sell ? "" : "\"bank_ids\": [\(bank.map(\.id).map({ "\($0)" }).joined(separator: ","))],")
"memo": "\(escapedMemo)"
}
}
"""
case .emoney:
return """
{
"data_type": "\(contentType.rawValue)",
"signature": {
"username": "\(username)",
"text": "\(timeText)",
"signature": "\(signature)"
},
"uuid": "\(uuid)",
"payload": {
"currency_id": \(tokenGet?.id ?? -1),
"rate": "\(rateSend)",
"is_cash_rate": "\(rateToken1 == "1")",
"payment_system_id": \(paymentSystem?.id ?? -1),
\(kind == .buy ? "" : "\"wallet_number\": \"\(account)\",")
"memo": "\(escapedMemo)"
}
}
"""
case .crypto:
return """
{
"data_type": "\(contentType.rawValue)",
"signature": {
"username": "\(username)",
"text": "\(timeText)",
"signature": "\(signature)"
},
"uuid": "\(uuid)",
"payload": {
"currency_id": \(tokenGet?.id ?? -1),
"rate": "\(rateSend)",
"is_cash_rate": "\(rateToken1 == "1")",
"blockchain_id": \(blockchain?.id ?? -1),
\(kind == .buy ? "" : "\"wallet_address\": \"\(account)\",")
"memo": "\(escapedMemo)"
}
}
"""
}
}
private var escapedMemo: String {
memo.replacingOccurrences(of: "\"", with: "\\\"")
}
func submit(privateKey: String, completion: @escaping (Error?) -> Void) {
var actions = [(contract: String, action: Network.Model.Blockchain.Action, data: Codable)]()
let value = (kind == .buy ? Decimal(0) : amount.toDecimal()) + insurance
if let order = order {
actions.append((Network.servers.current.contract(.p2p), kind == .sell ? .clssellord : .clsbuyord, ["ord_id": "\(order.id)", "token": order.symbol]))
let valueInOrder = kind == .sell ? order.volume.decimalValue + order.insurance.decimalValue : order.insurance.decimalValue
if valueInOrder > value {
if !Wallet.Service.Tokens.shared.balances.collection.contains(where: { $0.symbol == order.symbol }) {
actions.append((
Network.servers.current.contract(.cash),
.open, [
"owner": Accounts().current?.username ?? "",
"symbol": "5,\(order.symbol)",
"ram_payer": Accounts().current?.username ?? ""
]
))
}
actions.append((
Network.servers.current.contract(.p2p),
.withdraw, [
"owner" : Accounts().current?.username ?? "",
"value" : (valueInOrder - value).toCanonicalString(precision: 5, symbol: order.symbol),
]
))
}
} else {
if !(p2pBalancesService.balances.filter("symbol == '\(token?.symbol ?? "")'").count > 0) {
actions.append((Network.servers.current.contract(.p2p), .open, [
"owner" : Accounts().current?.username ?? "",
"symbol" : "5, \(token?.symbol ?? "")",
"ram_payer" : Accounts().current?.username ?? ""
]))
}
actions.append((Network.servers.current.contract(.cash), .transfer, Network.Model.Blockchain.Transfer(
from: Accounts().current?.username ?? "",
to: Network.servers.current.contract(.p2p),
quantity: (value + commission).toString(precision: 5, symbol: token?.symbol),
memo: "deposit"
)))
}
actions.append((Network.servers.current.contract(.p2p), kind == .buy ? .crtbuyord : .crtsellord, [
"user" : Accounts().current?.username ?? "",
"volume" : amount.toDecimal().toString(precision: 5, symbol: token?.symbol),
"insurance" : insurance.toString(precision: 5, symbol: token?.symbol),
"min_package" : _limitMin.toDecimal().toString(precision: 5, symbol: token?.symbol),
"max_package" : _limitMax.toDecimal().toString(precision: 5, symbol: token?.symbol),
"meta" : meta,
"data" : data
]))
Network.Service.Blockchain.signActions(actions: actions, privateKeys: [privateKey]) { [weak self] result in
switch result {
case let .success((signatures, serializedTransaction)):
let signaturesString = signatures.map({ "\"\($0)\"" }).joined(separator: ",")
let transaction = Accounts().quota.isEnabled
? "{\"serializedTransaction\":\"\(serializedTransaction)\",\"signatures\":[\(signaturesString)]}"
: "{\"compression\":0,\"packed_trx\":\"\(serializedTransaction)\",\"packed_context_free_data\":\"\",\"signatures\":[\(signaturesString)]}"
let variables = PushTransactionGraphQLRequest.Variables(
transaction: transaction,
transactionType: self?.kind == .buy ? "P2P_CREATE_BUY_ORDER" : "P2P_CREATE_SELL_ORDER",
data: self?.prepareOrderData(),
isUsingFreetx: Accounts().quota.isEnabled
)
AF.request(
Network.servers.current.paycashQraphql,
method: .post,
parameters: PushTransactionGraphQLRequest(variables: variables),
encoder: JSONParameterEncoder.default
).responseDecodable(of: GraphQLResponse<PushTransactionGraphQLRequest.ResponseData>.self) { response in
print(String(data: response.data ?? Data(), encoding: .utf8) ?? "")
completion(response.value?.data?.pushTransaction?.errorsDescription ?? response.value?.errorsDescription)
}
case let .failure(error):
completion(error)
}
}
}
}
}
extension P2POrder {
// swiftlint:disable identifier_name
private struct TogglePause: Codable {
let ord_id: Int
let token: String
}
func pause(privateKey: String, completion: @escaping (NSError?) -> Void) {
Network.Service.Blockchain.execute(
contract: .p2p,
action: kind == .sell ? .blsellord : .blbuyord,
data: TogglePause(ord_id: id, token: symbol),
privateKeys: [privateKey]
) { [weak self] (result) in
if case .success(_) = result {
// swiftlint:disable force_try
try! self?.modify({ $0.status = .inactive })
}
completion(.error(result))
}
}
func resume(privateKey: String, completion: @escaping (NSError?) -> Void) {
Network.Service.Blockchain.execute(
contract: .p2p,
action: kind == .sell ? .unblsellord : .unblbuyord,
data: TogglePause(ord_id: id, token: symbol),
privateKeys: [privateKey]
) { [weak self] (result) in
if case .success(_) = result {
// swiftlint:disable force_try
try! self?.modify({ $0.status = .active })
}
completion(.error(result))
}
}
}
extension P2POrder {
// swiftlint:disable identifier_name
private struct Cancel: Codable {
let ord_id: Int
let token: String
}
func cancel(privateKey: String, completion: @escaping (NSError?) -> Void) {
var actions = [(
contract: Network.Service.Blockchain.Contract,
action: Network.Service.Blockchain.Action,
data: Codable
)]()
actions.append((.p2p, kind == .sell ? .clssellord : .clsbuyord, Cancel(ord_id: id, token: symbol)))
let value = kind == .sell ? volume.decimalValue + insurance.decimalValue : insurance.decimalValue
if value > kCommissionMin {
if !Wallet.Service.Tokens.shared.balances.collection.contains(where: { $0.symbol == symbol }) {
actions.append((
.cash,
.open, [
"owner": Accounts().current?.username ?? "",
"symbol": "5,\(symbol)",
"ram_payer": Accounts().current?.username ?? ""
]
))
}
actions.append((
.p2p,
.withdraw, [
"owner" : Accounts().current?.username ?? "",
"value" : value.toCanonicalString(precision: 5, symbol: symbol),
]
))
}
Network.Service.Blockchain.execute(actions: actions, privateKeys: [privateKey]) { (result) in
if case .success(_) = result {
try? self.delete()
}
completion(.error(result))
}
}
}