Skip to content

Commit ec112e5

Browse files
committed
WIP: V2 protocol
1 parent ff0d6f9 commit ec112e5

File tree

26 files changed

+1337
-1162
lines changed

26 files changed

+1337
-1162
lines changed

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[versions]
2-
lightningkmp = "1.9.0"
2+
lightningkmp = "1.9.1-CARDPAYMENT"
33
secp256k1 = "0.17.1"
44

55
kotlin = "2.1.10"

phoenix-ios/phoenix-ios.xcodeproj/project.pbxproj

Lines changed: 39 additions & 11 deletions
Large diffs are not rendered by default.

phoenix-ios/phoenix-ios/Localizable.xcstrings

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8890,6 +8890,9 @@
88908890
},
88918891
"Card not associated with this wallet." : {
88928892

8893+
},
8894+
"Card options" : {
8895+
88938896
},
88948897
"Caused by" : {
88958898
"localizations" : {
@@ -12252,6 +12255,9 @@
1225212255
}
1225312256
}
1225412257
}
12258+
},
12259+
"Copy simulator's info:" : {
12260+
1225512261
},
1225612262
"Copy transaction id" : {
1225712263
"localizations" : {
@@ -12335,6 +12341,9 @@
1233512341
},
1233612342
"Could not authenticate with card." : {
1233712343

12344+
},
12345+
"Could not communicate with card's host" : {
12346+
"comment" : "Error message - scanning lightning invoice"
1233812347
},
1233912348
"Could not communicate with card's wallet" : {
1234012349
"comment" : "Error message - scanning lightning invoice"
@@ -12548,7 +12557,7 @@
1254812557
}
1254912558
}
1255012559
},
12551-
"Cound not communicate with card's wallet" : {
12560+
"Cound not communicate with card's host" : {
1255212561

1255312562
},
1255412563
"Create New Debit Card" : {
@@ -32574,9 +32583,6 @@
3257432583
},
3257532584
"Read card" : {
3257632585

32577-
},
32578-
"Read Card" : {
32579-
3258032586
},
3258132587
"Read card…" : {
3258232588

@@ -37223,10 +37229,10 @@
3722337229
"Simulator debugging" : {
3722437230

3722537231
},
37226-
"Simulator instructions" : {
37232+
"Simulator Info:" : {
3722737233

3722837234
},
37229-
"Simulator's HEX address:" : {
37235+
"Simulator instructions" : {
3723037236

3723137237
},
3723237238
"Since you've enabled Tor, you should use an onion address for this server." : {
@@ -44810,6 +44816,15 @@
4481044816
}
4481144817
}
4481244818
}
44819+
},
44820+
"Version 1 (lnurl-withdraw)" : {
44821+
44822+
},
44823+
"Version 1 & 2 (lnurl-withdraw + v2 param)" : {
44824+
44825+
},
44826+
"Version 2 (bip-353 & onion messages)" : {
44827+
4481344828
},
4481444829
"Version 2.5.0: Tor changes" : {
4481544830
"localizations" : {
@@ -46830,6 +46845,9 @@
4683046845
}
4683146846
}
4683246847
}
46848+
},
46849+
"Write card for simulator" : {
46850+
4683346851
},
4683446852
"Write error" : {
4683546853

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import Foundation
2+
3+
extension String {
4+
5+
func isValidEmailAddress() -> Bool {
6+
let types: NSTextCheckingResult.CheckingType = [.link]
7+
guard let linkDetector = try? NSDataDetector(types: types.rawValue) else {
8+
return false
9+
}
10+
let range = NSRange(location: 0, length: self.count)
11+
let result = linkDetector.firstMatch(in: self, options: .reportCompletion, range: range)
12+
let scheme = result?.url?.scheme ?? ""
13+
return (scheme == "mailto") && (result?.range.length == self.count)
14+
}
15+
}

phoenix-ios/phoenix-ios/kotlin/KotlinTypes.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ typealias Lightning_kmpNativeSocketException = Lightning_kmp_coreNativeSocketExc
4848
typealias Lightning_kmpNewChannelIncomingPayment = Lightning_kmp_coreNewChannelIncomingPayment
4949
typealias Lightning_kmpNodeEvents = Lightning_kmp_coreNodeEvents
5050
typealias Lightning_kmpNodeParams = Lightning_kmp_coreNodeParams
51+
typealias Lightning_kmpOfferInvoiceReceived = Lightning_kmp_coreOfferInvoiceReceived
5152
typealias Lightning_kmpOfferNotPaid = Lightning_kmp_coreOfferNotPaid
5253
typealias Lightning_kmpOfferPaymentMetadata = Lightning_kmp_coreOfferPaymentMetadata
5354
typealias Lightning_kmpOfferTypesOffer = Lightning_kmp_coreOfferTypesOffer

phoenix-ios/phoenix-ios/nfc/DnaCommunicator/Communicator/DNACommunicator+FileCommands.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ extension DnaCommunicator {
4747
) async -> Result<Void, Error> {
4848

4949
// Pg. 75
50-
let dataSizeBytes = Helper.byteArrayLE(from: Int32(data.count))[0...2]
51-
let offsetBytes = Helper.byteArrayLE(from: Int32(offset))[0...2]
50+
let offsetBytes = Helper.byteArrayLE(from: Int32(offset))[0...2] // 3 bytes
51+
let dataSizeBytes = Helper.byteArrayLE(from: Int32(data.count))[0...2] // 3 bytes
5252

5353
let result = await nxpSwitchedCommand(
5454
mode : mode,

phoenix-ios/phoenix-ios/nfc/DnaCommunicator/Configuration/Ndef.swift

Lines changed: 158 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -38,51 +38,160 @@ enum NdefHeaderType: UInt8 {
3838

3939
public class Ndef {
4040

41-
static let HEADER_SIZE = 7
41+
static let URL_HEADER_SIZE = 7
42+
static let TEXT_HEADER_SIZE = 9
4243

43-
class func ndefDataForUrl(url: URL) -> [UInt8] {
44+
class func ndefDataForUrl(_ url: URL) -> [UInt8] {
4445

4546
// See pgs. 30-31 of AN12196
4647

48+
// NDEF File Format:
49+
//
50+
// Field | Length | Description
51+
// ---------------------------------------------------------------
52+
// NLEN | 2 bytes | Length of the NDEF message in big-endian format.
53+
// NDEF Message | NLEN bytes | NDEF message. See NFC Data Exchange Format (NDEF).
54+
//
55+
// https://docs.nordicsemi.com/bundle/ncs-latest/page/nrfxlib/nfc/doc/type_4_tag.html#t4t-format
56+
//
57+
var fileHeader: [UInt8] = [
58+
0x00, // Placeholder for NLEN
59+
0x00, // Placeholder for NLEN
60+
]
61+
62+
// NDEF Message header:
63+
4764
let flags: NdefHeaderFlags = [.MB, .ME, .SR, .TNF_WELL_KNOWN]
4865
let type = NdefHeaderType.URL
4966

50-
let header: [UInt8] = [
51-
0x00, // Placeholder for data size (two bytes MSB)
52-
0x00,
67+
var messageHeader: [UInt8] = [
5368
flags.rawValue, // NDEF header flags
54-
0x01, // Length of "type" field
55-
0x00, // URL size placeholder
56-
type.rawValue, // This will be a URL record
57-
0x00 // Just the URI (no prepended protocol)
69+
0x01, // Type length
70+
0x00, // URL length placeholder
71+
type.rawValue // Well-known type: URL
72+
]
73+
74+
// Header for: Well-known-type(URL)
75+
//
76+
// Note: If you have a long URL that doesn't fit, you can change the typeHeader here.
77+
// For example, if you specify 0x02 for the typeHeader, it means:
78+
// - prepend `https://www.` to the URL content, saving a few bytes.
79+
//
80+
let typeHeader: [UInt8] = [
81+
0x00 // Just the URI (no prepended protocol)
5882
]
5983

6084
let urlData = url.absoluteString.data(using: .utf8) ?? Data()
6185
var urlBytes = Helper.bytesFromData(data: urlData)
6286

63-
let maxUrlSize = 255 - header.count
87+
let maxFileSize = 256 // As per NTAG 424 spcification for File #2
88+
let maxUrlSize = maxFileSize - (fileHeader.count + messageHeader.count + typeHeader.count)
6489
if urlBytes.count > maxUrlSize {
6590
urlBytes = Array(urlBytes[0..<maxUrlSize])
6691
}
6792

68-
var result: [UInt8] = header + urlBytes
69-
result[1] = UInt8(result.count - 2) // Length of everything that isn't the length
70-
result[4] = UInt8(urlBytes.count + 1) // Everything after type field
93+
fileHeader[1] = UInt8(messageHeader.count + typeHeader.count + urlBytes.count)
94+
messageHeader[2] = UInt8(typeHeader.count + urlBytes.count)
95+
96+
let result: [UInt8] = fileHeader + messageHeader + typeHeader + urlBytes
97+
return result
98+
}
99+
100+
class func ndefDataForText(_ text: String) -> [UInt8] {
101+
102+
// See pgs. 30-31 of AN12196
103+
104+
// NDEF File Format:
105+
//
106+
// Field | Length | Description
107+
// ---------------------------------------------------------------
108+
// NLEN | 2 bytes | Length of the NDEF message in big-endian format.
109+
// NDEF Message | NLEN bytes | NDEF message. See NFC Data Exchange Format (NDEF).
110+
//
111+
// https://docs.nordicsemi.com/bundle/ncs-latest/page/nrfxlib/nfc/doc/type_4_tag.html#t4t-format
112+
//
113+
var fileHeader: [UInt8] = [
114+
0x00, // Placeholder for NLEN
115+
0x00 // Placeholder for NLEN
116+
]
117+
118+
// NDEF Message header:
119+
120+
let flags: NdefHeaderFlags = [.MB, .ME, .SR, .TNF_WELL_KNOWN]
121+
let type = NdefHeaderType.TEXT
122+
123+
var messageHeader: [UInt8] = [
124+
flags.rawValue, // NDEF header flags
125+
0x01, // Type length
126+
0x00, // Text length placeholder
127+
type.rawValue // Well-known type: TEXT
128+
]
129+
130+
// Header for: Well-known-type(TEXT)
131+
//
132+
// RTD TEXT specification:
133+
//
134+
// Byte 0 bit pattern:
135+
//
136+
// | 7 | 6 | 5, 4, 3, 2, 1, 0 |
137+
// ----------------------------------------------
138+
// | UTF 8/16 | Reserved | Language code length |
139+
//
140+
// UTF-8 => 0
141+
// UTF-16 => 1
142+
//
143+
// Reserved => must be 0
144+
//
145+
// Language code should use ISO/IANA language code.
146+
// We will use "en" - although for our use case it will be ignored.
147+
//
148+
// Thus our bit pattern is:
149+
// 0b00000010 = 0x02
150+
//
151+
let typeHeader: [UInt8] = [
152+
0x02, // UTF-8; langCode.length = 2
153+
0x65, // 'e'
154+
0x6e // 'n'
155+
]
156+
157+
let textData = text.data(using: .utf8) ?? Data()
158+
var textBytes = Helper.bytesFromData(data: textData)
159+
160+
let maxFileSize = 256 // As per NTAG 424 spcification for File #2
161+
let maxTextSize = maxFileSize - (fileHeader.count + messageHeader.count + typeHeader.count)
162+
if textBytes.count > maxTextSize {
163+
textBytes = Array(textBytes[0..<maxTextSize])
164+
}
165+
166+
fileHeader[1] = UInt8(messageHeader.count + typeHeader.count + textBytes.count)
167+
messageHeader[2] = UInt8(typeHeader.count + textBytes.count)
71168

169+
let result: [UInt8] = fileHeader + messageHeader + typeHeader + textBytes
72170
return result
73171
}
74172

173+
class func ndefDataForTemplate(_ template: Template) -> [UInt8] {
174+
175+
switch template.value {
176+
case .Left(let url):
177+
return ndefDataForUrl(url)
178+
case .Right(let text):
179+
return ndefDataForText(text)
180+
}
181+
}
182+
75183
struct Template {
76-
let url: URL
184+
let value: Either<URL, String>
77185
let piccDataOffset: Int
78186
let cmacOffset: Int
79187

80-
var urlString: String {
81-
return url.absoluteString
82-
}
83-
84-
var urlData: Data {
85-
return urlString.data(using: .utf8) ?? Data()
188+
var valueString: String {
189+
switch value {
190+
case .Left(let url):
191+
return url.absoluteString
192+
case .Right(let text):
193+
return text
194+
}
86195
}
87196

88197
init?(baseUrl: URL) {
@@ -93,7 +202,7 @@ public class Ndef {
93202

94203
var queryItems = comps.queryItems ?? []
95204

96-
// The `baseUrl` should NOT have either `picc_data` or `cmac` parameters.
205+
// The `baseUrl` SHOULD NOT have either `picc_data` or `cmac` parameters.
97206
// But just to be safe, we'll remove them if they're present.
98207
//
99208
queryItems.removeAll(where: { item in
@@ -132,9 +241,33 @@ public class Ndef {
132241
}
133242
let offset2 = urlUtf8.distance(from: urlUtf8.startIndex, to: range2.upperBound)
134243

135-
self.url = resolvedUrl
136-
self.piccDataOffset = offset1 + Ndef.HEADER_SIZE
137-
self.cmacOffset = offset2 + Ndef.HEADER_SIZE
244+
self.value = Either.Left(resolvedUrl)
245+
self.piccDataOffset = offset1 + Ndef.URL_HEADER_SIZE
246+
self.cmacOffset = offset2 + Ndef.URL_HEADER_SIZE
138247
}
139-
}
248+
249+
init(baseText: String) {
250+
251+
// picc_data=(16_bytes_hexadecimal)
252+
// cmac=(8_bytes_hexadecimal)
253+
254+
let fullText = "\(baseText)?picc_data=00000000000000000000000000000000&cmac=0000000000000000"
255+
// +123456789 123456789 123456789 123456789 123456789
256+
// ^+11 ^+49
257+
258+
// Ultimately, the value gets encoded as UTF-8,
259+
// and the offsets are used as indexes within this UTF-8 representation.
260+
//
261+
// So we need to do our calculations within the string's utf8View.
262+
263+
let baseTextLength = baseText.utf8.count
264+
let offset1 = baseTextLength + 11
265+
let offset2 = baseTextLength + 49
266+
267+
self.value = Either.Right(fullText)
268+
self.piccDataOffset = offset1 + Ndef.TEXT_HEADER_SIZE
269+
self.cmacOffset = offset2 + Ndef.TEXT_HEADER_SIZE
270+
}
271+
272+
} // </struct Template>
140273
}

phoenix-ios/phoenix-ios/nfc/NfcWriter.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ class NfcWriter: NSObject, NFCTagReaderSessionDelegate {
291291
// Write file2 data.
292292

293293
do {
294-
let data = Ndef.ndefDataForUrl(url: input.template.url)
294+
let data = Ndef.ndefDataForTemplate(input.template)
295295
try await writeFile2Data(dna, data, file2Settings).get()
296296
} catch {
297297
return writeDisconnect(error: .protocolError(.writeFile2Data, error))
@@ -439,7 +439,7 @@ class NfcWriter: NSObject, NFCTagReaderSessionDelegate {
439439

440440
do {
441441
let url = URL(string: "https://phoenix.acinq.co")!
442-
let data = Ndef.ndefDataForUrl(url: url)
442+
let data = Ndef.ndefDataForUrl(url)
443443
try await writeFile2Data(dna, data, file2Settings).get()
444444
} catch {
445445
return resetDisconnect(error: .protocolError(.writeFile2Data, error))

0 commit comments

Comments
 (0)