Replace the JavaScript mobile app with a native Swift iOS wallet
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
import CryptoKit
|
||||
import Foundation
|
||||
|
||||
enum XRPAlgorithm: String {
|
||||
case ed25519
|
||||
case secp256k1
|
||||
}
|
||||
|
||||
/// Set by the app target so this file stays compilable outside iOS.
|
||||
enum Secp256k1Backend {
|
||||
static var derivePrivateKey: ((Data) -> Data?)?
|
||||
static var publicKey: ((Data) -> Data?)?
|
||||
static var sign: ((Data, Data) -> Data?)?
|
||||
static var addScalar: ((Data, Data) -> Data?)?
|
||||
}
|
||||
|
||||
struct XRPKeypair {
|
||||
let algorithm: XRPAlgorithm
|
||||
/// ed25519: 32-byte seed. secp256k1: 32-byte scalar.
|
||||
let privateKey: [UInt8]
|
||||
/// ed25519: 0xED + 32-byte key. secp256k1: 33-byte compressed point.
|
||||
let publicKey: [UInt8]
|
||||
|
||||
var address: String { XRPKey.address(fromPublicKey: publicKey) }
|
||||
|
||||
var signingPublicKeyHex: String { Hex.encode(publicKey) }
|
||||
}
|
||||
|
||||
enum XRPKey {
|
||||
static let ed25519SeedVersion: [UInt8] = [0x01, 0xe1, 0x4b]
|
||||
static let familySeedVersion: [UInt8] = [0x21]
|
||||
static let accountIdVersion: [UInt8] = [0x00]
|
||||
static let ed25519Prefix: UInt8 = 0xED
|
||||
|
||||
// MARK: - Seeds
|
||||
|
||||
static func generateSeed(algorithm: XRPAlgorithm = .ed25519) -> String {
|
||||
var entropy = [UInt8](repeating: 0, count: 16)
|
||||
for index in entropy.indices { entropy[index] = UInt8.random(in: 0...255) }
|
||||
let version = algorithm == .ed25519 ? ed25519SeedVersion : familySeedVersion
|
||||
return XRPBase58.checkEncode(version: version, payload: entropy)
|
||||
}
|
||||
|
||||
static func decodeSeed(_ seed: String) throws -> (entropy: [UInt8], algorithm: XRPAlgorithm) {
|
||||
let decoded = try XRPBase58.decode(seed.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
guard decoded.count > 4 else { throw XRPError.invalidSeed }
|
||||
let body = Array(decoded[0..<(decoded.count - 4)])
|
||||
let checksum = Array(decoded[(decoded.count - 4)...])
|
||||
guard XRPHash.checksum(body) == checksum else { throw XRPError.invalidSeed }
|
||||
|
||||
if body.count == ed25519SeedVersion.count + 16, Array(body.prefix(3)) == ed25519SeedVersion {
|
||||
return (Array(body.dropFirst(3)), .ed25519)
|
||||
}
|
||||
if body.count == familySeedVersion.count + 16, body[0] == familySeedVersion[0] {
|
||||
return (Array(body.dropFirst(1)), .secp256k1)
|
||||
}
|
||||
throw XRPError.unsupportedSeed
|
||||
}
|
||||
|
||||
// MARK: - Keys
|
||||
|
||||
/// Recovery phrase for a new wallet: 12 BIP-39 words.
|
||||
static func generateMnemonic() -> String {
|
||||
BIP39.generateMnemonic()
|
||||
}
|
||||
|
||||
/// Accepts either a 12 word BIP-39 recovery phrase or a legacy XRP family
|
||||
/// seed (`sEd…` / `s…`).
|
||||
static func keypair(fromSecret secret: String) throws -> XRPKeypair {
|
||||
let trimmed = secret.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if BIP39.isMnemonic(trimmed) {
|
||||
return try keypair(fromMnemonic: trimmed)
|
||||
}
|
||||
return try keypair(fromSeed: trimmed)
|
||||
}
|
||||
|
||||
/// Derives the account at `m/44'/144'/0'/0/0`, the path hardware and mobile
|
||||
/// wallets use for XRP.
|
||||
static func keypair(fromMnemonic mnemonic: String) throws -> XRPKeypair {
|
||||
guard BIP39.entropy(from: mnemonic) != nil else {
|
||||
throw XRPError.invalidSeed
|
||||
}
|
||||
let seed = BIP39.seed(from: mnemonic)
|
||||
guard let master = BIP32.masterKey(fromSeed: seed),
|
||||
let derived = BIP32.derive(BIP32.xrpPath, from: master) else {
|
||||
throw XRPError.unsupportedSeed
|
||||
}
|
||||
return XRPKeypair(algorithm: .secp256k1,
|
||||
privateKey: derived.privateKey,
|
||||
publicKey: derived.publicKey)
|
||||
}
|
||||
|
||||
static func keypair(fromSeed seed: String) throws -> XRPKeypair {
|
||||
let (entropy, algorithm) = try decodeSeed(seed)
|
||||
switch algorithm {
|
||||
case .ed25519:
|
||||
let raw = XRPHash.sha512Half(entropy)
|
||||
let privateKey = try Curve25519.Signing.PrivateKey(rawRepresentation: Data(raw))
|
||||
let publicKey = [ed25519Prefix] + Array(privateKey.publicKey.rawRepresentation)
|
||||
return XRPKeypair(algorithm: .ed25519, privateKey: raw, publicKey: publicKey)
|
||||
case .secp256k1:
|
||||
guard let derive = Secp256k1Backend.derivePrivateKey,
|
||||
let toPublic = Secp256k1Backend.publicKey,
|
||||
let privateKey = derive(Data(entropy)),
|
||||
let publicKey = toPublic(privateKey) else {
|
||||
throw XRPError.unsupportedSeed
|
||||
}
|
||||
return XRPKeypair(algorithm: .secp256k1,
|
||||
privateKey: Array(privateKey),
|
||||
publicKey: Array(publicKey))
|
||||
}
|
||||
}
|
||||
|
||||
static func address(fromPublicKey publicKey: [UInt8]) -> String {
|
||||
let accountId = XRPHash.ripemd160(XRPHash.sha256(publicKey))
|
||||
return XRPBase58.checkEncode(version: accountIdVersion, payload: accountId)
|
||||
}
|
||||
|
||||
static func isValidAddress(_ address: String) -> Bool {
|
||||
(try? XRPBase58.checkDecode(address, versionLength: 1)) != nil
|
||||
}
|
||||
|
||||
static func accountId(fromAddress address: String) throws -> [UInt8] {
|
||||
let payload = try XRPBase58.checkDecode(address, versionLength: 1)
|
||||
guard payload.count == 20 else { throw XRPError.invalidAddress }
|
||||
return payload
|
||||
}
|
||||
|
||||
// MARK: - Signing
|
||||
|
||||
/// ed25519 returns a 64-byte signature; secp256k1 a DER-encoded one, matching
|
||||
/// what the XRP Ledger expects for each signing scheme.
|
||||
static func sign(message: [UInt8], keypair: XRPKeypair) throws -> [UInt8] {
|
||||
switch keypair.algorithm {
|
||||
case .ed25519:
|
||||
let privateKey = try Curve25519.Signing.PrivateKey(rawRepresentation: Data(keypair.privateKey))
|
||||
let signature = try privateKey.signature(for: Data(message))
|
||||
return Array(signature)
|
||||
case .secp256k1:
|
||||
let digest = XRPHash.sha512Half(message)
|
||||
guard let signFn = Secp256k1Backend.sign,
|
||||
let signature = signFn(Data(digest), Data(keypair.privateKey)) else {
|
||||
throw XRPError.unsupportedSeed
|
||||
}
|
||||
return Array(signature)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user