Skip to content

Commit

Permalink
Updated PaymentURLDecoder to support metamask like addresses <#22>
Browse files Browse the repository at this point in the history
  • Loading branch information
gemdev111 committed Jun 3, 2024
1 parent df83b07 commit 0b38b12
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 39 deletions.
98 changes: 72 additions & 26 deletions Gem/Core/Decoder/PaymentURLDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,95 @@

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]))
}
return Payment(address: string, amount: .none, memo: .none)

throw AnyError("Invalid URL format")
}

func decodeQueryString(_ queryString: String) -> [String: String] {
}

// 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
}

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

return Payment(address: address, amount: amount, memo: memo, network: network)
}

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
31 changes: 18 additions & 13 deletions GemTests/Core/Decoder/PaymentURLDecoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,43 +9,48 @@ final class PaymentURLDecoderTests: XCTestCase {
// Put setup code here. This method is called before the invocation of each test method in the class.
}

func testAddress() {
XCTAssertEqual(
try! PaymentURLDecoder().decode(""),
Payment(address: "", amount: .none, memo: .none)
)
func testAddress() {
XCTAssertThrowsError(try PaymentURLDecoder().decode(""))
XCTAssertEqual(
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

0 comments on commit 0b38b12

Please sign in to comment.