Replace the JavaScript mobile app with a native Swift iOS wallet
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
import Foundation
|
||||
|
||||
/// High level XRP operations: wallet creation/import, balances, history, sends.
|
||||
final class XRPWalletService {
|
||||
static let shared = XRPWalletService()
|
||||
|
||||
private let rippleEpoch: TimeInterval = 946_684_800
|
||||
|
||||
// MARK: - Wallet lifecycle
|
||||
|
||||
func createWallet(name: String) throws -> (WalletRecord, String) {
|
||||
let mnemonic = XRPKey.generateMnemonic()
|
||||
let keypair = try XRPKey.keypair(fromMnemonic: mnemonic)
|
||||
let record = WalletRecord(id: "xrp_\(Int(Date().timeIntervalSince1970))",
|
||||
name: name,
|
||||
address: keypair.address,
|
||||
createdAt: Date())
|
||||
WalletStore.saveWallet(record, chain: .xrp)
|
||||
WalletStore.saveSecret(mnemonic, chain: .xrp)
|
||||
return (record, mnemonic)
|
||||
}
|
||||
|
||||
func importWallet(name: String, secret: String) throws -> WalletRecord {
|
||||
let trimmed = secret.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let keypair = try XRPKey.keypair(fromSecret: trimmed)
|
||||
let record = WalletRecord(id: "xrp_\(Int(Date().timeIntervalSince1970))",
|
||||
name: name,
|
||||
address: keypair.address,
|
||||
createdAt: Date())
|
||||
WalletStore.saveWallet(record, chain: .xrp)
|
||||
WalletStore.saveSecret(trimmed, chain: .xrp)
|
||||
return record
|
||||
}
|
||||
|
||||
func address(forSecret secret: String) throws -> String {
|
||||
try XRPKey.keypair(fromSecret: secret.trimmingCharacters(in: .whitespacesAndNewlines)).address
|
||||
}
|
||||
|
||||
// MARK: - Reads
|
||||
|
||||
func balance(address: String) async throws -> Balance {
|
||||
do {
|
||||
let result = try await XRPClient.shared.rpc("account_info",
|
||||
["account": address,
|
||||
"ledger_index": "current"])
|
||||
let accountData = result["account_data"] as? [String: Any] ?? [:]
|
||||
let drops = accountData["Balance"] as? String ?? "0"
|
||||
return Balance(total: XRPAmount.xrp(fromDrops: drops), unlocked: "", funded: true)
|
||||
} catch {
|
||||
if XRPClient.isAccountNotFound(error) {
|
||||
return Balance(total: "0", unlocked: "", funded: false)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
func transactions(address: String) async throws -> [TransactionRecord] {
|
||||
let result = try await XRPClient.shared.rpc("account_tx",
|
||||
["account": address,
|
||||
"ledger_index_min": -1,
|
||||
"ledger_index_max": -1,
|
||||
"limit": 40])
|
||||
let entries = result["transactions"] as? [[String: Any]] ?? []
|
||||
var records: [TransactionRecord] = []
|
||||
|
||||
for entry in entries {
|
||||
guard let tx = entry["tx_json"] as? [String: Any] ?? entry["tx"] as? [String: Any] else { continue }
|
||||
guard (tx["TransactionType"] as? String) == "Payment" else { continue }
|
||||
let meta = entry["meta"] as? [String: Any] ?? entry["metaData"] as? [String: Any] ?? [:]
|
||||
if let txResult = meta["TransactionResult"] as? String, txResult != "tesSUCCESS" { continue }
|
||||
|
||||
// IOU / MPT deliveries carry an object; only native XRP is shown.
|
||||
let delivered = (meta["delivered_amount"] as? String)
|
||||
?? (meta["DeliveredAmount"] as? String)
|
||||
?? (tx["Amount"] as? String)
|
||||
guard let deliveredDrops = delivered else { continue }
|
||||
|
||||
let destination = tx["Destination"] as? String ?? ""
|
||||
let isIncoming = destination == address
|
||||
let date = (tx["date"] as? NSNumber).map { Date(timeIntervalSince1970: $0.doubleValue + rippleEpoch) }
|
||||
|
||||
records.append(TransactionRecord(
|
||||
id: entry["hash"] as? String ?? tx["hash"] as? String ?? UUID().uuidString,
|
||||
amount: XRPAmount.xrp(fromDrops: deliveredDrops),
|
||||
timestamp: date,
|
||||
direction: isIncoming ? .incoming : .outgoing,
|
||||
confirmed: (entry["validated"] as? Bool) ?? false,
|
||||
fee: (tx["Fee"] as? String).map { XRPAmount.xrp(fromDrops: $0) },
|
||||
tag: (tx["DestinationTag"] as? NSNumber)?.intValue,
|
||||
counterparty: isIncoming ? (tx["Account"] as? String) : destination))
|
||||
}
|
||||
return records
|
||||
}
|
||||
|
||||
// MARK: - Sends
|
||||
|
||||
func send(secret: String,
|
||||
to: String,
|
||||
amountXRP: String,
|
||||
destinationTag: UInt32?) async throws -> (hash: String, fee: String) {
|
||||
let destination = to.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard XRPKey.isValidAddress(destination) else {
|
||||
throw XRPError.rpc("Enter a valid XRP address")
|
||||
}
|
||||
|
||||
let keypair = try XRPKey.keypair(fromSeed: secret)
|
||||
let amountDrops = try XRPAmount.drops(fromXRP: amountXRP)
|
||||
|
||||
async let infoCall = XRPClient.shared.rpc("account_info",
|
||||
["account": keypair.address,
|
||||
"ledger_index": "current"])
|
||||
async let feeCall = XRPClient.shared.rpc("fee", [:])
|
||||
async let ledgerCall = XRPClient.shared.rpc("ledger_current", [:])
|
||||
|
||||
let info = try await infoCall
|
||||
let accountData = info["account_data"] as? [String: Any] ?? [:]
|
||||
guard let sequence = (accountData["Sequence"] as? NSNumber)?.uint32Value else {
|
||||
throw XRPError.rpc("Could not read the account sequence")
|
||||
}
|
||||
let balanceDrops = UInt64((accountData["Balance"] as? String) ?? "0") ?? 0
|
||||
|
||||
let feeResult = try await feeCall
|
||||
let feeDrops = (feeResult["drops"] as? [String: Any])
|
||||
.flatMap { $0["open_ledger_fee"] as? String }
|
||||
.flatMap { UInt64($0) } ?? 12
|
||||
|
||||
let ledger = try await ledgerCall
|
||||
let ledgerIndex = (ledger["ledger_current_index"] as? NSNumber)?.uint32Value
|
||||
|
||||
guard amountDrops + feeDrops <= balanceDrops else {
|
||||
throw XRPError.rpc("Insufficient balance to cover the amount and network fee")
|
||||
}
|
||||
|
||||
var payment = XRPPayment(account: keypair.address,
|
||||
destination: destination,
|
||||
amountDrops: amountDrops,
|
||||
feeDrops: feeDrops,
|
||||
sequence: sequence,
|
||||
lastLedgerSequence: ledgerIndex.map { $0 + 20 },
|
||||
destinationTag: destinationTag,
|
||||
signingPublicKey: keypair.publicKey,
|
||||
signature: nil)
|
||||
|
||||
let signature = try XRPKey.sign(message: payment.signingPayload(), keypair: keypair)
|
||||
payment.signature = signature
|
||||
|
||||
let blob = Hex.encode(try payment.txBlob())
|
||||
let result = try await XRPClient.shared.rpc("submit", ["tx_blob": blob])
|
||||
let engineResult = result["engine_result"] as? String ?? ""
|
||||
guard engineResult.hasPrefix("tes") else {
|
||||
let message = result["engine_result_message"] as? String ?? "Transaction failed"
|
||||
throw XRPError.rpc("\(engineResult): \(message)")
|
||||
}
|
||||
|
||||
return (Hex.encode(try payment.transactionHash()), XRPAmount.xrp(fromDrops: feeDrops))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user