Skip to content

Commit

Permalink
Decode Invoice
Browse files Browse the repository at this point in the history
  • Loading branch information
wuyuehyang committed Jan 4, 2025
1 parent e80cbcd commit d243828
Show file tree
Hide file tree
Showing 4 changed files with 209 additions and 8 deletions.
4 changes: 4 additions & 0 deletions Mixin.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1220,6 +1220,7 @@
94F237562C6915B70057D1AB /* PriceHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94F237552C6915B70057D1AB /* PriceHistory.swift */; };
94F2375C2C691D5C0057D1AB /* TokenPriceChartCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94F2375A2C691D5C0057D1AB /* TokenPriceChartCell.swift */; };
94F2375D2C691D5C0057D1AB /* TokenPriceChartCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 94F2375B2C691D5C0057D1AB /* TokenPriceChartCell.xib */; };
94F35A602D255B61001BCAE7 /* Invoice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94F35A5F2D255B5F001BCAE7 /* Invoice.swift */; };
94F36AC626CA59B300AC30A5 /* PhoneNumberValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94F36AC526CA59B300AC30A5 /* PhoneNumberValidator.swift */; };
94F952582AE616F40025B995 /* TransferOutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94F952572AE616F40025B995 /* TransferOutViewController.swift */; };
94F9525D2AE6174A0025B995 /* TransferOutView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 94F9525C2AE6174A0025B995 /* TransferOutView.xib */; };
Expand Down Expand Up @@ -2715,6 +2716,7 @@
94F237552C6915B70057D1AB /* PriceHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceHistory.swift; sourceTree = "<group>"; };
94F2375A2C691D5C0057D1AB /* TokenPriceChartCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenPriceChartCell.swift; sourceTree = "<group>"; };
94F2375B2C691D5C0057D1AB /* TokenPriceChartCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TokenPriceChartCell.xib; sourceTree = "<group>"; };
94F35A5F2D255B5F001BCAE7 /* Invoice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Invoice.swift; sourceTree = "<group>"; };
94F36AC526CA59B300AC30A5 /* PhoneNumberValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberValidator.swift; sourceTree = "<group>"; };
94F952572AE616F40025B995 /* TransferOutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferOutViewController.swift; sourceTree = "<group>"; };
94F9525C2AE6174A0025B995 /* TransferOutView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TransferOutView.xib; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4230,6 +4232,7 @@
940DC5912B075E03009A15D1 /* SafePaymentURL.swift */,
94BD7E762B2983CA0004576D /* MultisigURL.swift */,
94EE1C002B4C647500BE9AD1 /* CodeURL.swift */,
94F35A5F2D255B5F001BCAE7 /* Invoice.swift */,
94C5A8922B2157CE00201CDA /* Payment.swift */,
94C6987A2B23257B002377EA /* PaymentPrecondition.swift */,
94A40C462B2781AF0028BC02 /* TransferPaymentOperation.swift */,
Expand Down Expand Up @@ -6207,6 +6210,7 @@
7C427BC428373F8000FFDE12 /* Wallpaper.swift in Sources */,
E090B90F23B0B27F0012C7E9 /* ConversationDAO+Search.swift in Sources */,
7BF42E3122DC85E9005066E6 /* GalleryImageItemViewController.swift in Sources */,
94F35A602D255B61001BCAE7 /* Invoice.swift in Sources */,
940764C529B26B0B00B779A6 /* PropertyListEncoder+Global.swift in Sources */,
7B8ED48021198EC20039946B /* StickerInputModelController.swift in Sources */,
94A0C8C32C77259E00BDE672 /* AddressReceiversCell.swift in Sources */,
Expand Down
192 changes: 192 additions & 0 deletions Mixin/UserInterface/Controllers/Wallet/Payment/Invoice.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import Foundation
import CryptoKit
import MixinServices

struct Invoice {

enum Reference {
case hash(String)
case index(Int)
}

struct Entry {
let traceID: String
let assetID: String
let amount: Decimal
let memo: String
let references: [Reference]
}

let recipient: MIXAddress
let entries: [Entry]

}

extension Invoice {

private enum InitError: Error {
case invalidString
case base64Decoding
case invalidLength(Int)
case invalidChecksum
case unknownVersion
case invalidRecipient
case invalidEntriesCount
case invalidEntry
case invalidReferenceType
case invalidHashReference
case invalidIndexReference
case sha3
}

private class DataReader {

private let data: Data

private var location: Data.Index

init(data: Data) {
self.data = data
self.location = data.startIndex
}

func readUInt8() -> UInt8? {
guard location != data.endIndex else {
return nil
}
let location = self.location
self.location = location.advanced(by: 1)
return data[location]
}

func readUInt16() -> UInt16? {
let nextLocation = location.advanced(by: 2)
guard nextLocation <= data.endIndex else {
return nil
}
let low = data[location]
let high = data[location.advanced(by: 1)]
self.location = nextLocation
return UInt16(data: [low, high], endianess: .big)
}

func readBytes(count: Int) -> Data? {
if count == 0 {
return Data()
}
let nextLocation = location.advanced(by: count)
guard nextLocation <= data.endIndex else {
return nil
}
let bytes = data[location..<nextLocation]
self.location = nextLocation
return bytes
}

func readUUID() -> String? {
if let data = readBytes(count: UUID.dataCount) {
UUID(data: data).uuidString.lowercased()
} else {
nil
}
}

}

private static let version: UInt8 = 0

init(string: String) throws {
let prefix = "MIN"

guard string.hasPrefix(prefix) else {
throw InitError.invalidString
}
guard let data = Data(base64URLEncoded: string.suffix(string.count - prefix.count)) else {
throw InitError.base64Decoding
}
guard data.count >= 3 + 23 + 1 else {
throw InitError.invalidLength(data.count)
}

let payload = data.prefix(data.count - 4)
let expectedChecksum = data.suffix(4)
let checksum = try {
let data = prefix.data(using: .utf8)! + payload
guard let digest = SHA3_256.hash(data: data) else {
throw InitError.sha3
}
return digest.prefix(4)
}()
guard expectedChecksum == checksum else {
throw InitError.invalidChecksum
}

let reader = DataReader(data: payload)
guard reader.readUInt8() == Self.version else {
throw InitError.unknownVersion
}

guard
let recipientLength = reader.readUInt16(),
let recipientData = reader.readBytes(count: Int(recipientLength)),
let recipient = MIXAddress(data: recipientData)
else {
throw InitError.invalidRecipient
}

guard let entriesCount = reader.readUInt8() else {
throw InitError.invalidEntriesCount
}
var entries: [Entry] = []
entries.reserveCapacity(Int(entriesCount))
for _ in 0..<entriesCount {
guard
let traceID = reader.readUUID(),
let assetID = reader.readUUID(),
let amountLength = reader.readUInt8(),
let amountData = reader.readBytes(count: Int(amountLength)),
let amountString = String(data: amountData, encoding: .utf8),
let amount = Decimal(string: amountString, locale: .enUSPOSIX),
let extraLength = reader.readUInt16(),
let extra = reader.readBytes(count: Int(extraLength)),
let memo = String(data: extra, encoding: .utf8),
let referencesCount = reader.readUInt8()
else {
throw InitError.invalidEntry
}

let references: [Reference] = try (0..<referencesCount).map { _ in
switch reader.readUInt8() {
case 0:
if let hash = reader.readBytes(count: 32) {
return .hash(hash.hexEncodedString())
} else {
throw InitError.invalidHashReference
}
case 1:
if let index = reader.readUInt8(), index < entries.count {
return .index(Int(index))
} else {
throw InitError.invalidIndexReference
}
default:
throw InitError.invalidReferenceType
}
}

let entry = Entry(
traceID: traceID,
assetID: assetID,
amount: amount,
memo: memo,
references: references
)
entries.append(entry)
}

self.recipient = recipient
self.entries = entries
}

}

14 changes: 9 additions & 5 deletions Mixin/UserInterface/Controllers/Wallet/Payment/MIXAddress.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ enum MIXAddress {
init?(string: String) {
let header = "MIX"
let headerData = header.data(using: .utf8)!
let version: UInt8 = 2

guard string.hasPrefix(header) else {
return nil
Expand Down Expand Up @@ -43,20 +42,25 @@ enum MIXAddress {
return nil
}

let payloadVersion: UInt8 = payload[0]
self.init(data: payload)
}

init?(data payload: Data) {
let version: UInt8 = 2
let payloadVersion: UInt8 = payload[payload.startIndex]
guard version == payloadVersion else {
Logger.general.debug(category: "MIXAddress", message: "Unknown version")
return nil
}

let threshold: UInt8 = payload[1]
let membersCount: Int = Int(payload[2])
let threshold: UInt8 = payload[payload.startIndex.advanced(by: 1)]
let membersCount: Int = Int(payload[payload.startIndex.advanced(by: 2)])
guard threshold != 0 && threshold <= membersCount && membersCount <= 64 else {
Logger.general.debug(category: "MIXAddress", message: "Invalid threshold: \(threshold), total: \(membersCount)")
return nil
}

let membersData = payload[3...]
let membersData = payload[payload.startIndex.advanced(by: 3)...]
switch membersData.count {
case 16 * membersCount:
let userIDs = (0..<membersCount).map { i in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ public extension Data {
self.init(bytesNoCopy: bytes, count: count, deallocator: .free)
}

init?(base64URLEncoded string: String) {
init?<S: StringProtocol>(base64URLEncoded string: S) {
var str = string
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
if string.count % 4 != 0 {
str.append(String(repeating: "=", count: 4 - string.count % 4))
let remainder = string.count % 4
if remainder != 0 {
str.append(String(repeating: "=", count: 4 - remainder))
}
self.init(base64Encoded: str)
}
Expand Down

0 comments on commit d243828

Please sign in to comment.