Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Payment url decoder updates #27

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 72 additions & 27 deletions Gem/Core/Decoder/PaymentURLDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,94 @@

import Foundation
import Primitives
import BigInt

struct Payment: Equatable {
let address: String
let amount: String?
let memo: String?
var network: String?
}

struct PaymentURLDecoder {
private let chainSchemes = Chain.allCases.map({ $0.rawValue })

func decode(_ string: String) throws -> Payment {
guard !string.isEmpty else {
throw AnyError("Input string is empty")
}

let chunks = string.split(separator: ":")

// has more than an address
if chunks.count == 2 {
//TODO: Check prefix for bitcoin and other chains
//let _prefix = chunks[0]
let path = chunks[1]
let pathChunks = path.split(separator: "?")

if pathChunks.count == 1 {
return Payment(address: String(path), amount: .none, memo: .none)
} else if pathChunks.count == 2 {
// BIP21 parsing
let address = String(pathChunks[0])
let query = String(pathChunks[1])
let params = decodeQueryString(query)
let amount = params["amount"]
let memo = params["memo"]

return Payment(address: address, amount: amount, memo: memo)
} else {
throw AnyError("BIP21 format is incorrect")
}
guard chunks.count >= 2 else {
return try parseAddressAndNetwork(path: string)
}

let scheme = String(chunks[0])
guard chainSchemes.contains(scheme) else {
throw AnyError("Unsupported scheme: \(scheme)")
}

let path = String(chunks[1])
let pathChunks = path.split(separator: "?")

if pathChunks.count == 1 {
return try parseAddressAndNetwork(path: String(pathChunks[0]))
} else if pathChunks.count == 2 {
return try parseBIP21(path: String(pathChunks[0]), query: String(pathChunks[1]))
}

throw AnyError("Invalid URL format")
}
}

// MARK: - Private

extension PaymentURLDecoder {
private func parseAddressAndNetwork(path: String) throws -> Payment {
let addressAndNetwork = path.split(separator: "@")

if addressAndNetwork.count == 2 {
let address = String(addressAndNetwork[0])
let networkHex = String(addressAndNetwork[1])
let network = BigInt(hex: networkHex)?.description
return Payment(address: address, amount: .none, memo: .none, network: network)
} else if addressAndNetwork.count == 1 {
return Payment(address: String(path), amount: .none, memo: .none, network: .none)
}

throw AnyError("Invalid address or network format")
}

private func parseBIP21(path: String, query: String) throws -> Payment {
let addressAndNetwork = path.split(separator: "@")
let address: String
let network: String?

if addressAndNetwork.count == 2 {
address = String(addressAndNetwork[0])
let networkHex = String(addressAndNetwork[1])
network = BigInt(hex: networkHex)?.description
} else {
address = String(addressAndNetwork[0])
network = .none
}
return Payment(address: string, amount: .none, memo: .none)

let params = decodeQueryString(query)
let amount = params["amount"]
let memo = params["memo"]

return Payment(address: address, amount: amount, memo: memo, network: network)
}
func decodeQueryString(_ queryString: String) -> [String: String] {

private func decodeQueryString(_ queryString: String) -> [String: String] {
return Dictionary(
uniqueKeysWithValues: queryString
.split(separator: "&")
.compactMap { pair in
let components = pair.split(separator: "=")
return components.count == 2 ? (String(components[0]), String(components[1])) : nil
guard components.count == 2 else { return nil }
let key = String(components[0]).removingPercentEncoding ?? String(components[0])
let value = String(components[1]).removingPercentEncoding ?? String(components[1])
return (key, value)
}
)
}
Expand Down
25 changes: 15 additions & 10 deletions GemTests/Core/Decoder/PaymentURLDecoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,42 +10,47 @@ final class PaymentURLDecoderTests: XCTestCase {
}

func testAddress() {
XCTAssertThrowsError(try PaymentURLDecoder().decode(""))
XCTAssertEqual(
try! PaymentURLDecoder().decode(""),
Payment(address: "", amount: .none, memo: .none)
try! PaymentURLDecoder().decode("0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326"),
Payment(address: "0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326", amount: .none, memo: .none, network: .none)
)
}

func testAddressWithNetwork() {
XCTAssertEqual(
try! PaymentURLDecoder().decode("0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326"),
Payment(address: "0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326", amount: .none, memo: .none)
try! PaymentURLDecoder().decode("0xcB3028d6120802148f03d6c884D6AD6A210Df62A@0x38"),
Payment(address: "0xcB3028d6120802148f03d6c884D6AD6A210Df62A", amount: .none, memo: .none, network: "56")
)
}

func testSolana() {
XCTAssertEqual(
try! PaymentURLDecoder().decode("HA4hQMs22nCuRN7iLDBsBkboz2SnLM1WkNtzLo6xEDY5"),
Payment(address: "HA4hQMs22nCuRN7iLDBsBkboz2SnLM1WkNtzLo6xEDY5", amount: .none, memo: .none)
Payment(address: "HA4hQMs22nCuRN7iLDBsBkboz2SnLM1WkNtzLo6xEDY5", amount: .none, memo: .none, network: .none)
)
XCTAssertEqual(
try! PaymentURLDecoder().decode("solana:HA4hQMs22nCuRN7iLDBsBkboz2SnLM1WkNtzLo6xEDY5?amount=0.266232"),
Payment(address: "HA4hQMs22nCuRN7iLDBsBkboz2SnLM1WkNtzLo6xEDY5", amount: "0.266232", memo: .none)
Payment(address: "HA4hQMs22nCuRN7iLDBsBkboz2SnLM1WkNtzLo6xEDY5", amount: "0.266232", memo: .none, network: .none)
)
}

func testBIP21() {
XCTAssertEqual(
try! PaymentURLDecoder().decode("bitcoin:bc1pn6pua8a566z7t822kphpd2el45ntm23354c3krfmpe3nnn33lkcskuxrdl?amount=0.00001"),
Payment(address: "bc1pn6pua8a566z7t822kphpd2el45ntm23354c3krfmpe3nnn33lkcskuxrdl", amount: "0.00001", memo: .none)
Payment(address: "bc1pn6pua8a566z7t822kphpd2el45ntm23354c3krfmpe3nnn33lkcskuxrdl", amount: "0.00001", memo: .none, network: .none)
)

XCTAssertEqual(
try! PaymentURLDecoder().decode("ethereum:0xA20d8935d61812b7b052E08f0768cFD6D81cB088?amount=0.01233&memo=test"),
Payment(address: "0xA20d8935d61812b7b052E08f0768cFD6D81cB088", amount: "0.01233", memo: "test")
Payment(address: "0xA20d8935d61812b7b052E08f0768cFD6D81cB088", amount: "0.01233", memo: "test", network: .none)
)

XCTAssertEqual(
try! PaymentURLDecoder().decode("solana:3u3ta6yXYgpheLGc2GVF3QkLHAUwBrvX71Eg8XXjJHGw?amount=0.42301"),
Payment(address: "3u3ta6yXYgpheLGc2GVF3QkLHAUwBrvX71Eg8XXjJHGw", amount: "0.42301", memo: .none)
Payment(address: "3u3ta6yXYgpheLGc2GVF3QkLHAUwBrvX71Eg8XXjJHGw", amount: "0.42301", memo: .none, network: .none)
)

XCTAssertEqual(
try! PaymentURLDecoder().decode("ton:EQAzoUpalAaXnVm5MoiYWRZguLFzY0KxFjLv3MkRq5BXzyiQ?amount=0.00001"),
Payment(address: "EQAzoUpalAaXnVm5MoiYWRZguLFzY0KxFjLv3MkRq5BXzyiQ", amount: "0.00001", memo: .none)
Expand Down
Loading