(_ type: VibrationFeedbackType, trigger: T) -> some View where T : Equatable {
31 | ModifiedContent(
32 | content: self,
33 | modifier: VibrationFeedback(type: type, trigger: trigger)
34 | )
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/configuration/advanced/wallet/UtxoWrapper.swift:
--------------------------------------------------------------------------------
1 | import Swift
2 | import PhoenixShared
3 |
4 | struct UtxoWrapper: Identifiable {
5 | let utxo: Lightning_kmpWalletState.Utxo
6 | let confirmationCount: Int64
7 |
8 | var amount: Bitcoin_kmpSatoshi {
9 | return utxo.amount
10 | }
11 |
12 | var txid: Bitcoin_kmpTxId {
13 | return utxo.previousTx.txid
14 | }
15 |
16 | var id: String {
17 | return utxo.id
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/configuration/fees/liquidity management/LiquidityFeeInfo.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import PhoenixShared
3 |
4 | struct LiquidityFeeParams {
5 | let amount: Bitcoin_kmpSatoshi
6 | let feerate: Lightning_kmpFeeratePerKw
7 | let fundingRate: Lightning_kmpLiquidityAdsFundingRate
8 | }
9 |
10 | struct LiquidityFeeEstimate {
11 | let minerFee: Bitcoin_kmpSatoshi
12 | let serviceFee: Bitcoin_kmpSatoshi
13 | }
14 |
15 | struct LiquidityFeeInfo {
16 | let params: LiquidityFeeParams
17 | let estimate: LiquidityFeeEstimate
18 | }
19 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/contacts/AddToContactsInfo.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import PhoenixShared
3 |
4 | struct AddToContactsInfo: Hashable {
5 | let offer: Lightning_kmpOfferTypesOffer?
6 | let address: String?
7 | }
8 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/content/RootView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct RootView: View {
4 |
5 | @ViewBuilder
6 | var body: some View {
7 |
8 | GeometryReader { geometry in
9 | ContentView()
10 | .frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
11 | .modifier(GlobalEnvironment.mainInstance())
12 | .onAppear {
13 | GlobalEnvironment.deviceInfo._windowSize = geometry.size
14 | GlobalEnvironment.deviceInfo.windowSafeArea = geometry.safeAreaInsets
15 | }
16 | .onChange(of: geometry.size) { newSize in
17 | GlobalEnvironment.deviceInfo._windowSize = newSize
18 | }
19 | .onChange(of: geometry.safeAreaInsets) { newValue in
20 | GlobalEnvironment.deviceInfo.windowSafeArea = newValue
21 | }
22 | } //
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/html/AboutHTML.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | class AboutHTML: AnyHTML {
4 |
5 | init() {
6 | super.init(htmlFilename: "about", cssFilename: "common")
7 | }
8 |
9 | // Nothing else to override here.
10 | // The defaults work fine for us.
11 | }
12 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/html/Base.lproj/liquidity.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Phoenix allows you to receive payments on bitcoin's blockchain
11 | layer (L1) and bitcoin's lightning layer (L2).
12 |
13 |
14 | -
15 | The blockchain layer (L1) is slower,
16 | and generally much more expensive (requires miner fees)
17 |
18 | -
19 | The lightning layer (L2) is much faster,
20 | and generally much cheaper (especially for smaller payments)
21 |
22 |
23 |
24 | When you receive a payment on L1, Phoenix will automatically
25 | move the funds to L2 IF the fees adhere to your
26 | configured fee policy.
27 |
28 |
29 | Payments you receive on L2 can be received instantly and for
30 | zero fees. However, occasionally an L1 operation is also
31 | required in order to manage the L2 payment channel. This can
32 | be done automatically IF the fees adhere to your
33 | configured fee policy.
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/html/LiquidityHTML.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | class LiquidityHTML: AnyHTML {
4 |
5 | init() {
6 | super.init(htmlFilename: "liquidity", cssFilename: "common")
7 | }
8 |
9 | // Nothing else to override here.
10 | // The defaults work fine for us.
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/html/common.css:
--------------------------------------------------------------------------------
1 | /* Credits:
2 | * https://useyourloaf.com/blog/using-dynamic-type-with-web-views/
3 | * https://useyourloaf.com/blog/supporting-dark-mode-in-wkwebview
4 | **/
5 |
6 | :root {
7 | background: [[background_color]];
8 | color: [[foreground_color]];
9 | --link-color: [[link_color]];
10 | }
11 |
12 | body {
13 | font: -apple-system-body;
14 | }
15 |
16 | a {
17 | color: var(--link-color);
18 | }
19 |
20 | p {
21 | margin-top: 0pt;
22 | margin-bottom: 0pt;
23 | padding-top: 5pt;
24 | padding-bottom: 10pt;
25 | }
26 |
27 | p.noBottomPadding {
28 | padding-bottom: 0pt;
29 | }
30 |
31 | ul {
32 | margin-top: 0;
33 | padding-top: 0;
34 | }
35 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/html/de.lproj/liquidity.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Phoenix allows you to receive payments on bitcoin's blockchain
11 | layer (L1) and bitcoin's lightning layer (L2).
12 |
13 |
14 | -
15 | The blockchain layer (L1) is slower,
16 | and generally much more expensive (requires miner fees)
17 |
18 | -
19 | The lightning layer (L2) is much faster,
20 | and generally much cheaper (especially for smaller payments)
21 |
22 |
23 |
24 | When you receive a payment on L1, Phoenix will automatically
25 | move the funds to L2 IF the fees adhere to your
26 | configured fee policy.
27 |
28 |
29 | Payments you receive on L2 can be received instantly and for
30 | zero fees. However, occasionally an L1 operation is also
31 | required in order to manage the L2 payment channel. This can
32 | be done automatically IF the fees adhere to your
33 | configured fee policy.
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/inspect/Details/DetailsRowWrapper.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct DetailsRowWrapper: View {
4 |
5 | let identifier: String
6 | let keyColumnWidth: CGFloat
7 | let keyColumn: KeyColumn
8 | let valueColumn: ValueColumn
9 |
10 | init(
11 | identifier: String,
12 | keyColumnWidth: CGFloat,
13 | @ViewBuilder keyColumn keyColumnBuilder: () -> KeyColumn,
14 | @ViewBuilder valueColumn valueColumnBuilder: () -> ValueColumn
15 | ) {
16 | self.identifier = identifier
17 | self.keyColumnWidth = keyColumnWidth
18 | self.keyColumn = keyColumnBuilder()
19 | self.valueColumn = valueColumnBuilder()
20 | }
21 |
22 | @ViewBuilder
23 | var body: some View {
24 |
25 | InfoGridRow(
26 | identifier: identifier,
27 | vAlignment: .firstTextBaseline,
28 | hSpacing: 8,
29 | keyColumnWidth: keyColumnWidth,
30 | keyColumnAlignment: .trailing
31 | ) {
32 | keyColumn
33 | } valueColumn: {
34 | valueColumn.font(.callout)
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/inspect/Details/DisplayAmounts.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct DisplayAmounts {
4 | let bitcoin: FormattedAmount
5 | let fiatCurrent: FormattedAmount?
6 | let fiatOriginal: FormattedAmount?
7 | }
8 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/inspect/Details/InlineSection.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct InlineSection: View {
4 |
5 | let header: Header
6 | let content: Content
7 |
8 | init(
9 | @ViewBuilder header headerBuilder: () -> Header,
10 | @ViewBuilder content contentBuilder: () -> Content
11 | ) {
12 | header = headerBuilder()
13 | content = contentBuilder()
14 | }
15 |
16 | @ViewBuilder
17 | var body: some View {
18 |
19 | VStack(alignment: HorizontalAlignment.center, spacing: 0) {
20 | header
21 | HStack(alignment: VerticalAlignment.center, spacing: 0) {
22 | VStack(alignment: HorizontalAlignment.leading, spacing: 12) {
23 | content
24 | }
25 | Spacer(minLength: 0)
26 | }
27 | .padding(.vertical, 10)
28 | .padding(.horizontal, 16)
29 | .background {
30 | Color(UIColor.secondarySystemGroupedBackground).cornerRadius(10)
31 | }
32 | .padding(.horizontal, 16)
33 | }
34 | .padding(.vertical, 16)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/main/MainView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import PhoenixShared
3 |
4 | fileprivate let filename = "MainView"
5 | #if DEBUG && true
6 | fileprivate var log = LoggerFactory.shared.logger(filename, .trace)
7 | #else
8 | fileprivate var log = LoggerFactory.shared.logger(filename, .warning)
9 | #endif
10 |
11 | enum HeaderButtonHeight: Preference {}
12 |
13 | struct MainView: View {
14 |
15 | static let idiom = UIDevice.current.userInterfaceIdiom
16 |
17 | @EnvironmentObject var popoverState: PopoverState
18 |
19 | @ViewBuilder
20 | var body: some View {
21 | Group {
22 | if MainView.idiom == .pad {
23 | MainView_Big()
24 | } else {
25 | MainView_Small()
26 | }
27 | }.onAppear {
28 | onAppear()
29 | }
30 | }
31 |
32 | func onAppear() {
33 | log.trace("onAppear()")
34 |
35 | if AppMigration.shared.didUpdate && AppMigration.shared.currentBuildNumber == "85" {
36 | if GroupPrefs.shared.isTorEnabled {
37 | popoverState.display(dismissable: false) {
38 | V85Popover()
39 | }
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/main/NavigationCoordinator.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | class NavigationCoordinator: ObservableObject {
4 | @Published var path = NavigationPath()
5 | }
6 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/notifications/NoticeBox.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | fileprivate let filename = "NoticeBox"
4 | #if DEBUG && true
5 | fileprivate var log = LoggerFactory.shared.logger(filename, .trace)
6 | #else
7 | fileprivate var log = LoggerFactory.shared.logger(filename, .warning)
8 | #endif
9 |
10 | struct NoticeBox: View {
11 |
12 | let backgroundColor: Color?
13 | let content: Content
14 |
15 | init(@ViewBuilder builder: () -> Content) {
16 | self.backgroundColor = nil
17 | self.content = builder()
18 | }
19 |
20 | init(backgroundColor: Color?, @ViewBuilder builder: () -> Content) {
21 | self.backgroundColor = backgroundColor
22 | self.content = builder()
23 | }
24 |
25 | @ViewBuilder
26 | var body: some View {
27 |
28 | HStack(alignment: VerticalAlignment.top, spacing: 0) {
29 | content
30 | Spacer(minLength: 0) // ensure content takes up full width of screen
31 | }
32 | .padding(12)
33 | .background(
34 | RoundedRectangle(cornerRadius: 8)
35 | .fill(backgroundColor ?? Color.clear)
36 | )
37 | .overlay(
38 | RoundedRectangle(cornerRadius: 8)
39 | .stroke(Color.appAccent, lineWidth: 1)
40 | )
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/onboarding/IntroContainer.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | fileprivate let filename = "IntroContainer"
4 | #if DEBUG && true
5 | fileprivate var log = LoggerFactory.shared.logger(filename, .trace)
6 | #else
7 | fileprivate var log = LoggerFactory.shared.logger(filename, .warning)
8 | #endif
9 |
10 | struct IntroContainer: View {
11 |
12 | @State var introFinished = false
13 |
14 | @StateObject var navCoordinator = NavigationCoordinator()
15 |
16 | @ViewBuilder
17 | var body: some View {
18 |
19 | NavigationStack(path: $navCoordinator.path) {
20 | content()
21 | }
22 | .environmentObject(navCoordinator)
23 | }
24 |
25 | @ViewBuilder
26 | func content() -> some View {
27 |
28 | ZStack {
29 |
30 | if introFinished {
31 |
32 | InitializationView()
33 | .zIndex(0)
34 |
35 | } else {
36 |
37 | IntroView(finish: introScreensFinished)
38 | .zIndex(1) // needed for proper animation
39 | .transition(.asymmetric(
40 | insertion : .identity,
41 | removal : .move(edge: .bottom)
42 | ))
43 | }
44 | }
45 | }
46 |
47 | func introScreensFinished() {
48 | log.trace("introScreenFinished()")
49 |
50 | withAnimation {
51 | introFinished = true
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/receive/InboundFeeWarning.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | enum InboundFeeWarning {
4 | case liquidityPolicyDisabled
5 | case overAbsoluteFee(
6 | canRequestLiquidity: Bool,
7 | maxAbsoluteFeeSats: Int64,
8 | swapFeeSats: Int64
9 | )
10 | case overRelativeFee(
11 | canRequestLiquidity: Bool,
12 | maxRelativeFeePercent: Double,
13 | swapFeeSats: Int64
14 | )
15 | case feeExpected(
16 | swapFeeSats: Int64
17 | )
18 | case unknownFeeExpected;
19 |
20 | var type: InboundFeeWarningType {
21 | switch self {
22 | case .liquidityPolicyDisabled:
23 | return .willFail
24 | case .overAbsoluteFee(_, _, _):
25 | return .willFail
26 | case .overRelativeFee(_, _, _):
27 | return .willFail
28 | case .feeExpected(_):
29 | return .feeExpected
30 | case .unknownFeeExpected:
31 | return .feeExpected
32 | }
33 | }
34 | }
35 |
36 | enum InboundFeeWarningType {
37 | case willFail
38 | case feeExpected
39 | }
40 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/receive/SourceInfo.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | enum SourceType {
4 | case text
5 | case image
6 | }
7 |
8 | struct SourceInfo {
9 | let type: SourceType
10 | let isDefault: Bool
11 | let title: String
12 | let subtitle: String?
13 | let callback: () -> Void
14 | }
15 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/send/FetchActivityNotice.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | fileprivate let filename = "FetchActivityNotice"
4 | #if DEBUG && true
5 | fileprivate var log = LoggerFactory.shared.logger(filename, .trace)
6 | #else
7 | fileprivate var log = LoggerFactory.shared.logger(filename, .warning)
8 | #endif
9 |
10 | /// Designed to go into a small sub-view
11 | ///
12 | struct FetchActivityNotice: View {
13 |
14 | let title: String
15 | let onCancel: () -> Void
16 |
17 | @ViewBuilder
18 | var body: some View {
19 |
20 | VStack(alignment: HorizontalAlignment.center, spacing: 8) {
21 | Text(title)
22 |
23 | ZStack {
24 | Divider()
25 | HorizontalActivity(color: .appAccent, diameter: 10, speed: 1.6)
26 | }
27 | .frame(width: 125, height: 10)
28 |
29 | Button {
30 | didTapCancel()
31 | } label: {
32 | Text("Cancel")
33 | }
34 | }
35 | .padding()
36 | .background(Color(UIColor.systemBackground))
37 | .cornerRadius(16)
38 | }
39 |
40 | func didTapCancel() {
41 | log.trace("didTapCancel()")
42 | onCancel()
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/send/MinerFeeInfo.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import PhoenixShared
3 |
4 | struct MinerFeeInfo {
5 | let pubKeyScript: Bitcoin_kmpByteVector? // For targets: .spliceOut
6 | let transaction: Bitcoin_kmpTransaction? // For targets: .expiredSwapIn, .finalWallet
7 | let feerate: Lightning_kmpFeeratePerKw
8 | let minerFee: Bitcoin_kmpSatoshi
9 | }
10 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/send/MsatRange.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import PhoenixShared
3 |
4 | struct MsatRange {
5 | let min: Lightning_kmpMilliSatoshi
6 | let max: Lightning_kmpMilliSatoshi
7 |
8 | init(min: Lightning_kmpMilliSatoshi, max: Lightning_kmpMilliSatoshi) {
9 | self.min = min
10 | self.max = max
11 | }
12 |
13 | init(min: Int64, max: Int64) {
14 | self.min = Lightning_kmpMilliSatoshi(msat: min)
15 | self.max = Lightning_kmpMilliSatoshi(msat: max)
16 | }
17 |
18 | func contains(msat: Lightning_kmpMilliSatoshi) -> Bool {
19 | return contains(msat: msat.msat)
20 | }
21 |
22 | func contains(msat: Int64) -> Bool {
23 | return msat >= min.msat && msat <= max.msat
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/send/PriorityBoxStyle.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct PriorityBoxStyle: GroupBoxStyle {
4 |
5 | let width: CGFloat?
6 | let height: CGFloat?
7 | let disabled: Bool
8 | let selected: Bool
9 | let tapped: () -> Void
10 |
11 | func makeBody(configuration: GroupBoxStyleConfiguration) -> some View {
12 | VStack(alignment: HorizontalAlignment.center, spacing: 4) {
13 | configuration.label
14 | .font(.headline)
15 | configuration.content
16 | }
17 | .frame(width: width?.advanced(by: -16.0), height: height?.advanced(by: -16.0))
18 | .padding(.all, 8)
19 | .background(RoundedRectangle(cornerRadius: 8, style: .continuous)
20 | .fill(Color(UIColor.quaternarySystemFill)))
21 | .overlay(
22 | RoundedRectangle(cornerRadius: 8)
23 | .stroke(selected ? Color.appAccent : Color(UIColor.quaternarySystemFill), lineWidth: 1)
24 | )
25 | .onTapGesture {
26 | tapped()
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/style/CenterTopLineAlignment.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | // alignmentGuide explanation:
4 | //
5 | // The Toggle wants to vertically align its switch in the center of the body:
6 | //
7 | // |body| |switch|
8 | //
9 | // This works good when the body is a single line.
10 | // But with multiple lines it looks like:
11 | //
12 | // |line1|
13 | // |line2| |switch|
14 | // |line3|
15 | //
16 | // This isn't always what we want.
17 | // Instead we can use a custom VerticalAlignment to achieve this:
18 | //
19 | // |line1| |switch|
20 | // |line2|
21 | // |line3|
22 | //
23 | // A good resource on alignment guides can be found here:
24 | // https://swiftui-lab.com/alignment-guides/
25 |
26 | extension VerticalAlignment {
27 | private enum CenterTopLineAlignment: AlignmentID {
28 | static func defaultValue(in d: ViewDimensions) -> CGFloat {
29 | return d[.bottom]
30 | }
31 | }
32 |
33 | static let centerTopLine = VerticalAlignment(CenterTopLineAlignment.self)
34 | }
35 |
36 |
37 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/style/CheckboxToggleStyle.swift:
--------------------------------------------------------------------------------
1 | /// Inspiration for this code came from this wonderful blog:
2 | /// https://swiftuirecipes.com/blog/custom-toggle-checkbox-in-swiftui
3 |
4 | import Foundation
5 | import SwiftUI
6 |
7 | struct CheckboxToggleStyle: ToggleStyle where Img1: View, Img2: View {
8 |
9 | let onImage: Img1
10 | let offImage: Img2
11 | let action: (() -> Void)?
12 |
13 | init(onImage: Img1, offImage: Img2, action: (() -> Void)? = nil) {
14 | self.onImage = onImage
15 | self.offImage = offImage
16 | self.action = action
17 | }
18 |
19 | @Environment(\.isEnabled) var isEnabled
20 |
21 | func makeBody(configuration: Configuration) -> some View {
22 | Button(action: {
23 | configuration.isOn.toggle()
24 | if let action = action {
25 | action()
26 | }
27 | }, label: {
28 | Label {
29 | configuration.label
30 | } icon: {
31 | if configuration.isOn {
32 | onImage
33 | } else {
34 | offImage
35 | }
36 | }
37 | })
38 | .buttonStyle(PlainButtonStyle()) // remove any implicit styling from the button
39 | .disabled(!isEnabled)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/style/InsetGroupBoxStyle.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// Makes a GroupBox look like a Section within a List with style `.insetGrouped`
4 | ///
5 | struct InsetGroupBoxStyle: GroupBoxStyle {
6 |
7 | func makeBody(configuration: GroupBoxStyleConfiguration) -> some View {
8 | VStack(alignment: .leading) {
9 | configuration.label
10 | configuration.content
11 | }
12 | .padding()
13 | .background(Color(.secondarySystemGroupedBackground))
14 | .cornerRadius(10)
15 | .padding(.horizontal)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/style/LabelAlignment.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct LabelAlignment: View {
4 |
5 | let titleContent: () -> TitleContent
6 | let iconContent: () -> IconContent
7 |
8 | init(
9 | title: @escaping () -> TitleContent,
10 | icon: @escaping () -> IconContent
11 | ) {
12 | self.titleContent = title
13 | self.iconContent = icon
14 | }
15 |
16 | @ViewBuilder
17 | var body: some View {
18 |
19 | ZStack(alignment: Alignment.topLeading) {
20 | Label {
21 | Text(verbatim: "Label title").lineLimit(1).hidden()
22 | } icon: {
23 | iconContent()
24 | }
25 | Label {
26 | titleContent()
27 | } icon: {
28 | iconContent().hidden()
29 | }
30 | }
31 |
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/style/ShakeEffect.swift:
--------------------------------------------------------------------------------
1 | // Credit:
2 | // https://www.objc.io/blog/2019/10/01/swiftui-shake-animation/
3 |
4 | import SwiftUI
5 |
6 | struct Shake: GeometryEffect {
7 | var amount: CGFloat = 10
8 | var shakesPerUnit = 3
9 | var animatableData: CGFloat
10 |
11 | func effectValue(size: CGSize) -> ProjectionTransform {
12 | ProjectionTransform(
13 | CGAffineTransform(
14 | translationX: amount * sin(animatableData * .pi * CGFloat(shakesPerUnit)),
15 | y: 0
16 | )
17 | )
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/style/ToggleAlignment.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct ToggleAlignment: View {
4 |
5 | let descriptionContent: () -> DescriptionContent
6 | let toggleContent: () -> ToggleContent
7 |
8 | init(
9 | description: @escaping () -> DescriptionContent,
10 | toggle: @escaping () -> ToggleContent
11 | ) {
12 | self.descriptionContent = description
13 | self.toggleContent = toggle
14 | }
15 |
16 | @ViewBuilder
17 | var body: some View {
18 |
19 | HStack(alignment: VerticalAlignment.centerTopLine) { // <- Custom VerticalAlignment
20 |
21 | ZStack(alignment: Alignment.topLeading) {
22 | Text(verbatim: "Toggle alignment")
23 | .lineLimit(1)
24 | .hidden()
25 | .alignmentGuide(VerticalAlignment.centerTopLine) { (d: ViewDimensions) in
26 | d[VerticalAlignment.center]
27 | }
28 |
29 | descriptionContent()
30 | }
31 |
32 | Spacer()
33 |
34 | toggleContent()
35 | .padding(.trailing, 2)
36 | .alignmentGuide(VerticalAlignment.centerTopLine) { (d: ViewDimensions) in
37 | d[VerticalAlignment.center]
38 | }
39 |
40 | } //
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/transactions/PaymentsSection.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import PhoenixShared
3 |
4 |
5 | struct PaymentsSection: Identifiable {
6 | let year: Int
7 | let month: Int
8 | let name: String
9 | var payments: [WalletPaymentInfo] = []
10 |
11 | var id: String {
12 | "\(year)-\(month)"
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/updates/V85Popover.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | fileprivate let filename = "V85Popover"
4 | #if DEBUG && true
5 | fileprivate var log = LoggerFactory.shared.logger(filename, .trace)
6 | #else
7 | fileprivate var log = LoggerFactory.shared.logger(filename, .warning)
8 | #endif
9 |
10 | struct V85Popover: View {
11 |
12 | @EnvironmentObject var popoverState: PopoverState
13 |
14 | @ViewBuilder
15 | var body: some View {
16 |
17 | VStack(alignment: HorizontalAlignment.leading, spacing: 15) {
18 |
19 | Text("Version 2.5.0: Tor changes")
20 | .font(.headline)
21 |
22 | Text(
23 | """
24 | Enabling Tor now requires installing a third-party Tor Proxy VPN app such as Orbot.
25 |
26 | This makes background payments much more reliable with Tor.
27 |
28 | Also Phoenix now always uses onion addresses for Lightning and Electrum \
29 | connections when Tor is enabled.
30 | """
31 | )
32 |
33 | HStack(alignment: VerticalAlignment.center, spacing: 0) {
34 | Spacer()
35 | Button {
36 | popoverState.close()
37 | } label: {
38 | Text("OK").font(.title3)
39 | }
40 | }
41 | }
42 | .padding(.all)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/widgets/InfoPopoverWindow.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct InfoPopoverWindow: View {
4 |
5 | let content: () -> Content
6 |
7 | @ViewBuilder
8 | var body: some View {
9 |
10 | content()
11 | .padding(.all, 16)
12 | .background(Color(.systemBackground))
13 | .cornerRadius(12)
14 | .shadow(
15 | color: Color(.label.withAlphaComponent(0.25)),
16 | radius: 40,
17 | x: 0,
18 | y: 4
19 | )
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/views/widgets/View+If.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | extension View {
4 | /// Applies the given transform if the given condition evaluates to `true`.
5 | /// - Parameters:
6 | /// - condition: The condition to evaluate.
7 | /// - transform: The transform to apply to the source `View`.
8 | /// - Returns: Either the original `View` or the modified `View` if the condition is `true`.
9 | @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View {
10 | if condition {
11 | transform(self)
12 | } else {
13 | self
14 | }
15 | }
16 |
17 | /// Can be used when extra logic is needed. For example:
18 | /// ```
19 | /// someView.modify { view in
20 | /// if #available(iOS 16.0, *) {
21 | /// view.foo()
22 | /// } else {
23 | /// view.bar()
24 | /// }
25 | /// }
26 | /// ```
27 | func modify(@ViewBuilder _ modifier: (Self) -> T) -> some View {
28 | return modifier(self)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/xpc/XPC+Background.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// This file is **ONLY** for the Notify-Service-Extension (background process)
4 | ///
5 | extension XPC {
6 | public static let shared = XPC(actor: .notifySrvExt)
7 | }
8 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-ios/xpc/XPC+Foreground.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// This file is **ONLY** for the main Phoenix app
4 | ///
5 | extension XPC {
6 | public static let shared = XPC(actor: .mainApp)
7 | }
8 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-iosTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-iosUITests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-notifySrvExt/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSExtension
6 |
7 | NSExtensionPointIdentifier
8 | com.apple.usernotifications.service
9 | NSExtensionPrincipalClass
10 | $(PRODUCT_MODULE_NAME).NotificationService
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/phoenix-ios/phoenix-notifySrvExt/phoenix-notifySrvExt.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.application-groups
6 |
7 | group.co.acinq.phoenix
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/phoenix-shared/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/phoenix-shared/src/androidMain/kotlin/fr/acinq/phoenix/data/androidElectrumRegtestConf.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.data
2 |
3 | import fr.acinq.lightning.io.TcpSocket
4 | import fr.acinq.lightning.utils.ServerAddress
5 |
6 | actual fun platformElectrumRegtestConf(): ServerAddress = ServerAddress(host = "10.0.2.2", port = 51002, tls = TcpSocket.TLS.DISABLED)
7 |
--------------------------------------------------------------------------------
/phoenix-shared/src/androidMain/kotlin/fr/acinq/phoenix/db/SqlPaymentHooks.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.db
2 |
3 | import fr.acinq.lightning.utils.UUID
4 | import fr.acinq.phoenix.db.payments.CloudKitInterface
5 | import fr.acinq.phoenix.db.sqldelight.AppDatabase
6 | import fr.acinq.phoenix.db.sqldelight.PaymentsDatabase
7 |
8 | actual fun didSaveWalletPayment(id: UUID, database: PaymentsDatabase) {}
9 | actual fun didDeleteWalletPayment(id: UUID, database: PaymentsDatabase) {}
10 | actual fun didUpdateWalletPaymentMetadata(id: UUID, database: PaymentsDatabase) {}
11 |
12 | actual fun didSaveContact(contactId: UUID, database: PaymentsDatabase) {}
13 | actual fun didDeleteContact(contactId: UUID, database: PaymentsDatabase) {}
14 |
15 | actual fun makeCloudKitDb(appDb: SqliteAppDb, paymentsDb: SqlitePaymentsDb): CloudKitInterface? {
16 | return null
17 | }
--------------------------------------------------------------------------------
/phoenix-shared/src/androidMain/kotlin/fr/acinq/phoenix/utils/platformAndroid.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.utils
2 |
3 | import android.content.Context
4 |
5 |
6 | actual class PlatformContext(val applicationContext: Context)
7 |
8 | actual fun getApplicationFilesDirectoryPath(ctx: PlatformContext): String =
9 | ctx.applicationContext.filesDir.absolutePath
10 |
11 | actual fun getDatabaseFilesDirectoryPath(ctx: PlatformContext): String? = null
12 |
13 | actual fun getApplicationCacheDirectoryPath(ctx: PlatformContext): String =
14 | ctx.applicationContext.cacheDir.absolutePath
15 |
16 | actual fun getTemporaryDirectoryPath(ctx: PlatformContext): String =
17 | ctx.applicationContext.cacheDir.absolutePath
18 |
--------------------------------------------------------------------------------
/phoenix-shared/src/androidUnitTest/kotlin/fr/acinq/phoenix/db/SqliteChannelsDatabaseTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 ACINQ SAS
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package fr.acinq.phoenix.db
18 |
19 | import app.cash.sqldelight.db.SqlDriver
20 | import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
21 | import fr.acinq.phoenix.db.sqldelight.ChannelsDatabase
22 |
23 | actual fun testChannelsDriver(): SqlDriver {
24 | val driver: SqlDriver = JdbcSqliteDriver(JdbcSqliteDriver.IN_MEMORY)
25 | ChannelsDatabase.Schema.create(driver)
26 | return driver
27 | }
28 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/MVI.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.controllers
2 |
3 | object MVI {
4 |
5 | abstract class Data {
6 | override fun toString(): String = this::class.simpleName ?: super.toString()
7 | }
8 |
9 | abstract class Model : Data()
10 |
11 | abstract class Intent : Data()
12 |
13 | abstract class Controller(val firstModel: M) {
14 |
15 | abstract fun subscribe(onModel: (M) -> Unit): () -> Unit
16 |
17 | abstract fun intent(intent: I)
18 |
19 | abstract fun stop()
20 |
21 | open class Mock(val model: M) : Controller(model) {
22 | override fun subscribe(onModel: (M) -> Unit): () -> Unit {
23 | onModel(model)
24 | return ({})
25 | }
26 | override fun intent(intent: I) {}
27 | override fun stop() {}
28 | }
29 |
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/config/Configuration.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.controllers.config
2 |
3 | import fr.acinq.phoenix.controllers.MVI
4 |
5 | object Configuration {
6 |
7 | sealed class Model : MVI.Model() {
8 | object SimpleMode : Model()
9 | object FullMode : Model()
10 | }
11 |
12 | sealed class Intent : MVI.Intent()
13 | }
14 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/config/ElectrumConfiguration.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.controllers.config
2 |
3 | import fr.acinq.lightning.utils.Connection
4 | import fr.acinq.lightning.utils.ServerAddress
5 | import fr.acinq.phoenix.controllers.MVI
6 | import fr.acinq.phoenix.data.ElectrumConfig
7 |
8 | object ElectrumConfiguration {
9 |
10 | data class Model(
11 | val configuration: ElectrumConfig? = null,
12 | val currentServer: ServerAddress? = null,
13 | val connection: Connection = Connection.CLOSED(reason = null),
14 | val feeRate: Long = 0,
15 | val blockHeight: Int = 0,
16 | val tipTimestamp: Long = 0,
17 | val walletIsInitialized: Boolean = false,
18 | val error: Error? = null
19 | ) : MVI.Model() {
20 | fun isCustom() = configuration != null && configuration is ElectrumConfig.Custom
21 | }
22 |
23 | sealed class Intent : MVI.Intent() {
24 | data class UpdateElectrumServer(val config: ElectrumConfig.Custom?) : Intent()
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/init/Initialization.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.controllers.init
2 |
3 | import fr.acinq.phoenix.controllers.MVI
4 | import fr.acinq.phoenix.utils.MnemonicLanguage
5 |
6 |
7 | object Initialization {
8 |
9 | sealed class Model : MVI.Model() {
10 | object Ready : Model()
11 | data class GeneratedWallet(
12 | val mnemonics: List,
13 | val language: MnemonicLanguage,
14 | val seed: ByteArray
15 | ) : Model() {
16 | override fun toString() = "GeneratedWallet"
17 | }
18 | }
19 |
20 | sealed class Intent : MVI.Intent() {
21 | data class GenerateWallet(
22 | val entropy: ByteArray,
23 | val language: MnemonicLanguage
24 | ) : Intent() {
25 | override fun toString() = "GenerateWallet"
26 | }
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/main/Content.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.controllers.main
2 |
3 | import fr.acinq.phoenix.controllers.MVI
4 |
5 | object Content {
6 |
7 | sealed class Model : MVI.Model() {
8 | object Waiting : Model()
9 | object IsInitialized : Model()
10 | object NeedInitialization : Model()
11 | }
12 |
13 | sealed class Intent : MVI.Intent()
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/main/Home.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.controllers.main
2 |
3 | import fr.acinq.lightning.MilliSatoshi
4 | import fr.acinq.phoenix.controllers.MVI
5 |
6 | object Home {
7 |
8 | data class Model(
9 | val balance: MilliSatoshi?,
10 | ) : MVI.Model()
11 |
12 | val emptyModel = Model(
13 | balance = null,
14 | )
15 |
16 | sealed class Intent : MVI.Intent()
17 | }
18 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/main/HomeController.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.controllers.main
2 |
3 | import fr.acinq.lightning.logging.LoggerFactory
4 | import fr.acinq.phoenix.PhoenixBusiness
5 | import fr.acinq.phoenix.controllers.AppController
6 | import fr.acinq.phoenix.managers.BalanceManager
7 | import kotlinx.coroutines.launch
8 |
9 |
10 | class AppHomeController(
11 | loggerFactory: LoggerFactory,
12 | private val balanceManager: BalanceManager
13 | ) : AppController(
14 | loggerFactory = loggerFactory,
15 | firstModel = Home.emptyModel
16 | ) {
17 | constructor(business: PhoenixBusiness): this(
18 | loggerFactory = business.loggerFactory,
19 | balanceManager = business.balanceManager
20 | )
21 |
22 | init {
23 | launch {
24 | balanceManager.balance.collect {
25 | model { copy(balance = it) }
26 | }
27 | }
28 | }
29 |
30 | override fun process(intent: Home.Intent) {}
31 | }
32 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/controllers/payments/Receive.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.controllers.payments
2 |
3 | import fr.acinq.lightning.MilliSatoshi
4 | import fr.acinq.phoenix.controllers.MVI
5 |
6 | object Receive {
7 |
8 | sealed class Model : MVI.Model() {
9 | object Awaiting : Model()
10 | object Generating: Model()
11 | data class Generated(val request: String, val paymentHash: String, val amount: MilliSatoshi?, val desc: String?): Model()
12 | }
13 |
14 | sealed class Intent : MVI.Intent() {
15 | data class Ask(
16 | val amount: MilliSatoshi?,
17 | val desc: String?,
18 | val expirySeconds: Long = 3600 * 24 * 7 // 7 days
19 | ) : Intent()
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/Wallet.kt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ACINQ/phoenix/b26ab3a809122467a63c6c17efd5f3d5d1bb3b4f/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/Wallet.kt
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/WalletContext.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.data
2 |
3 | /** Contains contextual information for the wallet, fetched from https://acinq.co/phoenix/walletcontext.json. */
4 | data class WalletContext(
5 | val isMempoolFull: Boolean,
6 | val androidLatestVersion: Int,
7 | val androidLatestCriticalVersion: Int,
8 | )
9 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/WalletNotice.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.data
2 |
3 | data class WalletNotice(val message: String, val index: Int)
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/data/WalletPayment.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.data
2 |
3 | import fr.acinq.lightning.db.WalletPayment
4 | import fr.acinq.phoenix.data.lnurl.LnurlPay
5 |
6 | /**
7 | * Represents a payment & its associated metadata.
8 | */
9 | data class WalletPaymentInfo(
10 | val payment: WalletPayment,
11 | val metadata: WalletPaymentMetadata,
12 | val contact: ContactInfo?
13 | ) {
14 | val id get() = payment.id
15 | }
16 |
17 | /**
18 | * Represents information from the `payments_metadata` table.
19 | */
20 | data class WalletPaymentMetadata(
21 | val lnurl: LnurlPayMetadata? = null,
22 | val originalFiat: ExchangeRate.BitcoinPriceRate? = null,
23 | val userDescription: String? = null,
24 | val userNotes: String? = null,
25 | val lightningAddress: String? = null,
26 | val modifiedAt: Long? = null
27 | )
28 |
29 | data class LnurlPayMetadata(
30 | val pay: LnurlPay.Intent,
31 | val description: String,
32 | val successAction: LnurlPay.Invoice.SuccessAction?
33 | ) {
34 | companion object { /* allow companion extensions */ }
35 | }
36 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/DbFactory.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020 ACINQ SAS
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package fr.acinq.phoenix.db
18 |
19 | import app.cash.sqldelight.db.SqlDriver
20 | import fr.acinq.phoenix.utils.PlatformContext
21 |
22 | expect fun createChannelsDbDriver(ctx: PlatformContext, fileName: String): SqlDriver
23 |
24 | expect fun createPaymentsDbDriver(ctx: PlatformContext, fileName: String, onError: (String) -> Unit): SqlDriver
25 |
26 | expect fun createAppDbDriver(ctx: PlatformContext): SqlDriver
27 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/cloud/CloudHelper.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.db.cloud
2 |
3 | import io.ktor.util.*
4 |
5 | // Kotlin wants to encode a ByteArray like this: {
6 | // "fail": [123,34,112,97,121,109,101,110,116,82,101,113,117]
7 | // }
8 | //
9 | // Lol. If we don't use Cbor, then we should at least use Base64.
10 |
11 | fun ByteArray.b64Encode(): String {
12 | return this.encodeBase64() // io.ktor.util
13 | }
14 |
15 | fun String.b64Decode(): ByteArray {
16 | return this.decodeBase64Bytes() // io.ktor.util
17 | }
18 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/migrations/v10/json/OutpointSerializer.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2024 ACINQ SAS
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package fr.acinq.phoenix.db.migrations.v10.json
18 |
19 | import fr.acinq.bitcoin.OutPoint
20 | import fr.acinq.bitcoin.TxHash
21 |
22 | class OutpointSerializer : AbstractStringSerializer(
23 | name = "Outpoint",
24 | fromString = { serialized ->
25 | serialized.split(":").let {
26 | OutPoint(hash = TxHash(it[0]), index = it[1].toLong())
27 | }
28 | },
29 | toString = { outpoint -> "${outpoint.hash}:${outpoint.index}" }
30 | )
31 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/migrations/v10/json/TxIdSerializer.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2024 ACINQ SAS
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package fr.acinq.phoenix.db.migrations.v10.json
18 |
19 | import fr.acinq.bitcoin.TxId
20 |
21 | object TxIdSerializer : AbstractStringSerializer(
22 | name = "TxId",
23 | toString = TxId::toString,
24 | fromString = ::TxId
25 | )
26 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/payments/CloudKitInterface.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.db.payments
2 |
3 | /* Cross-platform placeholder for CloudKitDb. */
4 | interface CloudKitInterface {
5 | }
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/db/serialization/contacts/Serialization.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.db.serialization.contacts
2 |
3 | import fr.acinq.phoenix.data.ContactInfo
4 |
5 | object Serialization {
6 |
7 | fun serialize(contact: ContactInfo): ByteArray {
8 | return fr.acinq.phoenix.db.serialization.contacts.v1.Serialization.serialize(contact)
9 | }
10 |
11 | fun deserialize(bin: ByteArray): Result {
12 | return runCatching {
13 | when (val version = bin.first().toInt()) {
14 | 1 -> fr.acinq.phoenix.db.serialization.contacts.v1.Deserialization.deserialize(bin)
15 | else -> error("unknown version $version")
16 | }
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/managers/NetworkMonitor.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.managers
2 |
3 | import fr.acinq.phoenix.utils.PlatformContext
4 | import kotlinx.coroutines.flow.StateFlow
5 | import fr.acinq.lightning.logging.LoggerFactory
6 |
7 | enum class NetworkState {
8 | Available,
9 | NotAvailable
10 | }
11 |
12 | expect class NetworkMonitor(loggerFactory: LoggerFactory, ctx: PlatformContext) {
13 | val networkState: StateFlow
14 | fun enable()
15 | fun disable()
16 | fun start()
17 | fun stop()
18 | }
19 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/PlatformContext.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 ACINQ SAS
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package fr.acinq.phoenix.utils
18 |
19 |
20 | expect class PlatformContext
21 |
22 | expect fun getApplicationFilesDirectoryPath(ctx: PlatformContext): String
23 | expect fun getDatabaseFilesDirectoryPath(ctx: PlatformContext): String?
24 | expect fun getApplicationCacheDirectoryPath(ctx: PlatformContext): String
25 | expect fun getTemporaryDirectoryPath(ctx: PlatformContext): String
26 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/ChainExtensions.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.utils.extensions
2 |
3 | import fr.acinq.bitcoin.Chain
4 |
5 | /**
6 | * Value used by Phoenix for naming files relative to the [Chain].
7 | * Specifically, testnet3 name must be "testnet", for historical reasons.
8 | */
9 | val Chain.phoenixName: String
10 | get() = when (this) {
11 | Chain.Regtest -> "regtest"
12 | Chain.Signet -> "signet"
13 | Chain.Testnet3 -> "testnet"
14 | Chain.Testnet4 -> "testnet4"
15 | Chain.Mainnet -> "mainnet"
16 | }
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/kotlin/fr.acinq.phoenix/utils/extensions/MiscExtensions.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2024 ACINQ SAS
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package fr.acinq.phoenix.utils.extensions
18 |
19 | import fr.acinq.bitcoin.ByteVector32
20 | import fr.acinq.bitcoin.io.ByteArrayOutput
21 | import fr.acinq.lightning.serialization.OutputExtensions.writeUuid
22 | import fr.acinq.lightning.utils.UUID
23 |
24 | fun ByteVector32.deriveUUID(): UUID = UUID.fromBytes(this.take(16).toByteArray())
25 |
26 | // TODO: use standard Uuid once migrated to kotlin 2
27 | fun UUID.toByteArray() =
28 | ByteArrayOutput().run {
29 | writeUuid(this@toByteArray)
30 | toByteArray()
31 | }
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/sqldelight/appdb/fr/acinq/phoenix/db/sqldelight/ExchangeRates.sq:
--------------------------------------------------------------------------------
1 | import fr.acinq.phoenix.data.ExchangeRate;
2 |
3 | CREATE TABLE IF NOT EXISTS exchange_rates (
4 | fiat TEXT NOT NULL PRIMARY KEY,
5 | price REAL NOT NULL,
6 | type TEXT AS ExchangeRate.Type NOT NULL,
7 | source TEXT NOT NULL,
8 | updated_at INTEGER NOT NULL
9 | );
10 |
11 | insert:
12 | INSERT INTO exchange_rates(
13 | fiat, price, type, source, updated_at
14 | ) VALUES (?, ?, ?, ?, ?);
15 |
16 | update:
17 | UPDATE exchange_rates SET price=?, type=?, source=?, updated_at=? WHERE fiat=?;
18 |
19 | get:
20 | SELECT * FROM exchange_rates WHERE fiat=?;
21 |
22 | list:
23 | SELECT * FROM exchange_rates;
24 |
25 | delete:
26 | DELETE FROM exchange_rates WHERE fiat=?;
27 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/sqldelight/appdb/fr/acinq/phoenix/db/sqldelight/KeyValueStore.sq:
--------------------------------------------------------------------------------
1 | -- Generic key/value store
2 |
3 | CREATE TABLE IF NOT EXISTS key_value_store (
4 | key TEXT NOT NULL PRIMARY KEY,
5 | value BLOB NOT NULL,
6 | updated_at INTEGER NOT NULL
7 | );
8 |
9 | get:
10 | SELECT * FROM key_value_store WHERE key = ?;
11 |
12 | exists:
13 | SELECT COUNT(*) FROM key_value_store WHERE key = ?;
14 |
15 | insert:
16 | INSERT INTO key_value_store(key, value, updated_at) VALUES (?, ?, ?);
17 |
18 | update:
19 | UPDATE key_value_store SET value = ?, updated_at = ? WHERE key = ?;
20 |
21 | delete:
22 | DELETE FROM key_value_store WHERE key = ?;
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/sqldelight/appdb/fr/acinq/phoenix/db/sqldelight/migrations/1.sqm:
--------------------------------------------------------------------------------
1 | -- Migration: v1 -> v2
2 | --
3 | -- Changes:
4 | -- * Added table key_value_store
5 |
6 | CREATE TABLE IF NOT EXISTS key_value_store (
7 | key TEXT NOT NULL PRIMARY KEY,
8 | value BLOB NOT NULL,
9 | updated_at INTEGER NOT NULL
10 | );
11 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/sqldelight/appdb/fr/acinq/phoenix/db/sqldelight/migrations/2.sqm:
--------------------------------------------------------------------------------
1 | -- Migration: v2 -> v3
2 | --
3 | -- Changes:
4 | -- * Deleted table bitcoin_price_rates
5 | -- * Added table exchange_rates
6 |
7 | DROP TABLE IF EXISTS bitcoin_price_rates;
8 |
9 | CREATE TABLE IF NOT EXISTS exchange_rates (
10 | fiat TEXT NOT NULL PRIMARY KEY,
11 | price REAL NOT NULL,
12 | type TEXT NOT NULL,
13 | source TEXT NOT NULL,
14 | updated_at INTEGER NOT NULL
15 | );
16 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/sqldelight/appdb/fr/acinq/phoenix/db/sqldelight/migrations/3.sqm:
--------------------------------------------------------------------------------
1 | -- Migration: v3 -> v4
2 | --
3 | -- Changes:
4 | -- * add notifications table
5 |
6 | CREATE TABLE IF NOT EXISTS notifications (
7 | id TEXT NOT NULL PRIMARY KEY,
8 | type_version TEXT AS NotificationTypeVersion NOT NULL,
9 | data_json BLOB NOT NULL,
10 | created_at INTEGER NOT NULL,
11 | read_at INTEGER DEFAULT NULL
12 | );
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/sqldelight/appdb/fr/acinq/phoenix/db/sqldelight/migrations/4.sqm:
--------------------------------------------------------------------------------
1 | -- Migration: v4 -> v5
2 | --
3 | -- Changes:
4 | -- * add contacts table
5 | -- * add contact_offers table
6 |
7 | CREATE TABLE IF NOT EXISTS contacts (
8 | id TEXT NOT NULL PRIMARY KEY,
9 | name TEXT NOT NULL,
10 | photo_uri TEXT,
11 | created_at INTEGER NOT NULL,
12 | updated_at INTEGER DEFAULT NULL
13 | );
14 |
15 | CREATE TABLE IF NOT EXISTS contact_offers (
16 | offer_id BLOB NOT NULL PRIMARY KEY,
17 | contact_id TEXT NOT NULL,
18 | offer TEXT NOT NULL,
19 | created_at INTEGER NOT NULL,
20 |
21 | FOREIGN KEY(contact_id) REFERENCES contacts(id)
22 | );
23 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/sqldelight/appdb/fr/acinq/phoenix/db/sqldelight/migrations/5.sqm:
--------------------------------------------------------------------------------
1 | -- Migration: v5 -> v6
2 | --
3 | -- Changes:
4 | -- * add use_offer_key flag to the contacts table. By default, a contact is trusted.
5 |
6 | ALTER TABLE contacts
7 | ADD COLUMN use_offer_key INTEGER AS Boolean DEFAULT 1 NOT NULL;
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/sqldelight/appdb/fr/acinq/phoenix/db/sqldelight/migrations/6.sqm:
--------------------------------------------------------------------------------
1 | -- Migration: v6 -> v7
2 | --
3 | -- Changes:
4 | -- * Added table cloudkit_contacts_metadata
5 | -- * Added index on table cloudkit_contacts_metadata
6 | -- * Added table cloudkit_contacts_queue
7 | --
8 | -- See CloudKitContacts.sq for more details.
9 |
10 | CREATE TABLE IF NOT EXISTS cloudkit_contacts_metadata (
11 | id TEXT NOT NULL PRIMARY KEY,
12 | record_creation INTEGER NOT NULL,
13 | record_blob BLOB NOT NULL
14 | );
15 |
16 | CREATE INDEX IF NOT EXISTS record_creation_idx
17 | ON cloudkit_contacts_metadata(record_creation);
18 |
19 | CREATE TABLE IF NOT EXISTS cloudkit_contacts_queue (
20 | rowid INTEGER PRIMARY KEY,
21 | id TEXT NOT NULL,
22 | date_added INTEGER NOT NULL
23 | );
24 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/sqldelight/appdb/fr/acinq/phoenix/db/sqldelight/migrations/7.sqm:
--------------------------------------------------------------------------------
1 | -- Migration: v7 -> v8
2 | --
3 | -- Changes:
4 | -- * Migrating contacts to paymentsDb
5 | --
6 |
7 | DROP INDEX IF EXISTS contact_id_index;
8 |
9 | --- The original `contact_offers` table has a FOREIGN KEY constraint.
10 | --- This is going to cause a problem when we rename the contacts table.
11 | --- And there's no way to drop a constraint in sqlite.
12 | --- So we need to migrate the old table to a new one without the constraint.
13 | CREATE TABLE IF NOT EXISTS contact_offers_old (
14 | offer_id BLOB NOT NULL PRIMARY KEY,
15 | contact_id TEXT NOT NULL,
16 | offer TEXT NOT NULL,
17 | created_at INTEGER NOT NULL
18 | );
19 | INSERT INTO contact_offers_old SELECT * FROM contact_offers;
20 | DROP TABLE IF EXISTS contact_offers;
21 |
22 | ALTER TABLE contacts RENAME TO contacts_old;
23 |
24 | DROP INDEX IF EXISTS record_creation_idx;
25 | ALTER TABLE cloudkit_contacts_metadata RENAME TO cloudkit_contacts_metadata_old;
26 | DROP TABLE IF EXISTS cloudkit_contacts_queue;
27 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/sqldelight/paymentsdb/fr/acinq/phoenix/db/sqldelight/Contacts.sq:
--------------------------------------------------------------------------------
1 | import fr.acinq.lightning.utils.UUID;
2 | import fr.acinq.phoenix.data.ContactInfo;
3 |
4 | CREATE TABLE IF NOT EXISTS contacts (
5 | id BLOB AS UUID NOT NULL PRIMARY KEY,
6 | data BLOB AS ContactInfo NOT NULL,
7 | created_at INTEGER NOT NULL,
8 | updated_at INTEGER DEFAULT NULL
9 | );
10 |
11 | listContacts:
12 | SELECT data
13 | FROM contacts;
14 |
15 | getContact:
16 | SELECT data
17 | FROM contacts
18 | WHERE id = :contactId;
19 |
20 | scanContacts:
21 | SELECT id, created_at FROM contacts;
22 |
23 | existsContact:
24 | SELECT COUNT(*) FROM contacts
25 | WHERE id = ?;
26 |
27 | insertContact:
28 | INSERT INTO contacts(id, data, created_at, updated_at)
29 | VALUES (:id, :data, :createdAt, :updatedAt);
30 |
31 | updateContact:
32 | UPDATE contacts SET data=:data, updated_at=:updatedAt
33 | WHERE id=:contactId;
34 |
35 | deleteContact:
36 | DELETE FROM contacts WHERE id=:contactId;
37 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/sqldelight/paymentsdb/fr/acinq/phoenix/db/sqldelight/OnChainTransactions.sq:
--------------------------------------------------------------------------------
1 | import fr.acinq.bitcoin.ByteVector32;
2 | import fr.acinq.bitcoin.TxId;
3 | import fr.acinq.lightning.utils.UUID;
4 |
5 | CREATE TABLE on_chain_txs (
6 | payment_id BLOB AS UUID NOT NULL PRIMARY KEY,
7 | tx_id BLOB AS TxId NOT NULL,
8 | confirmed_at INTEGER,
9 | locked_at INTEGER
10 | );
11 |
12 | CREATE INDEX on_chain_txs_tx_id ON on_chain_txs(tx_id);
13 |
14 | insert:
15 | INSERT INTO on_chain_txs(
16 | payment_id,
17 | tx_id,
18 | confirmed_at,
19 | locked_at)
20 | VALUES (?, ?, ?, ?);
21 |
22 | setConfirmed:
23 | UPDATE on_chain_txs
24 | SET confirmed_at=?
25 | WHERE tx_id=?;
26 |
27 | setLocked:
28 | UPDATE on_chain_txs
29 | SET locked_at=?
30 | WHERE tx_id=?;
31 |
32 | listUnconfirmed:
33 | SELECT tx_id
34 | FROM on_chain_txs
35 | WHERE confirmed_at IS NULL;
36 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/sqldelight/paymentsdb/migrations/1.sqm:
--------------------------------------------------------------------------------
1 | -- Migration: v1 -> v2
2 | --
3 | -- Changes:
4 | -- * Added table cloudkit_payments_metadata
5 | -- * Added table cloudkit_payments_queue
6 |
7 | CREATE TABLE IF NOT EXISTS cloudkit_payments_metadata (
8 | type INTEGER NOT NULL,
9 | id TEXT NOT NULL,
10 | unpadded_size INTEGER NOT NULL,
11 | record_creation INTEGER NOT NULL,
12 | record_blob BLOB NOT NULL,
13 | PRIMARY KEY (type, id)
14 | );
15 |
16 | CREATE INDEX IF NOT EXISTS record_creation_idx
17 | ON cloudkit_payments_metadata(record_creation);
18 |
19 | CREATE TABLE IF NOT EXISTS cloudkit_payments_queue (
20 | rowid INTEGER PRIMARY KEY,
21 | type INTEGER NOT NULL,
22 | id TEXT NOT NULL,
23 | date_added INTEGER NOT NULL
24 | );
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/sqldelight/paymentsdb/migrations/10.sqm:
--------------------------------------------------------------------------------
1 | -- Migration: v10 -> v11
2 | --
3 | -- This is a code migration, see AfterVersion10.kt
4 |
5 | -- incoming payments
6 | CREATE TABLE payments_incoming (
7 | id BLOB AS UUID NOT NULL PRIMARY KEY,
8 | payment_hash BLOB AS ByteVector32 UNIQUE,
9 | tx_id BLOB AS TxId,
10 | created_at INTEGER NOT NULL,
11 | received_at INTEGER,
12 | data BLOB AS IncomingPayment NOT NULL
13 | );
14 |
15 | CREATE INDEX payments_incoming_payment_hash_idx ON payments_incoming(payment_hash);
16 | CREATE INDEX payments_incoming_tx_id_idx ON payments_incoming(tx_id);
17 |
18 | -- Create indexes to optimize the queries in AggregatedQueries.
19 | -- Tip: Use "explain query plan" to ensure they're actually being used.
20 | CREATE INDEX payments_incoming_filter_idx
21 | ON payments_incoming(received_at)
22 | WHERE received_at IS NOT NULL;
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/sqldelight/paymentsdb/migrations/2.sqm:
--------------------------------------------------------------------------------
1 | -- Migration: v2 -> v3
2 | --
3 | -- Changes:
4 | -- * Added table payments_metadata
5 |
6 | import fr.acinq.phoenix.db.payments.LnurlBase;
7 | import fr.acinq.phoenix.db.payments.LnurlMetadata;
8 | import fr.acinq.phoenix.db.payments.LnurlSuccessAction;
9 |
10 | CREATE TABLE IF NOT EXISTS payments_metadata (
11 | type INTEGER NOT NULL,
12 | id TEXT NOT NULL,
13 | lnurl_base_type TEXT AS LnurlBase.TypeVersion,
14 | lnurl_base_blob BLOB,
15 | lnurl_description TEXT,
16 | lnurl_metadata_type TEXT AS LnurlMetadata.TypeVersion,
17 | lnurl_metadata_blob BLOB,
18 | lnurl_successAction_type TEXT AS LnurlSuccessAction.TypeVersion,
19 | lnurl_successAction_blob BLOB,
20 | user_description TEXT,
21 | PRIMARY KEY (type, id)
22 | );
23 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/sqldelight/paymentsdb/migrations/3.sqm:
--------------------------------------------------------------------------------
1 | -- Migration: v3 -> v4
2 | --
3 | -- Changes:
4 | -- * Added column: payments_metadata.user_notes
5 | -- * Added column: payments_metadata.modified_at
6 | -- * Added indexes to improve performance of listAllPaymentsOrder query
7 |
8 | ALTER TABLE payments_metadata ADD COLUMN user_notes TEXT DEFAULT NULL;
9 | ALTER TABLE payments_metadata ADD COLUMN modified_at INTEGER DEFAULT NULL;
10 |
11 | CREATE INDEX IF NOT EXISTS incoming_payments_created_at_idx ON incoming_payments(created_at);
12 | CREATE INDEX IF NOT EXISTS outgoing_payments_created_at_idx ON outgoing_payments(created_at);
13 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/sqldelight/paymentsdb/migrations/4.sqm:
--------------------------------------------------------------------------------
1 | -- Migration: v4 -> v5
2 | --
3 | -- Changes:
4 | -- * Added column: payments_metadata.original_fiat_type
5 | -- * Added column: payments_metadata.original_fiat_rate
6 |
7 | ALTER TABLE payments_metadata ADD COLUMN original_fiat_type TEXT DEFAULT NULL;
8 | ALTER TABLE payments_metadata ADD COLUMN original_fiat_rate REAL DEFAULT NULL;
9 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/sqldelight/paymentsdb/migrations/5.sqm:
--------------------------------------------------------------------------------
1 | import fr.acinq.phoenix.db.payments.OutgoingPartClosingInfoTypeVersion;
2 |
3 | -- Migration: v5 -> v6
4 | --
5 | -- Changes:
6 | -- * Add a new table that stores the transactions closing a lightning channel, represented
7 | -- as parts of an outgoing payment.
8 | -- * Add an index on parent_id in the closing txs table.
9 |
10 | CREATE TABLE IF NOT EXISTS outgoing_payment_closing_tx_parts (
11 | part_id TEXT NOT NULL PRIMARY KEY,
12 | part_parent_id TEXT NOT NULL,
13 | part_tx_id BLOB NOT NULL,
14 | part_amount_sat INTEGER NOT NULL,
15 | part_closing_info_type TEXT AS OutgoingPartClosingInfoTypeVersion NOT NULL,
16 | part_closing_info_blob BLOB NOT NULL,
17 | part_created_at INTEGER NOT NULL,
18 |
19 | FOREIGN KEY(part_parent_id) REFERENCES outgoing_payments(id)
20 | );
21 |
22 | CREATE INDEX IF NOT EXISTS parent_id_idx ON outgoing_payment_closing_tx_parts(part_parent_id);
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/sqldelight/paymentsdb/migrations/6.sqm:
--------------------------------------------------------------------------------
1 | -- Migration: v6 -> v7
2 | --
3 | -- Changes:
4 | -- * Removed index: received_amount_msat_idx
5 | -- * Replaced index: incoming_payments_created_at_idx -> incoming_payments_filter_idx
6 | -- * Replaced index: outgoing_payments_created_at_idx -> outgoing_payments_filter_idx
7 |
8 | DROP INDEX IF EXISTS received_amount_msat_idx;
9 |
10 | DROP INDEX IF EXISTS incoming_payments_created_at_idx;
11 | CREATE INDEX IF NOT EXISTS incoming_payments_filter_idx
12 | ON incoming_payments(received_at)
13 | WHERE received_at IS NOT NULL;
14 |
15 | DROP INDEX IF EXISTS outgoing_payments_created_at_idx;
16 | CREATE INDEX IF NOT EXISTS outgoing_payments_filter_idx
17 | ON outgoing_payments(completed_at);
18 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/sqldelight/paymentsdb/migrations/8.sqm:
--------------------------------------------------------------------------------
1 | import fr.acinq.phoenix.db.payments.InboundLiquidityLeaseTypeVersion;
2 |
3 | -- Migration: v8 -> v9
4 | --
5 | -- Changes:
6 | -- * add a new inbound_liquidity_outgoing_payments table to store inbound liquidity payments
7 |
8 | CREATE TABLE IF NOT EXISTS inbound_liquidity_outgoing_payments (
9 | id TEXT NOT NULL PRIMARY KEY,
10 | mining_fees_sat INTEGER NOT NULL,
11 | channel_id BLOB NOT NULL,
12 | tx_id BLOB NOT NULL,
13 | lease_type TEXT AS InboundLiquidityLeaseTypeVersion NOT NULL,
14 | lease_blob BLOB NOT NULL,
15 | created_at INTEGER NOT NULL,
16 | confirmed_at INTEGER DEFAULT NULL,
17 | locked_at INTEGER DEFAULT NULL
18 | );
19 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonMain/sqldelight/paymentsdb/migrations/9.sqm:
--------------------------------------------------------------------------------
1 | import fr.acinq.phoenix.db.payments.InboundLiquidityLeaseTypeVersion;
2 |
3 | -- Migration: v9 -> v10
4 | --
5 | -- Changes:
6 | -- * Added a new column [payment_details_type] in table [inbound_liquidity_outgoing_payments]
7 |
8 | ALTER TABLE inbound_liquidity_outgoing_payments ADD COLUMN payment_details_type TEXT DEFAULT NULL;
9 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonTest/kotlin/fr/acinq/phoenix/utils/TestLoggerFactory.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2024 ACINQ SAS
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package fr.acinq.phoenix.utils
18 |
19 | import co.touchlab.kermit.CommonWriter
20 | import co.touchlab.kermit.NoTagFormatter
21 | import co.touchlab.kermit.Severity
22 | import co.touchlab.kermit.loggerConfigInit
23 | import fr.acinq.lightning.logging.LoggerFactory
24 |
25 | val testLoggerFactory: LoggerFactory by lazy {
26 | LoggerFactory(
27 | config = loggerConfigInit(
28 | logWriters = arrayOf(CommonWriter(NoTagFormatter)),
29 | minSeverity = Severity.Info
30 | )
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/phoenix-shared/src/commonTest/resources/sampledbs/v1/payments-testnet-a224978853d2f4c94ac8e2dbb2acf8344e0146d0.sqlite:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ACINQ/phoenix/b26ab3a809122467a63c6c17efd5f3d5d1bb3b4f/phoenix-shared/src/commonTest/resources/sampledbs/v1/payments-testnet-a224978853d2f4c94ac8e2dbb2acf8344e0146d0.sqlite
--------------------------------------------------------------------------------
/phoenix-shared/src/commonTest/resources/sampledbs/v1/payments-testnet-fedc36138a62ceadc8a93861d2c46f5ca5e8b418.sqlite:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ACINQ/phoenix/b26ab3a809122467a63c6c17efd5f3d5d1bb3b4f/phoenix-shared/src/commonTest/resources/sampledbs/v1/payments-testnet-fedc36138a62ceadc8a93861d2c46f5ca5e8b418.sqlite
--------------------------------------------------------------------------------
/phoenix-shared/src/commonTest/resources/sampledbs/v10/payments-testnet-28903aff.sqlite:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ACINQ/phoenix/b26ab3a809122467a63c6c17efd5f3d5d1bb3b4f/phoenix-shared/src/commonTest/resources/sampledbs/v10/payments-testnet-28903aff.sqlite
--------------------------------------------------------------------------------
/phoenix-shared/src/commonTest/resources/sampledbs/v10/payments-testnet-6a5e6f.sqlite:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ACINQ/phoenix/b26ab3a809122467a63c6c17efd5f3d5d1bb3b4f/phoenix-shared/src/commonTest/resources/sampledbs/v10/payments-testnet-6a5e6f.sqlite
--------------------------------------------------------------------------------
/phoenix-shared/src/commonTest/resources/sampledbs/v10/payments-testnet-f921bddf.sqlite:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ACINQ/phoenix/b26ab3a809122467a63c6c17efd5f3d5d1bb3b4f/phoenix-shared/src/commonTest/resources/sampledbs/v10/payments-testnet-f921bddf.sqlite
--------------------------------------------------------------------------------
/phoenix-shared/src/commonTest/resources/sampledbs/v6/payments-testnet-700486fc7a90d5922d6f993f2941ab9f9f1a9d85.sqlite:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ACINQ/phoenix/b26ab3a809122467a63c6c17efd5f3d5d1bb3b4f/phoenix-shared/src/commonTest/resources/sampledbs/v6/payments-testnet-700486fc7a90d5922d6f993f2941ab9f9f1a9d85.sqlite
--------------------------------------------------------------------------------
/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/data/iosElectrumRegtestConf.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.data
2 |
3 | import fr.acinq.lightning.io.TcpSocket
4 | import fr.acinq.lightning.utils.ServerAddress
5 |
6 | actual fun platformElectrumRegtestConf(): ServerAddress = ServerAddress(host = "127.0.0.1", port = 51002, TcpSocket.TLS.DISABLED)
7 |
--------------------------------------------------------------------------------
/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/db/CloudKitDb.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.db
2 |
3 | import fr.acinq.phoenix.db.payments.*
4 | import kotlinx.coroutines.*
5 |
6 | class CloudKitDb(
7 | appDb: SqliteAppDb,
8 | paymentsDb: SqlitePaymentsDb
9 | ): CloudKitInterface, CoroutineScope by MainScope() {
10 |
11 | val contacts = CloudKitContactsDb(paymentsDb)
12 | val payments = CloudKitPaymentsDb(paymentsDb)
13 | }
14 |
--------------------------------------------------------------------------------
/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/utils/PassthruLogWriter.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.utils
2 |
3 | import co.touchlab.kermit.LogWriter
4 | import co.touchlab.kermit.Severity
5 |
6 | class PassthruLogWriter(
7 | val logger: (Severity, String, String) -> Unit
8 | ) : LogWriter() {
9 |
10 | constructor(ctx: PlatformContext) : this(
11 | logger = ctx.logger!!
12 | )
13 |
14 | override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) {
15 | logger(severity, message, tag)
16 | }
17 | }
--------------------------------------------------------------------------------
/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/utils/PhoenixExposure.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.utils
2 |
3 | import fr.acinq.lightning.MilliSatoshi
4 | import fr.acinq.lightning.utils.UUID
5 | import fr.acinq.phoenix.data.ContactInfo
6 | import fr.acinq.phoenix.data.LocalChannelInfo
7 | import fr.acinq.phoenix.data.availableForReceive
8 | import fr.acinq.phoenix.data.canRequestLiquidity
9 | import fr.acinq.phoenix.data.inFlightPaymentsCount
10 |
11 | /**
12 | * Workarounds for various shortcomings between Kotlin and iOS.
13 | */
14 |
15 | //fun NodeParamsManager.Companion._liquidityLeaseRate(amount: Satoshi): LiquidityAds_LeaseRate {
16 | // val result = this.liquidityLeaseRate(amount)
17 | // return LiquidityAds_LeaseRate(result)
18 | //}
19 |
20 | fun LocalChannelInfo.Companion.availableForReceive(
21 | channels: List
22 | ): MilliSatoshi? {
23 | return channels.availableForReceive()
24 | }
25 |
26 | fun LocalChannelInfo.Companion.canRequestLiquidity(
27 | channels: List
28 | ): Boolean {
29 | return channels.canRequestLiquidity()
30 | }
31 |
32 | fun LocalChannelInfo.Companion.inFlightPaymentsCount(
33 | channels: List
34 | ): Int {
35 | return channels.inFlightPaymentsCount()
36 | }
--------------------------------------------------------------------------------
/phoenix-shared/src/iosMain/kotlin/fr/acinq/phoenix/utils/platformIos.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.utils
2 |
3 | import co.touchlab.kermit.Severity
4 | import platform.Foundation.*
5 |
6 | actual class PlatformContext(
7 | val logger: ((Severity, String, String) -> Unit)? = null
8 | )
9 |
10 | actual fun getApplicationFilesDirectoryPath(ctx: PlatformContext): String =
11 | NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true)[0] as String
12 |
13 | actual fun getApplicationCacheDirectoryPath(ctx: PlatformContext): String =
14 | NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, true)[0] as String
15 |
16 | actual fun getDatabaseFilesDirectoryPath(ctx: PlatformContext): String? {
17 | return NSFileManager.defaultManager.containerURLForSecurityApplicationGroupIdentifier(
18 | groupIdentifier = "group.co.acinq.phoenix"
19 | )?.URLByAppendingPathComponent(
20 | pathComponent = "databases",
21 | isDirectory = true
22 | )?.path
23 | }
24 |
25 | actual fun getTemporaryDirectoryPath(ctx: PlatformContext): String =
26 | NSTemporaryDirectory()
27 |
--------------------------------------------------------------------------------
/phoenix-shared/src/iosTest/kotlin/fr/acinq/phoenix/data/ElectrumServersTest.kt:
--------------------------------------------------------------------------------
1 | package fr.acinq.phoenix.data
2 |
3 | import fr.acinq.lightning.utils.ServerAddress
4 |
5 | actual suspend fun connect(server: ServerAddress) {
6 | }
--------------------------------------------------------------------------------
/phoenix-shared/src/iosTest/kotlin/fr/acinq/phoenix/db/PaymentsDbMigrationTest.ios.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2024 ACINQ SAS
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package fr.acinq.phoenix.db
18 |
19 | import fr.acinq.phoenix.utils.PlatformContext
20 | import okio.Path
21 | import kotlin.collections.List
22 |
23 | actual abstract class UsingContextTest {
24 | actual fun getPlatformContext(): PlatformContext = PlatformContext()
25 | actual fun setUpDatabase(context: PlatformContext, databasePaths: List) {
26 | TODO()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | mavenCentral()
5 | maven(url = "https://maven.pkg.jetbrains.space/public/p/compose/dev")
6 | }
7 | }
8 |
9 | rootProject.name = "phoenix"
10 |
11 | // Android app may be skipped to make life easier on iOS developers.
12 | // Use `skip.android` in `local.properties` to define whether android app are built or not.
13 | // By default, Android apps are NOT skipped.
14 | val skipAndroid = File("$rootDir/local.properties").takeIf { it.exists() }
15 | ?.inputStream()?.use { java.util.Properties().apply { load(it) } }
16 | ?.run { getProperty("skip.android", "false")?.toBoolean() }
17 | ?: false
18 |
19 | // Inject the skip value in System properties so that it can be used in other gradle build files.
20 | System.setProperty("includeAndroid", (!skipAndroid).toString())
21 |
22 | // The shared app is always included.
23 | include(":phoenix-shared")
24 |
25 | // Android apps are optional.
26 | if (!skipAndroid) {
27 | include(":phoenix-android")
28 | }
29 |
--------------------------------------------------------------------------------