lnlink

iOS app for connecting to lightning nodes
git clone git://jb55.com/lnlink
Log | Files | Refs | Submodules | README

commit 8b307f8a359f39bc58b78d874c8a09e8d3460a0c
parent 1d30d0d5662583178de7823d1fe3351a6a101aa4
Author: William Casarin <jb55@jb55.com>
Date:   Sun, 20 Feb 2022 17:50:16 -0800

invoices: parse invoice amount

Let's skip parsing the entire invoice for now since that's a bit
complicated

Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mlightninglink.xcodeproj/project.pbxproj | 10++++------
Dlightninglink/Bech32.swift | 347-------------------------------------------------------------------------------
Mlightninglink/ContentView.swift | 27+++++++++++++++------------
Mlightninglink/Invoice.swift | 52+++++++++++++++++++++++++++++++++++++---------------
Alightninglink/LNLink.swift | 14++++++++++++++
Mlightninglink/PayView.swift | 34++++++++++++++++++++++++----------
Mlightninglink/RPC.swift | 3++-
Mlightninglink/lightninglinkApp.swift | 7++++---
8 files changed, 100 insertions(+), 394 deletions(-)

diff --git a/lightninglink.xcodeproj/project.pbxproj b/lightninglink.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ 4C0359FB27AEE86600FF92CE /* QRScan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0359FA27AEE86600FF92CE /* QRScan.swift */; }; 4C035A0027AEF90000FF92CE /* PayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0359FF27AEF90000FF92CE /* PayView.swift */; }; - 4C035A0227AEFB2400FF92CE /* Bech32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C035A0127AEFB2400FF92CE /* Bech32.swift */; }; 4C035A0427AEFD2F00FF92CE /* Invoice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C035A0327AEFD2F00FF92CE /* Invoice.swift */; }; 4C641D192788FF2F002A36C9 /* lightninglinkApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C641D182788FF2F002A36C9 /* lightninglinkApp.swift */; }; 4C641D1B2788FF2F002A36C9 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C641D1A2788FF2F002A36C9 /* ContentView.swift */; }; @@ -19,12 +18,12 @@ 4C641D342788FF31002A36C9 /* lightninglinkUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C641D332788FF31002A36C9 /* lightninglinkUITests.swift */; }; 4C641D362788FF31002A36C9 /* lightninglinkUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C641D352788FF31002A36C9 /* lightninglinkUITestsLaunchTests.swift */; }; 4C641D492789083E002A36C9 /* lightninglink.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C641D482789083E002A36C9 /* lightninglink.c */; }; - 4C641D4B279CFA32002A36C9 /* lnsocket in Resources */ = {isa = PBXBuildFile; fileRef = 4C641D4A279CFA32002A36C9 /* lnsocket */; }; 4C873FCF27A62DC1008C972C /* lnsocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C873FCE27A62DC1008C972C /* lnsocket.a */; }; 4C873FD127A62DE7008C972C /* libsodium.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C873FD027A62DE7008C972C /* libsodium.a */; }; 4C873FD327A62DF5008C972C /* libsecp256k1.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4C873FD227A62DF5008C972C /* libsecp256k1.a */; }; 4C873FD527A6EF3F008C972C /* LNSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C873FD427A6EF3F008C972C /* LNSocket.swift */; }; 4C873FD727A6F1F5008C972C /* RPC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C873FD627A6F1F5008C972C /* RPC.swift */; }; + 4C8B289327B44EAF00DF3372 /* LNLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8B289227B44EAF00DF3372 /* LNLink.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -48,7 +47,6 @@ 4C0359FA27AEE86600FF92CE /* QRScan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScan.swift; sourceTree = "<group>"; }; 4C0359FE27AEEE8500FF92CE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; 4C0359FF27AEF90000FF92CE /* PayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PayView.swift; sourceTree = "<group>"; }; - 4C035A0127AEFB2400FF92CE /* Bech32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32.swift; sourceTree = "<group>"; }; 4C035A0327AEFD2F00FF92CE /* Invoice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Invoice.swift; sourceTree = "<group>"; }; 4C641D152788FF2F002A36C9 /* lightninglink.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = lightninglink.app; sourceTree = BUILT_PRODUCTS_DIR; }; 4C641D182788FF2F002A36C9 /* lightninglinkApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = lightninglinkApp.swift; sourceTree = "<group>"; }; @@ -70,6 +68,7 @@ 4C873FD227A62DF5008C972C /* libsecp256k1.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libsecp256k1.a; path = lnsocket/target/ios/libsecp256k1.a; sourceTree = "<group>"; }; 4C873FD427A6EF3F008C972C /* LNSocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LNSocket.swift; sourceTree = "<group>"; }; 4C873FD627A6F1F5008C972C /* RPC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RPC.swift; sourceTree = "<group>"; }; + 4C8B289227B44EAF00DF3372 /* LNLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNLink.swift; sourceTree = "<group>"; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -135,8 +134,8 @@ 4C873FD627A6F1F5008C972C /* RPC.swift */, 4C0359FA27AEE86600FF92CE /* QRScan.swift */, 4C0359FF27AEF90000FF92CE /* PayView.swift */, - 4C035A0127AEFB2400FF92CE /* Bech32.swift */, 4C035A0327AEFD2F00FF92CE /* Invoice.swift */, + 4C8B289227B44EAF00DF3372 /* LNLink.swift */, ); path = lightninglink; sourceTree = "<group>"; @@ -294,7 +293,6 @@ files = ( 4C641D202788FF30002A36C9 /* Preview Assets.xcassets in Resources */, 4C641D1D2788FF30002A36C9 /* Assets.xcassets in Resources */, - 4C641D4B279CFA32002A36C9 /* lnsocket in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -322,11 +320,11 @@ 4C035A0027AEF90000FF92CE /* PayView.swift in Sources */, 4C873FD527A6EF3F008C972C /* LNSocket.swift in Sources */, 4C641D1B2788FF2F002A36C9 /* ContentView.swift in Sources */, + 4C8B289327B44EAF00DF3372 /* LNLink.swift in Sources */, 4C641D492789083E002A36C9 /* lightninglink.c in Sources */, 4C035A0427AEFD2F00FF92CE /* Invoice.swift in Sources */, 4C641D192788FF2F002A36C9 /* lightninglinkApp.swift in Sources */, 4C873FD727A6F1F5008C972C /* RPC.swift in Sources */, - 4C035A0227AEFB2400FF92CE /* Bech32.swift in Sources */, 4C0359FB27AEE86600FF92CE /* QRScan.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/lightninglink/Bech32.swift b/lightninglink/Bech32.swift @@ -1,347 +0,0 @@ -// Copyright (c) 2017 Alex Bosworth -// Copyright (c) 2017 Pieter Wuille -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. -// - -import Foundation - -extension String { - func lastIndex(of string: String) -> Int? { - guard let range = self.range(of: string, options: .backwards) else { return nil } - - return self.distance(from: startIndex, to: range.lowerBound) - } -} - -let CHARSET = byteConvert(string: "qpzry9x8gf2tvdw0s3jn54khce6mua7l") -let GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] - -func polymod(_ values: [Int]) -> Int { - return values.reduce(1) { chk, value in - let top = chk >> 25 - - return (Int()..<5).reduce((chk & 0x1ffffff) << 5 ^ value) { chk, i in - guard (top >> i) & 1 > Int() else { return chk } - - return chk ^ GENERATOR[i] - } - } -} - -func hrpExpand(_ hrp: [UInt8]) -> [UInt8] { - return (Int()..<hrp.count).map { hrp[$0] >> 5 } + [UInt8()] + (Int()..<hrp.count).map { hrp[$0] & 31 } -} - -func verifyChecksum(hrp: [UInt8], data: [UInt8]) -> Bool { - return polymod((hrpExpand(hrp) + data).map { Int($0) }) == 1 -} - -func createChecksum(hrp: [UInt8], data: [UInt8]) -> [UInt8] { - let values = (hrpExpand(hrp) + data + Array(repeating: UInt8(), count: 6)).map { Int($0) } - let mod: Int = polymod(values) ^ 1 - - return (Int()..<6).map { (mod >> (5 * (5 - $0))) & 31 }.map { UInt8($0) } -} - -func byteConvert(string: String) -> [UInt8] { - return string.map { String($0).unicodeScalars.first?.value }.flatMap { $0 }.map { UInt8($0) } -} - -func stringConvert(bytes: [UInt8]) -> String { - return bytes.reduce(String(), { $0 + String(format: "%c", $1)}) -} - -func encode(hrp: [UInt8], data: [UInt8]) -> String { - let checksum = createChecksum(hrp: hrp, data: data) - - return stringConvert(bytes: hrp) + "1" + stringConvert(bytes: (data + checksum).map { CHARSET[Int($0)] }) -} - -enum DecodeBech32Error: Error { - case caseMixing - case inconsistentHrp - case invalidAddress - case invalidBits - case invalidCharacter(String) - case invalidChecksum - case invalidPayToHashLength - case invalidVersion - case missingSeparator - case missingVersion - - var localizedDescription: String { - switch self { - case .caseMixing: - return "Mixed case characters are not allowed" - - case .inconsistentHrp: - return "Internally inconsistent HRP" - - case .invalidAddress: - return "Address is not a valid type" - - case .invalidBits: - return "Bits are not valid" - - case .invalidCharacter(let char): - return "Character \"\(char)\" is not valid" - - case .invalidChecksum: - return "Checksum failed to verify data" - - case .invalidPayToHashLength: - return "Unknown hash length for encoded output payload hash" - - case .invalidVersion: - return "Invalid version number" - - case .missingSeparator: - return "Missing address data separator" - - case .missingVersion: - return "Missing address version" - } - } -} - -public func decodeBech32(bechString: String) throws -> (hrp: [UInt8], data: [UInt8]) { - let bechBytes = byteConvert(string: bechString) - - guard !(bechBytes.contains() { $0 < 33 && $0 > 126 }) else { throw DecodeBech32Error.invalidCharacter(bechString) } - - let hasLower = bechBytes.contains() { $0 >= 97 && $0 <= 122 } - let hasUpper = bechBytes.contains() { $0 >= 65 && $0 <= 90 } - - if hasLower && hasUpper { throw DecodeBech32Error.caseMixing } - - let bechString = bechString.lowercased() - - guard let pos = bechString.lastIndex(of: "1") else { throw DecodeBech32Error.missingSeparator } - - if pos < 1 || pos + 7 > bechString.count { - throw DecodeBech32Error.missingSeparator - } - - let bechStringBytes = byteConvert(string: bechString) - let hrp = byteConvert(string: bechString.substring(to: bechString.index(bechString.startIndex, offsetBy: pos))) - - let data: [UInt8] = try ((pos + 1)..<bechStringBytes.count).map { i in - guard let d = CHARSET.firstIndex(of: bechStringBytes[i]) else { - throw DecodeBech32Error.invalidCharacter(stringConvert(bytes: [bechStringBytes[i]])) - } - - return UInt8(d) - } - - guard verifyChecksum(hrp: hrp, data: data) else { throw DecodeBech32Error.invalidChecksum } - - return (hrp: hrp, data: Array(data[Int()..<data.count - 6])) -} - -func convertbits(data: [UInt8], fromBits: Int, toBits: Int, pad: Bool) throws -> [UInt8] { - var acc = Int() - var bits = UInt8() - let maxv = (1 << toBits) - 1 - - let converted: [[Int]] = try data.map { value in - if (value < 0 || (UInt8(Int(value) >> fromBits)) != 0) { - throw DecodeBech32Error.invalidCharacter(stringConvert(bytes: [value])) - } - - acc = (acc << fromBits) | Int(value) - bits += UInt8(fromBits) - - var values = [Int]() - - while bits >= UInt8(toBits) { - bits -= UInt8(toBits) - values += [(acc >> Int(bits)) & maxv] - } - - return values - } - - let padding = pad && bits > UInt8() ? [acc << (toBits - Int(bits)) & maxv] : [] - - if !pad && (bits >= UInt8(fromBits) || acc << (toBits - Int(bits)) & maxv > Int()) { - throw DecodeBech32Error.invalidBits - } - - return ((converted.flatMap { $0 }) + padding).map { UInt8($0) } -} - -func encode(hrp: [UInt8], version: UInt8, program: [UInt8]) throws -> String { - let address = try encode(hrp: hrp, data: [version] + convertbits(data: program, fromBits: 8, toBits: 5, pad: true)) - - // Confirm encoded address parses without error - let _ = try decodeAddress(hrp: hrp, address: address) - - return address -} - -func decodeAddress(hrp: [UInt8], address: String) throws -> (version: UInt8, program: [UInt8]) { - let decoded = try decodeBech32(bechString: address) - - // Confirm decoded address matches expected type - guard stringConvert(bytes: decoded.hrp) == stringConvert(bytes: hrp) else { throw DecodeBech32Error.inconsistentHrp } - - // Confirm version byte is present - guard let versionByte = decoded.data.first else { throw DecodeBech32Error.missingVersion } - - // Confirm version byte is within the acceptable range - guard !decoded.data.isEmpty && versionByte <= 16 else { throw DecodeBech32Error.invalidVersion } - - let program = try convertbits(data: Array(decoded.data[1..<decoded.data.count]), fromBits: 5, toBits: 8, pad: false) - - // Confirm program is a valid length - guard program.count > 1 && program.count < 41 else { throw DecodeBech32Error.invalidAddress } - - if versionByte == UInt8() { - // Confirm program is a known byte length (20 for pkhash, 32 for scripthash) - guard program.count == 20 || program.count == 32 else { throw DecodeBech32Error.invalidPayToHashLength } - } - - return (version: versionByte, program: program) -} - -func segwitScriptPubKey(version: UInt8, program: [UInt8]) -> [UInt8] { - return [version > UInt8() ? version + 0x50 : UInt8(), UInt8(program.count)] + program -} - - -/* -class TestBech32: XCTestCase { - func testInvalidAddresses() { - let INVALID_ADDRESS = [ - "tc1qw508d6qejxtdg4y5r3zarvary0c5xw7kg3g4ty", - "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t5", - "BC13W508D6QEJXTDG4Y5R3ZARVARY0C5XW7KN40WF2", - "bc1rw5uspcuh", - "bc10w508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7kw5rljs90", - "BC1QR508D6QEJXTDG4Y5R3ZARVARYV98GJ9P", - "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sL5k7", - "tb1pw508d6qejxtdg4y5r3zarqfsj6c3", - "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3pjxtptv", - ] - - INVALID_ADDRESS.forEach { test in - ["bc", "tb"].forEach { type in - do { - let _ = try decodeAddress(hrp: byteConvert(string: type), address: test) - - XCTFail("Expected invalid address: \(test)") - } catch { - return - } - } - } - } - - func testChecksums() { - let VALID_CHECKSUM: [String] = [ - "A12UEL5L", - "an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs", - "abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", - "11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", - "split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w" - ] - - do { - try VALID_CHECKSUM.forEach { test in - let _ = try decodeBech32(bechString: test) - } - } catch { - XCTFail(error.localizedDescription) - } - } - - func testValidAddresses() { - let VALID_BC_ADDRESSES: [String: (decoded: [UInt8], type: String)] = [ - "BC1QW508D6QEJXTDG4Y5R3ZARVARY0C5XW7KV8F3T4": ( - decoded: [ - 0x00, 0x14, 0x75, 0x1e, 0x76, 0xe8, 0x19, 0x91, 0x96, 0xd4, 0x54, - 0x94, 0x1c, 0x45, 0xd1, 0xb3, 0xa3, 0x23, 0xf1, 0x43, 0x3b, 0xd6 - ], - type: "bc" - ), - - "tb1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3q0sl5k7": ( - decoded: [ - 0x00, 0x20, 0x18, 0x63, 0x14, 0x3c, 0x14, 0xc5, 0x16, 0x68, 0x04, - 0xbd, 0x19, 0x20, 0x33, 0x56, 0xda, 0x13, 0x6c, 0x98, 0x56, 0x78, - 0xcd, 0x4d, 0x27, 0xa1, 0xb8, 0xc6, 0x32, 0x96, 0x04, 0x90, 0x32, - 0x62 - ], - type: "tb" - ), - - "bc1pw508d6qejxtdg4y5r3zarvary0c5xw7kw508d6qejxtdg4y5r3zarvary0c5xw7k7grplx": ( - decoded: [ - 0x51, 0x28, 0x75, 0x1e, 0x76, 0xe8, 0x19, 0x91, 0x96, 0xd4, 0x54, - 0x94, 0x1c, 0x45, 0xd1, 0xb3, 0xa3, 0x23, 0xf1, 0x43, 0x3b, 0xd6, - 0x75, 0x1e, 0x76, 0xe8, 0x19, 0x91, 0x96, 0xd4, 0x54, 0x94, 0x1c, - 0x45, 0xd1, 0xb3, 0xa3, 0x23, 0xf1, 0x43, 0x3b, 0xd6 - ], - type: "bc" - ), - - "BC1SW50QA3JX3S": (decoded: [0x60, 0x02, 0x75, 0x1e], type: "bc"), - - "bc1zw508d6qejxtdg4y5r3zarvaryvg6kdaj": ( - decoded: [ - 0x52, 0x10, 0x75, 0x1e, 0x76, 0xe8, 0x19, 0x91, 0x96, 0xd4, 0x54, - 0x94, 0x1c, 0x45, 0xd1, 0xb3, 0xa3, 0x23 - ], - type: "bc" - ), - - "tb1qqqqqp399et2xygdj5xreqhjjvcmzhxw4aywxecjdzew6hylgvsesrxh6hy": ( - decoded: [ - 0x00, 0x20, 0x00, 0x00, 0x00, 0xc4, 0xa5, 0xca, 0xd4, 0x62, 0x21, - 0xb2, 0xa1, 0x87, 0x90, 0x5e, 0x52, 0x66, 0x36, 0x2b, 0x99, 0xd5, - 0xe9, 0x1c, 0x6c, 0xe2, 0x4d, 0x16, 0x5d, 0xab, 0x93, 0xe8, 0x64, - 0x33 - ], - type: "tb" - ) - ] - - do { - try VALID_BC_ADDRESSES.forEach { address, result in - let scriptPubKey = result.decoded - let hrp = byteConvert(string: result.type) - - let ret = try decodeAddress(hrp: hrp, address: address) - - let output = segwitScriptPubKey(version: ret.version, program: ret.program) - - XCTAssertEqual(output, scriptPubKey) - - let recreated = try encode(hrp: hrp, version: ret.version, program: ret.program).lowercased() - - XCTAssertEqual(recreated, address.lowercased()) - } - } catch { - XCTFail(error.localizedDescription) - } - } -} - - */ diff --git a/lightninglink/ContentView.swift b/lightninglink/ContentView.swift @@ -24,7 +24,7 @@ enum ActiveSheet: Identifiable { } case qr - case pay(Invoice, String) + case pay(Int64, String) } struct Funds { @@ -55,18 +55,20 @@ struct ContentView: View { @State private var last_pay: Pay? @State private var funds: Funds - private var ln: LNSocket - private var token: String + private var lnlink: LNLink - init(info: GetInfo, ln: LNSocket, token: String, funds: ListFunds) { + init(info: GetInfo, lnlink: LNLink, funds: ListFunds) { self.info = info - self.ln = ln - self.token = token + self.lnlink = lnlink self.funds = Funds.from_listfunds(fs: funds) } func refresh_funds() { - let funds = fetch_funds(ln: self.ln, token: self.token) + let ln = LNSocket() + guard ln.connect_and_init(node_id: self.lnlink.node_id, host: self.lnlink.host) else { + return + } + let funds = fetch_funds(ln: ln, token: lnlink.token) self.funds = Funds.from_listfunds(fs: funds) } @@ -119,15 +121,15 @@ struct ContentView: View { let index = code.index(code.startIndex, offsetBy: 10) invstr = String(code[index...]) } - let m_parsed = parseInvoice(invstr) + let m_parsed = parseInvoiceAmount(invstr) guard let parsed = m_parsed else { return } self.activeSheet = .pay(parsed, invstr) } - case .pay(let inv, let raw): - PayView(invoice_str: raw, invoice: inv, ln: self.ln, token: self.token) + case .pay(let amt, let raw): + PayView(invoice_str: raw, amount: amt, lnlink: self.lnlink) } } .onReceive(NotificationCenter.default.publisher(for: .sentPayment)) { payment in @@ -137,11 +139,12 @@ struct ContentView: View { } } +/* struct ContentView_Previews: PreviewProvider { static var previews: some View { - let ln = LNSocket() Group { - ContentView(info: .empty, ln: ln, token: "", funds: .empty) + ContentView(info: .empty, lnlink: ln, token: "", funds: .empty) } } } + */ diff --git a/lightninglink/Invoice.swift b/lightninglink/Invoice.swift @@ -8,29 +8,50 @@ import Foundation -public struct Bolt11Invoice { - var msats: Int64 -} +public func parseInvoiceAmount(_ inv: String) -> Int64? +{ + if !inv.starts(with: "lnbc") { + return nil + } -public struct Bolt12Invoice { - var msats: Int64 -} + var ind = 4 + var num: String = "" + var scale: Character = Character("p") -public enum Invoice { - case bolt11(Bolt11Invoice) - case bolt12(Bolt12Invoice) + // number part + while true { + let c = inv[inv.index(inv.startIndex, offsetBy: ind)] + ind += 1 - func amount() -> Int64 { - return invoiceAmount(self) + if c >= "0" && c <= "9" { + continue + } else { + let start_ind = inv.index(inv.startIndex, offsetBy: 4) + let end_ind = inv.index(inv.startIndex, offsetBy: ind - 1) + scale = inv[inv.index(inv.startIndex, offsetBy: ind - 1)] + num = String(inv[start_ind..<end_ind]) + break + } + } + + if !(scale == "m" || scale == "u" || scale == "n" || scale == "p") { + return nil + } + + guard let n = Int(num) else { + return nil } - static var empty: Invoice { - let b11 = Bolt11Invoice(msats: 0) - let inv: Invoice = .bolt11(b11) - return inv + switch scale { + case "m": return Int64(n * 100000000); + case "u": return Int64(n * 100000); + case "n": return Int64(n * 100); + case "p": return Int64(n * 1); + default: return nil } } +/* public func parseInvoice(_ str: String) -> Invoice? { // decode bech32 @@ -61,3 +82,4 @@ public func invoiceAmount(_ inv: Invoice) -> Int64 } } + */ diff --git a/lightninglink/LNLink.swift b/lightninglink/LNLink.swift @@ -0,0 +1,14 @@ +// +// LNLink.swift +// lightninglink +// +// Created by William Casarin on 2022-02-09. +// + +import Foundation + +public struct LNLink { + var token: String + var host: String + var node_id: String +} diff --git a/lightninglink/PayView.swift b/lightninglink/PayView.swift @@ -7,21 +7,27 @@ import SwiftUI +func render_amount(_ amount: Int64) -> String { + if amount < 1000 { + return "\(amount) msats" + } + + return "\(amount / 1000) sats" +} + struct PayView: View { var invoice_str: String - var invoice: Invoice - var ln: LNSocket - var token: String + var amount: Int64 + var lnlink: LNLink @State var pay_result: Pay? @State var error: String? @Environment(\.presentationMode) var presentationMode - init(invoice_str: String, invoice: Invoice, ln: LNSocket, token: String) { + init(invoice_str: String, amount: Int64, lnlink: LNLink) { self.invoice_str = invoice_str - self.invoice = invoice - self.ln = ln - self.token = token + self.amount = amount + self.lnlink = lnlink } var successView: some View { @@ -47,7 +53,7 @@ struct PayView: View { .font(.largeTitle) Spacer() Text("Pay") - Text("\(self.invoice.amount()) msats?") + Text("\(render_amount(self.amount))?") .font(.title) Text("\(self.error ?? "")") Spacer() @@ -60,9 +66,17 @@ struct PayView: View { Spacer() Button("Confirm") { + // do a fresh connection for each payment + let ln = LNSocket() + + guard ln.connect_and_init(node_id: self.lnlink.node_id, host: self.lnlink.host) else { + self.error = "Failed to connect, please try again!" + return + } + let res = rpc_pay( - ln: self.ln, - token: self.token, + ln: ln, + token: lnlink.token, bolt11: self.invoice_str, amount_msat: nil) diff --git a/lightninglink/RPC.swift b/lightninglink/RPC.swift @@ -248,7 +248,7 @@ func commando_read_all(ln: LNSocket, timeout_ms: Int32 = 2000) -> RequestRes<Dat return .success(all_data) } -public let default_timeout: Int32 = 3000 +public let default_timeout: Int32 = 8000 public func rpc_getinfo(ln: LNSocket, token: String) -> RequestRes<GetInfo> { @@ -258,6 +258,7 @@ public func rpc_getinfo(ln: LNSocket, token: String) -> RequestRes<GetInfo> public func rpc_pay(ln: LNSocket, token: String, bolt11: String, amount_msat: Int64?) -> RequestRes<Pay> { + var params: Array<String> = [ bolt11 ] if amount_msat != nil { params.append("\(amount_msat!)") diff --git a/lightninglink/lightninglinkApp.swift b/lightninglink/lightninglinkApp.swift @@ -11,14 +11,15 @@ import SwiftUI struct lightninglinkApp: App { var info: GetInfo = .empty var funds: ListFunds = .empty - var ln: LNSocket - var token: String + var lnlink: LNLink init() { self.ln = LNSocket() self.token = "" let node_id = "03f3c108ccd536b8526841f0a5c58212bb9e6584a1eb493080e7c1cc34f82dad71" let host = "24.84.152.187" + let lnlink = LNLink(token: token, host: host, node_id: node_id) + self.lnlink = lnlink guard ln.connect_and_init(node_id: node_id, host: host) else { return @@ -30,7 +31,7 @@ struct lightninglinkApp: App { var body: some Scene { WindowGroup { - ContentView(info: self.info, ln: self.ln, token: self.token, funds: self.funds) + ContentView(info: self.info, lnlink: self.lnlink, funds: self.funds) } } }