Show live swap status, explorer links, and full kyc.rip route details
This commit is contained in:
@@ -313,12 +313,14 @@ final class MoneroService {
|
||||
|
||||
// MARK: - Sends
|
||||
|
||||
func send(address: String, amountXMR: String, priority: TransactionPriority) async throws -> (hash: String, fee: String) {
|
||||
func send(address: String, amountXMR: String, priority: TransactionPriority,
|
||||
accountIndex: Int = 0) async throws -> (hash: String, fee: String) {
|
||||
let atomic = try MoneroAmount.atomic(fromXMR: amountXMR)
|
||||
let response = try await invoke(["op": "transferTo",
|
||||
"address": address.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
"amount": String(atomic),
|
||||
"priority": priority.rawValue])
|
||||
"priority": priority.rawValue,
|
||||
"accountIndex": accountIndex])
|
||||
return (response["txHash"] as? String ?? "",
|
||||
MoneroAmount.xmr(fromAtomic: response["fee"] as? String ?? "0"))
|
||||
}
|
||||
|
||||
@@ -2,17 +2,24 @@ import Foundation
|
||||
|
||||
/// A route offered by one of kyc.rip's underlying exchange engines.
|
||||
struct SwapRoute: Identifiable {
|
||||
var id: String { provider + engine }
|
||||
var id: String { "\(provider).\(engine).\(amountTo)" }
|
||||
var provider: String
|
||||
var engine: String
|
||||
var amountFrom: Double
|
||||
var amountTo: Double
|
||||
var eta: Int? // minutes
|
||||
var fixedRate: Bool
|
||||
var spread: Double?
|
||||
var spread: Double? // percent above/below the market rate
|
||||
var kycRating: String? // A/B/C privacy grade from the aggregator
|
||||
var logPolicy: String? // the provider's logging policy grade
|
||||
var insurance: Int? // percent of the swap insured by the provider
|
||||
var minDeposit: Double?
|
||||
var maxDeposit: Double?
|
||||
var recommended: Bool
|
||||
}
|
||||
|
||||
struct SwapEstimate {
|
||||
var id: String
|
||||
var amountTo: Double
|
||||
var routes: [SwapRoute]
|
||||
}
|
||||
@@ -33,8 +40,27 @@ struct SwapOrder: Codable, Identifiable {
|
||||
var createdAt: Date
|
||||
var eta: Int?
|
||||
|
||||
/// Latest-known lifecycle state (not persisted).
|
||||
/// Latest-known lifecycle state (refreshed from the API).
|
||||
var status: String?
|
||||
|
||||
/// Transaction hashes reported by the provider once the swap progresses.
|
||||
var txIn: String?
|
||||
var txOut: String?
|
||||
|
||||
/// Provider support link, reported by the status endpoint when available.
|
||||
var supportUrl: String?
|
||||
|
||||
/// Set once the deposit has been broadcast from this device, so the
|
||||
/// "Send deposit" button can never be pressed twice for the same order.
|
||||
var depositSent: Bool?
|
||||
}
|
||||
|
||||
/// Latest-known state of an order, as reported by the status endpoint.
|
||||
struct SwapOrderUpdate {
|
||||
var status: String
|
||||
var txIn: String?
|
||||
var txOut: String?
|
||||
var supportUrl: String?
|
||||
}
|
||||
|
||||
enum SwapError: LocalizedError {
|
||||
@@ -94,14 +120,21 @@ final class SwapService {
|
||||
amountTo: (entry["amount_to"] as? NSNumber)?.doubleValue ?? 0,
|
||||
eta: (entry["eta"] as? NSNumber)?.intValue,
|
||||
fixedRate: (entry["fixed"] as? Bool) ?? false,
|
||||
spread: (entry["spread"] as? NSNumber)?.doubleValue)
|
||||
spread: (entry["spread"] as? NSNumber)?.doubleValue,
|
||||
kycRating: entry["kyc"] as? String,
|
||||
logPolicy: entry["log_policy"] as? String,
|
||||
insurance: (entry["insurance"] as? NSNumber)?.intValue,
|
||||
minDeposit: (entry["min_deposit"] as? NSNumber)?.doubleValue,
|
||||
maxDeposit: (entry["max_deposit"] as? NSNumber)?.doubleValue,
|
||||
recommended: (entry["recommended"] as? Bool) ?? false)
|
||||
}
|
||||
return SwapEstimate(amountTo: amountTo, routes: routes)
|
||||
return SwapEstimate(id: json["id"] as? String ?? "", amountTo: amountTo, routes: routes)
|
||||
}
|
||||
|
||||
// MARK: - Orders
|
||||
|
||||
func createOrder(route: SwapRoute,
|
||||
estimateID: String,
|
||||
from: String,
|
||||
to: String,
|
||||
addressTo: String,
|
||||
@@ -111,7 +144,7 @@ final class SwapService {
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let body: [String: Any] = [
|
||||
var body: [String: Any] = [
|
||||
"provider": route.provider,
|
||||
"engine": route.engine,
|
||||
"from_currency": from,
|
||||
@@ -120,7 +153,15 @@ final class SwapService {
|
||||
"to_network": "Mainnet",
|
||||
"address_to": addressTo,
|
||||
"amount_from": amountFrom,
|
||||
"amount_to": route.amountTo,
|
||||
"fixed_rate": route.fixedRate,
|
||||
"source": "swap",
|
||||
"isPayment": false,
|
||||
]
|
||||
// The create endpoint expects the quote it was estimated against.
|
||||
if !estimateID.isEmpty { body["trade_id"] = estimateID }
|
||||
if let kyc = route.kycRating { body["kyc_rating"] = kyc }
|
||||
if let log = route.logPolicy { body["log_policy"] = log }
|
||||
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
||||
|
||||
let (data, response) = try await session.data(for: request)
|
||||
@@ -141,16 +182,18 @@ final class SwapService {
|
||||
depositAddress: json["depositAddress"] as? String ?? "",
|
||||
depositAmount: (json["depositAmount"] as? NSNumber)?.doubleValue ?? amountFrom,
|
||||
depositMemo: json["depositMemo"] as? String,
|
||||
fromTicker: json["fromTicker"] as? String ?? from.uppercased(),
|
||||
fromTicker: (json["fromTicker"] as? String ?? from).uppercased(),
|
||||
fromAmount: (json["fromAmount"] as? NSNumber)?.doubleValue ?? amountFrom,
|
||||
toTicker: json["toTicker"] as? String ?? to.uppercased(),
|
||||
toTicker: (json["toTicker"] as? String ?? to).uppercased(),
|
||||
toAmount: (json["toAmount"] as? NSNumber)?.doubleValue ?? 0,
|
||||
addressTo: json["address_user"] as? String ?? addressTo,
|
||||
createdAt: Date(),
|
||||
eta: (json["eta"] as? NSNumber)?.intValue)
|
||||
eta: (json["eta"] as? NSNumber)?.intValue,
|
||||
status: json["status"] as? String,
|
||||
supportUrl: nonEmpty(json["supportUrl"] as? String))
|
||||
}
|
||||
|
||||
func orderStatus(_ order: SwapOrder) async throws -> (status: String, txIn: String?, txOut: String?) {
|
||||
func orderStatus(_ order: SwapOrder) async throws -> SwapOrderUpdate {
|
||||
var components = URLComponents(string: "\(base)/v2/exchange/status/\(order.id)")
|
||||
components?.queryItems = [URLQueryItem(name: "engine", value: order.engine)]
|
||||
guard let url = components?.url else { throw SwapError.noRoute }
|
||||
@@ -163,12 +206,22 @@ final class SwapService {
|
||||
}
|
||||
if let error = json["error"] as? String { throw SwapError.api(error) }
|
||||
|
||||
let txIn = json["txIn"] as? String
|
||||
?? (json["txIn"] as? [String: Any]).flatMap { $0["hash"] as? String }
|
||||
let txOut = json["txOut"] as? String
|
||||
?? (json["txOut"] as? [String: Any]).flatMap { $0["hash"] as? String }
|
||||
let txIn = nonEmpty(json["txIn"] as? String)
|
||||
?? (json["txIn"] as? [String: Any]).flatMap { nonEmpty($0["hash"] as? String) }
|
||||
let txOut = nonEmpty(json["txOut"] as? String)
|
||||
?? (json["txOut"] as? [String: Any]).flatMap { nonEmpty($0["hash"] as? String) }
|
||||
|
||||
return (json["status"] as? String ?? "UNKNOWN", txIn, txOut)
|
||||
return SwapOrderUpdate(status: json["status"] as? String ?? "UNKNOWN",
|
||||
txIn: txIn,
|
||||
txOut: txOut,
|
||||
supportUrl: nonEmpty(json["supportUrl"] as? String))
|
||||
}
|
||||
|
||||
/// The API sends blank strings while a hash is still unknown; treat those as
|
||||
/// absent so the UI does not render empty rows.
|
||||
private func nonEmpty(_ value: String?) -> String? {
|
||||
guard let value, !value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { return nil }
|
||||
return value
|
||||
}
|
||||
|
||||
private func check(_ response: URLResponse) throws {
|
||||
|
||||
Reference in New Issue
Block a user