Fill in payments by scanning a QR code or picking one from photos

This commit is contained in:
2026-09-20 02:00:52 +08:00
parent 399502cf72
commit c0903f4f02
8 changed files with 423 additions and 11 deletions
+54
View File
@@ -122,6 +122,11 @@ enum SelfTest {
emit("KOINS_SELFTEST \(name): \(ok ? "PASS" : "FAIL") \(actual)")
}
func expectTrue(_ name: String, _ value: Bool) {
if !value { failures += 1 }
emit("KOINS_SELFTEST \(name): \(value ? "PASS" : "FAIL")")
}
expect("ripemd160", Hex.encode(RIPEMD160.hash(Array("abc".utf8))),
"8eb208f7e05d987a9b044a8e98c6b087f15a0bfc")
@@ -236,6 +241,55 @@ enum SelfTest {
}
}
// QR round trip: generate an image, read it back, parse the payment URI.
let xmrAddress = "46cRPeU77sBVGSVXPyCjvJUdC9AjqvETWb6giUGskdnWN8ZifindmrH7mcztKU52iKgvWXY4dyPujg9LYUY1Q7dvDiwHNCE"
let xrpAddress = "rHR6xtDW7BxeQN8VB8R9HzXzyMeS42n7C"
func roundTrip(_ name: String, _ text: String, checks: [(String, String)]) {
guard let image = QRCodeImage.make(text) else {
failures += 1
emit("KOINS_SELFTEST \(name): FAIL could not render QR image")
return
}
guard let decoded = QRCodeReader.decodeText(in: image) else {
failures += 1
emit("KOINS_SELFTEST \(name): FAIL vision could not read the QR image (\(Int(image.size.width))px)")
return
}
guard let payload = QRCodeReader.parse(decoded) else {
failures += 1
emit("KOINS_SELFTEST \(name): FAIL parse of '\(decoded)'")
return
}
for (label, expected) in checks {
let actual: String
switch label {
case "address": actual = payload.address
case "amount": actual = payload.amount ?? ""
case "tag": actual = payload.tag.map(String.init) ?? ""
case "paymentId": actual = payload.paymentId ?? ""
case "memo": actual = payload.memo ?? ""
default: actual = ""
}
expect("\(name) \(label)", actual, expected)
}
}
roundTrip("qr monero uri",
"monero:\(xmrAddress)?tx_amount=1.5&tx_payment_id=abc123&tx_description=Rent",
checks: [("address", xmrAddress), ("amount", "1.5"), ("paymentId", "abc123"), ("memo", "Rent")])
roundTrip("qr monero plain", xmrAddress, checks: [("address", xmrAddress)])
roundTrip("qr xrp uri",
"ripple:\(xrpAddress)?amount=12.5&dt=99",
checks: [("address", xrpAddress), ("amount", "12.5"), ("tag", "99")])
roundTrip("qr xrp plain", xrpAddress, checks: [("address", xrpAddress)])
expectTrue("qr monero detected", AddressFormat.isMonero(xmrAddress))
expectTrue("qr xrp detected", AddressFormat.isXRP(xrpAddress))
expectTrue("qr rejects junk", QRCodeReader.parse("hello world")?.address == "hello world")
expectTrue("qr not monero", !AddressFormat.isMonero(xrpAddress))
expectTrue("qr not xrp", !AddressFormat.isXRP(xmrAddress))
// secp256k1 signing must produce a low-S DER signature.
if let keypair = try? XRPKey.keypair(fromSeed: secpSeed),
let message = Hex.decode(signingHex),