Initial commit
This commit is contained in:
commit
d791179125
46
.gitignore
vendored
Normal file
46
.gitignore
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
.gradle
|
||||||
|
build/
|
||||||
|
.cxx
|
||||||
|
|
||||||
|
# this file is local to the dev environment and must not be pushed!
|
||||||
|
local.properties
|
||||||
|
|
||||||
|
# Ignore specific Mac files
|
||||||
|
.DS_Store
|
||||||
|
# Non relevant Xcode files
|
||||||
|
xcuserdata/
|
||||||
|
# HPROF
|
||||||
|
*.hprof
|
||||||
|
|
||||||
|
# Ignore Gradle GUI config
|
||||||
|
gradle-app.setting
|
||||||
|
|
||||||
|
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
|
||||||
|
!gradle-wrapper.jar
|
||||||
|
|
||||||
|
# Cache of project
|
||||||
|
.gradletasknamecache
|
||||||
|
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# sbt specific
|
||||||
|
.cache/
|
||||||
|
.history/
|
||||||
|
.lib/
|
||||||
|
dist/*
|
||||||
|
target/
|
||||||
|
lib_managed/
|
||||||
|
src_managed/
|
||||||
|
project/boot/
|
||||||
|
project/plugins/project/
|
||||||
|
|
||||||
|
# Scala-IDE specific
|
||||||
|
.scala_dependencies
|
||||||
|
.worksheet
|
||||||
|
|
||||||
|
.idea
|
||||||
|
*.iml
|
||||||
|
target/
|
||||||
|
project/target
|
||||||
|
DeleteMe*.scala
|
152
CONTRIBUTING.md
Normal file
152
CONTRIBUTING.md
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
# Contributing to the lightning-kmp project
|
||||||
|
|
||||||
|
ACINQ welcomes contributions in the form of peer review, testing and patches.
|
||||||
|
This document explains the practical process and guidelines for contributing.
|
||||||
|
|
||||||
|
While developing a Lightning implementation is an exciting project that spans many domains
|
||||||
|
(cryptography, peer-to-peer networking, databases, etc), contributors must keep in mind that this
|
||||||
|
represents real money and introducing bugs or security vulnerabilities can have far more dire
|
||||||
|
consequences than in typical projects. In the world of cryptocurrencies, even the smallest bug in
|
||||||
|
the wrong area can cost users a significant amount of money.
|
||||||
|
|
||||||
|
If you're looking for somewhere to start contributing, check out the [good first issue](https://github.com/ACINQ/lightning-kmp/issues?q=is%3Aopen+is%3Aissue+label%3A"good+first+issue") list.
|
||||||
|
|
||||||
|
Another way to start contributing is by adding tests or improving them.
|
||||||
|
This will help you understand the different parts of the codebase and how they work together.
|
||||||
|
|
||||||
|
## Communicating
|
||||||
|
|
||||||
|
We recommend using our Gitter [developers channel](https://gitter.im/ACINQ/developers).
|
||||||
|
Introducing yourself and explaining what you'd like to work on is always a good idea: you will get
|
||||||
|
some pointers and feedback from experienced contributors. It will also ensure that you're not
|
||||||
|
duplicating work that someone else is doing.
|
||||||
|
|
||||||
|
We use Github issues only for, well, issues (mostly bugs that need to be investigated).
|
||||||
|
You can also use Github issues for [feature requests](https://github.com/ACINQ/lightning-kmp/issues?q=is%3Aissue+label%3A"feature+request").
|
||||||
|
|
||||||
|
## Recommended Reading
|
||||||
|
|
||||||
|
- [Bitcoin Whitepaper](https://bitcoin.org/bitcoin.pdf)
|
||||||
|
- [Lightning Network Whitepaper](https://lightning.network/lightning-network-paper.pdf)
|
||||||
|
- [Deployable Lightning](https://github.com/ElementsProject/lightning/raw/master/doc/deployable-lightning.pdf)
|
||||||
|
- [Understanding the Lightning Network](https://bitcoinmagazine.com/articles/understanding-the-lightning-network-part-building-a-bidirectional-payment-channel-1464710791)
|
||||||
|
- [Lightning Network Specification](https://github.com/lightningnetwork/lightning-rfc)
|
||||||
|
- [High Level Lightning Network Specification](https://medium.com/@rusty_lightning/the-bitcoin-lightning-spec-part-1-8-a7720fb1b4da)
|
||||||
|
|
||||||
|
## Recommended Skillset
|
||||||
|
|
||||||
|
lightning-kmp uses [Kotlin Multiplatform](https://kotlinlang.org/docs/reference/multiplatform.html) with [Kotlin Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html).
|
||||||
|
Good understanding of these technologies is required to contribute.
|
||||||
|
There are a lot of good resources online to learn about them.
|
||||||
|
|
||||||
|
## Contributor Workflow
|
||||||
|
|
||||||
|
To contribute a patch, the workflow is as follows:
|
||||||
|
|
||||||
|
1. [Fork repository](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) (only the first time)
|
||||||
|
2. Create a topic branch
|
||||||
|
3. Add commits
|
||||||
|
4. Open a pull request
|
||||||
|
|
||||||
|
### Pull Request Philosophy
|
||||||
|
|
||||||
|
Pull requests should always be focused. For example, a pull request could add a feature, fix a bug,
|
||||||
|
or refactor code; but not a mixture.
|
||||||
|
Please also avoid super pull requests which attempt to do too much, are overly large, or overly
|
||||||
|
complex as this makes review difficult.
|
||||||
|
|
||||||
|
You should try your best to make reviewers' lives as easy as possible: a lot more time will be
|
||||||
|
spent reading your code than the time you spent writing it.
|
||||||
|
The quicker your changes are merged to master, the less time you will need to spend rebasing and
|
||||||
|
otherwise trying to keep up with the master branch.
|
||||||
|
|
||||||
|
Pull request should always include a clean, detailed description of what they fix/improve, why,
|
||||||
|
and how.
|
||||||
|
Even if you think that it is obvious, don't be shy and add explicit details and explanations.
|
||||||
|
|
||||||
|
When fixing a bug, please start by adding a failing test that reproduces the issue.
|
||||||
|
Create a first commit containing that test without the fix: this makes it easy to verify that the
|
||||||
|
test corcrectly failed. You can then fix the bug in additional commits.
|
||||||
|
|
||||||
|
When adding a new feature, thought must be given to the long term technical debt and maintenance
|
||||||
|
that feature may require after inclusion. Before proposing a new feature that will require
|
||||||
|
maintenance, please consider if you are willing to maintain it (including bug fixing).
|
||||||
|
|
||||||
|
When addressing pull request comments, we recommend using [fixup commits](https://robots.thoughtbot.com/autosquashing-git-commits).
|
||||||
|
The reason for this is two fold: it makes it easier for the reviewer to see what changes have been
|
||||||
|
made between versions (since Github doesn't easily show prior versions) and it makes it easier on
|
||||||
|
the PR author as they can set it to auto-squash the fixup commits on rebase.
|
||||||
|
|
||||||
|
It's recommended to take great care in writing tests and ensuring the entire test suite has a
|
||||||
|
stable successful outcome; lightning-kmp uses continuous integration techniques and having a stable build
|
||||||
|
helps the reviewers with their job.
|
||||||
|
|
||||||
|
Contributors should follow the default Kotlin coding style guide. If you use IntelliJ:
|
||||||
|
|
||||||
|
- File > Settings > Editor > Code Style
|
||||||
|
- In the "Formatter Control" tab, check "Enable formatter markers in comments"
|
||||||
|
- File > Settings > Editor > Code Style > Kotlin
|
||||||
|
- select "Set from..." and choose "Kotline style guide"
|
||||||
|
- set "Hard wrap at" to 240
|
||||||
|
|
||||||
|
### Signed Commits
|
||||||
|
|
||||||
|
We ask contributors to sign their commits.
|
||||||
|
You can find setup instructions [here](https://help.github.com/en/github/authenticating-to-github/signing-commits).
|
||||||
|
|
||||||
|
### Commit Message
|
||||||
|
|
||||||
|
lightning-kmp keeps a clean commit history on the master branch with well-formed commit messages.
|
||||||
|
|
||||||
|
Here is a model Git commit message:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Short (50 chars or less) summary of changes
|
||||||
|
|
||||||
|
More detailed explanatory text, if necessary. Wrap it to about 72
|
||||||
|
characters or so. In some contexts, the first line is treated as the
|
||||||
|
subject of an email and the rest of the text as the body. The blank
|
||||||
|
line separating the summary from the body is critical (unless you omit
|
||||||
|
the body entirely); tools like rebase can get confused if you run the
|
||||||
|
two together.
|
||||||
|
|
||||||
|
Write your commit message in the present tense: "Fix bug" and not
|
||||||
|
"Fixed bug". This convention matches up with commit messages generated
|
||||||
|
by commands like git merge and git revert.
|
||||||
|
|
||||||
|
Further paragraphs come after blank lines.
|
||||||
|
|
||||||
|
- Bullet points are okay, too
|
||||||
|
- Typically a hyphen or asterisk is used for the bullet, preceded by a
|
||||||
|
single space, with blank lines in between, but conventions vary here
|
||||||
|
- Use a hanging indent
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
We try to minimize our dependencies (libraries and tools). Introducing new dependencies increases
|
||||||
|
package size, attack surface and cognitive overhead.
|
||||||
|
|
||||||
|
If your contribution is adding a new dependency, please detail:
|
||||||
|
|
||||||
|
- why you need it
|
||||||
|
- why you chose this specific library/tool (a thorough analysis of alternatives will be
|
||||||
|
appreciated)
|
||||||
|
|
||||||
|
Contributions that add new dependencies may take longer to approve because a detailed audit of the
|
||||||
|
dependency may be required.
|
||||||
|
|
||||||
|
### IntelliJ Tips
|
||||||
|
|
||||||
|
If you're using [IntelliJ](https://www.jetbrains.com/idea/), here are some useful commands:
|
||||||
|
|
||||||
|
- Ctrl+Alt+L: format file (ensures consistency in the codebase)
|
||||||
|
- Ctrl+Alt+o: optimize imports (removes unused imports)
|
||||||
|
|
||||||
|
### Contribution Checklist
|
||||||
|
|
||||||
|
- The code being submitted is accompanied by tests which exercise both the positive and negative
|
||||||
|
(error paths) conditions (if applicable)
|
||||||
|
- The code being submitted is correctly formatted
|
||||||
|
- The code being submitted has a clean, easy-to-follow commit history
|
||||||
|
- All commits are signed
|
201
LICENSE
Normal file
201
LICENSE
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "{}"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright 2018 ACINQ SAS
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
119
build.gradle.kts
Normal file
119
build.gradle.kts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTargetWithHostTests
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
dependencies {
|
||||||
|
classpath("app.cash.sqldelight:gradle-plugin:2.0.1")
|
||||||
|
}
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
kotlin("multiplatform") version "1.9.23"
|
||||||
|
kotlin("plugin.serialization") version "1.9.23"
|
||||||
|
id("app.cash.sqldelight") version "2.0.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
group = "fr.acinq.lightning"
|
||||||
|
version = "0.1-SNAPSHOT"
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
// using the local maven repository with Kotlin Multi Platform can lead to build errors that are hard to diagnose.
|
||||||
|
// uncomment this only if you need to experiment with snapshot dependencies that have not yet be published.
|
||||||
|
mavenLocal()
|
||||||
|
maven("https://oss.sonatype.org/content/repositories/snapshots")
|
||||||
|
mavenCentral()
|
||||||
|
google()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvm()
|
||||||
|
|
||||||
|
fun KotlinNativeTargetWithHostTests.phoenixBinaries() {
|
||||||
|
binaries {
|
||||||
|
executable("phoenixd") {
|
||||||
|
entryPoint = "fr.acinq.lightning.bin.main"
|
||||||
|
optimized = false // without this, release mode throws 'Index 0 out of bounds for length 0' in StaticInitializersOptimization.kt
|
||||||
|
}
|
||||||
|
executable("phoenix-cli") {
|
||||||
|
entryPoint = "fr.acinq.lightning.cli.main"
|
||||||
|
optimized = false // without this, release mode throws 'Index 0 out of bounds for length 0' in StaticInitializersOptimization.kt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentOs = org.gradle.internal.os.OperatingSystem.current()
|
||||||
|
if (currentOs.isLinux) {
|
||||||
|
linuxX64 {
|
||||||
|
phoenixBinaries()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentOs.isMacOsX) {
|
||||||
|
macosX64 {
|
||||||
|
phoenixBinaries()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val ktorVersion = "2.3.8"
|
||||||
|
fun ktor(module: String) = "io.ktor:ktor-$module:$ktorVersion"
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
commonMain {
|
||||||
|
dependencies {
|
||||||
|
implementation("fr.acinq.lightning:lightning-kmp:1.6.2-SNAPSHOT")
|
||||||
|
// ktor serialization
|
||||||
|
implementation(ktor("serialization-kotlinx-json"))
|
||||||
|
// ktor server
|
||||||
|
implementation(ktor("server-core"))
|
||||||
|
implementation(ktor("server-content-negotiation"))
|
||||||
|
implementation(ktor("server-cio"))
|
||||||
|
implementation(ktor("server-websockets"))
|
||||||
|
implementation(ktor("server-auth"))
|
||||||
|
implementation(ktor("server-status-pages")) // exception handling
|
||||||
|
// ktor client (needed for webhook)
|
||||||
|
implementation(ktor("client-core"))
|
||||||
|
implementation(ktor("client-content-negotiation"))
|
||||||
|
implementation(ktor("client-cio"))
|
||||||
|
implementation(ktor("client-auth"))
|
||||||
|
implementation(ktor("client-json"))
|
||||||
|
|
||||||
|
implementation("com.squareup.okio:okio:3.8.0")
|
||||||
|
implementation("com.github.ajalt.clikt:clikt:4.2.2")
|
||||||
|
implementation("app.cash.sqldelight:coroutines-extensions:2.0.1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jvmMain {
|
||||||
|
dependencies {
|
||||||
|
implementation("app.cash.sqldelight:sqlite-driver:2.0.1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nativeMain {
|
||||||
|
dependencies {
|
||||||
|
implementation("app.cash.sqldelight:native-driver:2.0.1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// forward std input when app is run via gradle (otherwise keyboard input will return EOF)
|
||||||
|
tasks.withType<JavaExec> {
|
||||||
|
standardInput = System.`in`
|
||||||
|
}
|
||||||
|
|
||||||
|
sqldelight {
|
||||||
|
databases {
|
||||||
|
create("ChannelsDatabase") {
|
||||||
|
packageName.set("fr.acinq.phoenix.db")
|
||||||
|
srcDirs.from("src/commonMain/sqldelight/channelsdb")
|
||||||
|
}
|
||||||
|
create("PaymentsDatabase") {
|
||||||
|
packageName.set("fr.acinq.phoenix.db")
|
||||||
|
srcDirs.from("src/commonMain/sqldelight/paymentsdb")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
gradle.properties
Normal file
9
gradle.properties
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# gradle
|
||||||
|
org.gradle.jvmargs=-Xmx1536m
|
||||||
|
org.gradle.parallel=true
|
||||||
|
# kotlin
|
||||||
|
kotlin.code.style=official
|
||||||
|
kotlin.incremental.multiplatform=true
|
||||||
|
kotlin.mpp.stability.nowarn=true
|
||||||
|
kotlin.mpp.enableCInteropCommonization=true
|
||||||
|
kotlin.native.ignoreDisabledTargets=true
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
249
gradlew
vendored
Executable file
249
gradlew
vendored
Executable file
@ -0,0 +1,249 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
92
gradlew.bat
vendored
Normal file
92
gradlew.bat
vendored
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
9
settings.gradle.kts
Normal file
9
settings.gradle.kts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
rootProject.name = "phoenixd"
|
||||||
|
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
gradlePluginPortal()
|
||||||
|
maven("https://dl.bintray.com/kotlin/kotlin-eap")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
185
src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt
Normal file
185
src/commonMain/kotlin/fr/acinq/lightning/bin/Api.kt
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
package fr.acinq.lightning.bin
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.Bitcoin
|
||||||
|
import fr.acinq.bitcoin.ByteVector
|
||||||
|
import fr.acinq.bitcoin.ByteVector32
|
||||||
|
import fr.acinq.bitcoin.Script
|
||||||
|
import fr.acinq.bitcoin.utils.Either
|
||||||
|
import fr.acinq.bitcoin.utils.toEither
|
||||||
|
import fr.acinq.lightning.Lightning.randomBytes32
|
||||||
|
import fr.acinq.lightning.NodeParams
|
||||||
|
import fr.acinq.lightning.bin.json.ApiType.*
|
||||||
|
import fr.acinq.lightning.blockchain.fee.FeeratePerByte
|
||||||
|
import fr.acinq.lightning.blockchain.fee.FeeratePerKw
|
||||||
|
import fr.acinq.lightning.channel.ChannelCommand
|
||||||
|
import fr.acinq.lightning.channel.states.ChannelStateWithCommitments
|
||||||
|
import fr.acinq.lightning.channel.states.ClosingFeerates
|
||||||
|
import fr.acinq.lightning.io.Peer
|
||||||
|
import fr.acinq.lightning.io.WrappedChannelCommand
|
||||||
|
import fr.acinq.lightning.payment.Bolt11Invoice
|
||||||
|
import fr.acinq.lightning.utils.sat
|
||||||
|
import fr.acinq.lightning.utils.sum
|
||||||
|
import fr.acinq.lightning.utils.toByteVector
|
||||||
|
import fr.acinq.lightning.utils.toMilliSatoshi
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.serialization.kotlinx.*
|
||||||
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.auth.*
|
||||||
|
import io.ktor.server.engine.*
|
||||||
|
import io.ktor.server.plugins.*
|
||||||
|
import io.ktor.server.plugins.contentnegotiation.*
|
||||||
|
import io.ktor.server.plugins.statuspages.*
|
||||||
|
import io.ktor.server.request.*
|
||||||
|
import io.ktor.server.response.*
|
||||||
|
import io.ktor.server.routing.*
|
||||||
|
import io.ktor.server.websocket.*
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
class Api(private val nodeParams: NodeParams, private val peer: Peer, private val eventsFlow: SharedFlow<ApiEvent>, private val password: String, private val webhookUrl: Url?) {
|
||||||
|
|
||||||
|
fun Application.module() {
|
||||||
|
|
||||||
|
val json = Json {
|
||||||
|
prettyPrint = true
|
||||||
|
isLenient = true
|
||||||
|
serializersModule = fr.acinq.lightning.json.JsonSerializers.json.serializersModule
|
||||||
|
}
|
||||||
|
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json(json)
|
||||||
|
}
|
||||||
|
install(WebSockets) {
|
||||||
|
contentConverter = KotlinxWebsocketSerializationConverter(json)
|
||||||
|
timeoutMillis = 10_000
|
||||||
|
pingPeriodMillis = 10_000
|
||||||
|
}
|
||||||
|
install(StatusPages) {
|
||||||
|
exception<Throwable> { call, cause ->
|
||||||
|
call.respondText(text = cause.message ?: "", status = defaultExceptionStatusCode(cause) ?: HttpStatusCode.InternalServerError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
install(Authentication) {
|
||||||
|
basic {
|
||||||
|
validate { credentials ->
|
||||||
|
if (credentials.password == password) {
|
||||||
|
UserIdPrincipal(credentials.name)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
routing {
|
||||||
|
authenticate {
|
||||||
|
get("getinfo") {
|
||||||
|
val info = NodeInfo(
|
||||||
|
nodeId = nodeParams.nodeId,
|
||||||
|
channels = peer.channels.values.map { Channel.from(it) }
|
||||||
|
)
|
||||||
|
call.respond(info)
|
||||||
|
}
|
||||||
|
get("getbalance") {
|
||||||
|
val balance = peer.channels.values
|
||||||
|
.filterIsInstance<ChannelStateWithCommitments>()
|
||||||
|
.map { it.commitments.active.first().availableBalanceForSend(it.commitments.params, it.commitments.changes) }
|
||||||
|
.sum().truncateToSatoshi()
|
||||||
|
call.respond(Balance(balance, nodeParams.feeCredit.value))
|
||||||
|
}
|
||||||
|
get("listchannels") {
|
||||||
|
call.respond(peer.channels.values.toList())
|
||||||
|
}
|
||||||
|
post("createinvoice") {
|
||||||
|
val formParameters = call.receiveParameters()
|
||||||
|
val amount = formParameters.getLong("amountSat").sat
|
||||||
|
val description = formParameters.getString("description")
|
||||||
|
val invoice = peer.createInvoice(randomBytes32(), amount.toMilliSatoshi(), Either.Left(description))
|
||||||
|
call.respond(GeneratedInvoice(invoice.amount?.truncateToSatoshi(), invoice.paymentHash, serialized = invoice.write()))
|
||||||
|
}
|
||||||
|
post("payinvoice") {
|
||||||
|
val formParameters = call.receiveParameters()
|
||||||
|
val overrideAmount = formParameters["amountSat"]?.let { it.toLongOrNull() ?: invalidType("amountSat", "integer") }?.sat?.toMilliSatoshi()
|
||||||
|
val invoice = formParameters.getInvoice("invoice")
|
||||||
|
val amount = (overrideAmount ?: invoice.amount) ?: missing("amountSat")
|
||||||
|
when (val event = peer.sendLightning(amount, invoice)) {
|
||||||
|
is fr.acinq.lightning.io.PaymentSent -> call.respond(PaymentSent(event))
|
||||||
|
is fr.acinq.lightning.io.PaymentNotSent -> call.respond(PaymentFailed(event))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
post("sendtoaddress") {
|
||||||
|
val res = kotlin.runCatching {
|
||||||
|
val formParameters = call.receiveParameters()
|
||||||
|
val amount = formParameters.getLong("amountSat").sat
|
||||||
|
val scriptPubKey = formParameters.getAddressAndConvertToScript("address")
|
||||||
|
val feerate = FeeratePerKw(FeeratePerByte(formParameters.getLong("feerateSatByte").sat))
|
||||||
|
peer.spliceOut(amount, scriptPubKey, feerate)
|
||||||
|
}.toEither()
|
||||||
|
when (res) {
|
||||||
|
is Either.Right -> when (val r = res.value) {
|
||||||
|
is ChannelCommand.Commitment.Splice.Response.Created -> call.respondText(r.fundingTxId.toString())
|
||||||
|
is ChannelCommand.Commitment.Splice.Response.Failure -> call.respondText(r.toString())
|
||||||
|
else -> call.respondText("no channel available")
|
||||||
|
}
|
||||||
|
is Either.Left -> call.respondText(res.value.message.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
post("closechannel") {
|
||||||
|
val formParameters = call.receiveParameters()
|
||||||
|
val channelId = formParameters.getByteVector32("channelId")
|
||||||
|
val scriptPubKey = formParameters.getAddressAndConvertToScript("address")
|
||||||
|
val feerate = FeeratePerKw(FeeratePerByte(formParameters.getLong("feerateSatByte").sat))
|
||||||
|
peer.send(WrappedChannelCommand(channelId, ChannelCommand.Close.MutualClose(scriptPubKey, ClosingFeerates(feerate))))
|
||||||
|
call.respondText("ok")
|
||||||
|
}
|
||||||
|
webSocket("/websocket") {
|
||||||
|
try {
|
||||||
|
eventsFlow.collect { sendSerialized(it) }
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
println("onError ${closeReason.await()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
webhookUrl?.let { url ->
|
||||||
|
val client = HttpClient(io.ktor.client.engine.cio.CIO) {
|
||||||
|
install(io.ktor.client.plugins.contentnegotiation.ContentNegotiation) {
|
||||||
|
json(json = Json {
|
||||||
|
prettyPrint = true
|
||||||
|
isLenient = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
eventsFlow.collect { event ->
|
||||||
|
client.post(url) {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun missing(argName: String): Nothing = throw MissingRequestParameterException(argName)
|
||||||
|
|
||||||
|
private fun invalidType(argName: String, typeName: String): Nothing = throw ParameterConversionException(argName, typeName)
|
||||||
|
|
||||||
|
private fun Parameters.getString(argName: String): String = (this[argName] ?: missing(argName))
|
||||||
|
|
||||||
|
private fun Parameters.getByteVector32(argName: String): ByteVector32 = getString(argName).let { hex -> kotlin.runCatching { ByteVector32.fromValidHex(hex) }.getOrNull() ?: invalidType(argName, "hex32") }
|
||||||
|
|
||||||
|
private fun Parameters.getAddressAndConvertToScript(argName: String): ByteVector = Script.write(Bitcoin.addressToPublicKeyScript(nodeParams.chainHash, getString(argName)).right ?: error("invalid address")).toByteVector()
|
||||||
|
|
||||||
|
private fun Parameters.getInvoice(argName: String): Bolt11Invoice = getString(argName).let { invoice -> Bolt11Invoice.read(invoice).getOrElse { invalidType(argName, "bolt11invoice") } }
|
||||||
|
|
||||||
|
private fun Parameters.getLong(argName: String): Long = ((this[argName] ?: missing(argName)).toLongOrNull()) ?: invalidType(argName, "integer")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
9
src/commonMain/kotlin/fr/acinq/lightning/bin/Expects.kt
Normal file
9
src/commonMain/kotlin/fr/acinq/lightning/bin/Expects.kt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package fr.acinq.lightning.bin
|
||||||
|
|
||||||
|
import app.cash.sqldelight.db.SqlDriver
|
||||||
|
import okio.Path
|
||||||
|
|
||||||
|
expect val homeDirectory: Path
|
||||||
|
|
||||||
|
expect fun createAppDbDriver(dir: Path): SqlDriver
|
||||||
|
expect fun createPaymentsDbDriver(dir: Path): SqlDriver
|
@ -0,0 +1,135 @@
|
|||||||
|
package fr.acinq.lightning.bin
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.ByteVector32
|
||||||
|
import fr.acinq.bitcoin.Crypto
|
||||||
|
import fr.acinq.bitcoin.TxId
|
||||||
|
import fr.acinq.bitcoin.utils.Either
|
||||||
|
import fr.acinq.lightning.channel.ChannelException
|
||||||
|
import fr.acinq.lightning.db.*
|
||||||
|
import fr.acinq.lightning.payment.FinalFailure
|
||||||
|
import fr.acinq.lightning.payment.OutgoingPaymentFailure
|
||||||
|
import fr.acinq.lightning.utils.UUID
|
||||||
|
import fr.acinq.lightning.utils.toByteVector32
|
||||||
|
import fr.acinq.lightning.wire.FailureMessage
|
||||||
|
|
||||||
|
class InMemoryPaymentsDb : PaymentsDb {
|
||||||
|
private val incoming = mutableMapOf<ByteVector32, IncomingPayment>()
|
||||||
|
private val outgoing = mutableMapOf<UUID, LightningOutgoingPayment>()
|
||||||
|
private val outgoingParts = mutableMapOf<UUID, Pair<UUID, LightningOutgoingPayment.Part>>()
|
||||||
|
override suspend fun setLocked(txId: TxId) {}
|
||||||
|
|
||||||
|
override suspend fun addIncomingPayment(preimage: ByteVector32, origin: IncomingPayment.Origin, createdAt: Long): IncomingPayment {
|
||||||
|
val paymentHash = Crypto.sha256(preimage).toByteVector32()
|
||||||
|
require(!incoming.contains(paymentHash)) { "an incoming payment for $paymentHash already exists" }
|
||||||
|
val incomingPayment = IncomingPayment(preimage, origin, null, createdAt)
|
||||||
|
incoming[paymentHash] = incomingPayment
|
||||||
|
return incomingPayment
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getIncomingPayment(paymentHash: ByteVector32): IncomingPayment? = incoming[paymentHash]
|
||||||
|
|
||||||
|
override suspend fun receivePayment(paymentHash: ByteVector32, receivedWith: List<IncomingPayment.ReceivedWith>, receivedAt: Long) {
|
||||||
|
when (val payment = incoming[paymentHash]) {
|
||||||
|
null -> Unit // no-op
|
||||||
|
else -> incoming[paymentHash] = run {
|
||||||
|
payment.copy(
|
||||||
|
received = IncomingPayment.Received(
|
||||||
|
receivedWith = (payment.received?.receivedWith ?: emptySet()) + receivedWith,
|
||||||
|
receivedAt = receivedAt
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun listIncomingPayments(count: Int, skip: Int): List<IncomingPayment> =
|
||||||
|
incoming.values
|
||||||
|
.asSequence()
|
||||||
|
.sortedByDescending { it.createdAt }
|
||||||
|
.drop(skip)
|
||||||
|
.take(count)
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
override suspend fun listExpiredPayments(fromCreatedAt: Long, toCreatedAt: Long): List<IncomingPayment> =
|
||||||
|
incoming.values
|
||||||
|
.asSequence()
|
||||||
|
.filter { it.createdAt in fromCreatedAt until toCreatedAt }
|
||||||
|
.filter { it.isExpired() }
|
||||||
|
.filter { it.received == null }
|
||||||
|
.sortedByDescending { it.createdAt }
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
override suspend fun removeIncomingPayment(paymentHash: ByteVector32): Boolean {
|
||||||
|
val payment = getIncomingPayment(paymentHash)
|
||||||
|
return when (payment?.received) {
|
||||||
|
null -> incoming.remove(paymentHash) != null
|
||||||
|
else -> false // do nothing if payment already partially paid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun addOutgoingPayment(outgoingPayment: OutgoingPayment) {
|
||||||
|
require(!outgoing.contains(outgoingPayment.id)) { "an outgoing payment with id=${outgoingPayment.id} already exists" }
|
||||||
|
when (outgoingPayment) {
|
||||||
|
is LightningOutgoingPayment -> {
|
||||||
|
outgoingPayment.parts.forEach { require(!outgoingParts.contains(it.id)) { "an outgoing payment part with id=${it.id} already exists" } }
|
||||||
|
outgoing[outgoingPayment.id] = outgoingPayment.copy(parts = listOf())
|
||||||
|
outgoingPayment.parts.forEach { outgoingParts[it.id] = Pair(outgoingPayment.id, it) }
|
||||||
|
}
|
||||||
|
is OnChainOutgoingPayment -> {} // we don't persist on-chain payments
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getLightningOutgoingPayment(id: UUID): LightningOutgoingPayment? {
|
||||||
|
return outgoing[id]?.let { payment ->
|
||||||
|
val parts = outgoingParts.values.filter { it.first == payment.id }.map { it.second }
|
||||||
|
return when (payment.status) {
|
||||||
|
is LightningOutgoingPayment.Status.Completed.Succeeded -> payment.copy(parts = parts.filter { it.status is LightningOutgoingPayment.Part.Status.Succeeded })
|
||||||
|
else -> payment.copy(parts = parts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun completeOutgoingPaymentOffchain(id: UUID, preimage: ByteVector32, completedAt: Long) {
|
||||||
|
require(outgoing.contains(id)) { "outgoing payment with id=$id doesn't exist" }
|
||||||
|
val payment = outgoing[id]!!
|
||||||
|
outgoing[id] = payment.copy(status = LightningOutgoingPayment.Status.Completed.Succeeded.OffChain(preimage = preimage, completedAt = completedAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun completeOutgoingPaymentOffchain(id: UUID, finalFailure: FinalFailure, completedAt: Long) {
|
||||||
|
require(outgoing.contains(id)) { "outgoing payment with id=$id doesn't exist" }
|
||||||
|
val payment = outgoing[id]!!
|
||||||
|
outgoing[id] = payment.copy(status = LightningOutgoingPayment.Status.Completed.Failed(reason = finalFailure, completedAt = completedAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun addOutgoingLightningParts(parentId: UUID, parts: List<LightningOutgoingPayment.Part>) {
|
||||||
|
require(outgoing.contains(parentId)) { "parent outgoing payment with id=$parentId doesn't exist" }
|
||||||
|
parts.forEach { require(!outgoingParts.contains(it.id)) { "an outgoing payment part with id=${it.id} already exists" } }
|
||||||
|
parts.forEach { outgoingParts[it.id] = Pair(parentId, it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun completeOutgoingLightningPart(partId: UUID, failure: Either<ChannelException, FailureMessage>, completedAt: Long) {
|
||||||
|
require(outgoingParts.contains(partId)) { "outgoing payment part with id=$partId doesn't exist" }
|
||||||
|
val (parentId, part) = outgoingParts[partId]!!
|
||||||
|
outgoingParts[partId] = Pair(parentId, part.copy(status = OutgoingPaymentFailure.convertFailure(failure, completedAt)))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun completeOutgoingLightningPart(partId: UUID, preimage: ByteVector32, completedAt: Long) {
|
||||||
|
require(outgoingParts.contains(partId)) { "outgoing payment part with id=$partId doesn't exist" }
|
||||||
|
val (parentId, part) = outgoingParts[partId]!!
|
||||||
|
outgoingParts[partId] = Pair(parentId, part.copy(status = LightningOutgoingPayment.Part.Status.Succeeded(preimage, completedAt)))
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getLightningOutgoingPaymentFromPartId(partId: UUID): LightningOutgoingPayment? {
|
||||||
|
return outgoingParts[partId]?.let { (parentId, _) ->
|
||||||
|
require(outgoing.contains(parentId)) { "parent outgoing payment with id=$parentId doesn't exist" }
|
||||||
|
getLightningOutgoingPayment(parentId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun listLightningOutgoingPayments(paymentHash: ByteVector32): List<LightningOutgoingPayment> {
|
||||||
|
return outgoing.values.filter { it.paymentHash == paymentHash }.map { payment ->
|
||||||
|
val parts = outgoingParts.values.filter { it.first == payment.id }.map { it.second }
|
||||||
|
payment.copy(parts = parts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
339
src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt
Normal file
339
src/commonMain/kotlin/fr/acinq/lightning/bin/Main.kt
Normal file
@ -0,0 +1,339 @@
|
|||||||
|
package fr.acinq.lightning.bin
|
||||||
|
|
||||||
|
import co.touchlab.kermit.CommonWriter
|
||||||
|
import co.touchlab.kermit.Severity
|
||||||
|
import co.touchlab.kermit.StaticConfig
|
||||||
|
import com.github.ajalt.clikt.core.CliktCommand
|
||||||
|
import com.github.ajalt.clikt.core.context
|
||||||
|
import com.github.ajalt.clikt.core.terminal
|
||||||
|
import com.github.ajalt.clikt.output.MordantHelpFormatter
|
||||||
|
import com.github.ajalt.clikt.parameters.groups.OptionGroup
|
||||||
|
import com.github.ajalt.clikt.parameters.groups.provideDelegate
|
||||||
|
import com.github.ajalt.clikt.parameters.options.*
|
||||||
|
import com.github.ajalt.clikt.parameters.types.choice
|
||||||
|
import com.github.ajalt.clikt.parameters.types.int
|
||||||
|
import com.github.ajalt.clikt.parameters.types.restrictTo
|
||||||
|
import com.github.ajalt.clikt.sources.MapValueSource
|
||||||
|
import com.github.ajalt.mordant.rendering.TextColors.*
|
||||||
|
import com.github.ajalt.mordant.rendering.TextStyles.bold
|
||||||
|
import com.github.ajalt.mordant.rendering.TextStyles.underline
|
||||||
|
import fr.acinq.bitcoin.Chain
|
||||||
|
import fr.acinq.lightning.*
|
||||||
|
import fr.acinq.lightning.Lightning.randomBytes32
|
||||||
|
import fr.acinq.lightning.bin.conf.LSP
|
||||||
|
import fr.acinq.lightning.bin.conf.getOrGenerateSeed
|
||||||
|
import fr.acinq.lightning.bin.conf.readConfFile
|
||||||
|
import fr.acinq.lightning.bin.db.SqliteChannelsDb
|
||||||
|
import fr.acinq.lightning.bin.db.SqlitePaymentsDb
|
||||||
|
import fr.acinq.lightning.bin.json.ApiType
|
||||||
|
import fr.acinq.lightning.bin.logs.FileLogWriter
|
||||||
|
import fr.acinq.lightning.blockchain.electrum.ElectrumClient
|
||||||
|
import fr.acinq.lightning.blockchain.electrum.ElectrumConnectionStatus
|
||||||
|
import fr.acinq.lightning.blockchain.electrum.ElectrumWatcher
|
||||||
|
import fr.acinq.lightning.crypto.LocalKeyManager
|
||||||
|
import fr.acinq.lightning.db.ChannelsDb
|
||||||
|
import fr.acinq.lightning.db.Databases
|
||||||
|
import fr.acinq.lightning.db.PaymentsDb
|
||||||
|
import fr.acinq.lightning.io.Peer
|
||||||
|
import fr.acinq.lightning.io.TcpSocket
|
||||||
|
import fr.acinq.lightning.logging.LoggerFactory
|
||||||
|
import fr.acinq.lightning.payment.LiquidityPolicy
|
||||||
|
import fr.acinq.lightning.utils.Connection
|
||||||
|
import fr.acinq.lightning.utils.ServerAddress
|
||||||
|
import fr.acinq.lightning.utils.msat
|
||||||
|
import fr.acinq.lightning.utils.sat
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.cio.*
|
||||||
|
import io.ktor.server.engine.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
import okio.FileSystem
|
||||||
|
import okio.buffer
|
||||||
|
import okio.use
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
|
||||||
|
fun main(args: Array<String>) = Phoenixd().main(args)
|
||||||
|
|
||||||
|
class LiquidityOptions : OptionGroup(name = "Liquidity Options") {
|
||||||
|
val autoLiquidity by option("--auto-liquidity", help = "Amount automatically requested when inbound liquidity is needed").choice(
|
||||||
|
"off" to 0.sat,
|
||||||
|
"2m" to 2_000_000.sat,
|
||||||
|
"5m" to 5_000_000.sat,
|
||||||
|
"10m" to 10_000_000.sat,
|
||||||
|
).default(2_000_000.sat)
|
||||||
|
val maxAbsoluteFee by option("--max-absolute-fee", help = "Max absolute fee for on-chain operations. Includes mining fee and service fee for auto-liquidity.")
|
||||||
|
.int().convert { it.sat }
|
||||||
|
.restrictTo(5_000.sat..100_000.sat)
|
||||||
|
.default(40_000.sat) // with a default auto-liquidity of 2m sat, that's a max total fee of 2%
|
||||||
|
val maxRelativeFeeBasisPoint by option("--max-relative-fee-percent", help = "Max relative fee for on-chain operations in percent.", hidden = true)
|
||||||
|
.int()
|
||||||
|
.restrictTo(1..50)
|
||||||
|
.default(30)
|
||||||
|
val maxFeeCredit by option("--max-fee-credit", help = "Max fee credit, if reached payments will be rejected.", hidden = true)
|
||||||
|
.int().convert { it.sat }
|
||||||
|
.restrictTo(0.sat..100_000.sat)
|
||||||
|
.default(100_000.sat)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Phoenixd : CliktCommand() {
|
||||||
|
//private val datadir by option("--datadir", help = "Data directory").convert { it.toPath() }.default(homeDirectory / ".phoenix", defaultForHelp = "~/.phoenix")
|
||||||
|
private val datadir = homeDirectory / ".phoenix"
|
||||||
|
private val confFile = datadir / "phoenix.conf"
|
||||||
|
private val chain by option("--chain", help = "Bitcoin chain to use").choice(
|
||||||
|
"mainnet" to Chain.Mainnet, "testnet" to Chain.Testnet
|
||||||
|
).default(Chain.Testnet, defaultForHelp = "testnet")
|
||||||
|
private val customElectrumServer by option("--electrum-server", "-e", help = "Custom Electrum server")
|
||||||
|
.convert { it.split(":").run { ServerAddress(first(), last().toInt(), TcpSocket.TLS.DISABLED) } }
|
||||||
|
private val httpBindIp by option("--http-bind-ip", help = "Bind ip for the http api").default("127.0.0.1")
|
||||||
|
private val httpBindPort by option("--http-bind-port", help = "Bind port for the http api").int().default(9740)
|
||||||
|
private val httpPassword by option("--http-password", help = "Password for the http api").defaultLazy {
|
||||||
|
// the additionalValues map already contains values in phoenix.conf, so if we are here then there are no existing password
|
||||||
|
terminal.print(yellow("Generating default api password..."))
|
||||||
|
val value = randomBytes32().toHex()
|
||||||
|
val confFile = datadir / "phoenix.conf"
|
||||||
|
FileSystem.SYSTEM.createDirectories(datadir)
|
||||||
|
FileSystem.SYSTEM.appendingSink(confFile, mustExist = false).buffer().use { it.writeUtf8("\nhttp-password=$value\n") }
|
||||||
|
terminal.println(white("done"))
|
||||||
|
value
|
||||||
|
}
|
||||||
|
private val webHookUrl by option("--webhook", help = "Webhook http endpoint for push notifications (alternative to websocket)")
|
||||||
|
.convert { Url(it) }
|
||||||
|
|
||||||
|
private val liquidityOptions by LiquidityOptions()
|
||||||
|
|
||||||
|
private val verbose: Boolean by option("--verbose", help = "Verbose mode").flag(default = false)
|
||||||
|
private val silent: Boolean by option("--silent", "-s", help = "Silent mode").flag(default = false)
|
||||||
|
|
||||||
|
init {
|
||||||
|
context {
|
||||||
|
valueSource = MapValueSource(readConfFile(confFile))
|
||||||
|
helpFormatter = { MordantHelpFormatter(it, showDefaultValues = true) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
|
override fun run() {
|
||||||
|
FileSystem.SYSTEM.createDirectories(datadir)
|
||||||
|
val (seed, new) = getOrGenerateSeed(datadir)
|
||||||
|
if (new) {
|
||||||
|
runBlocking {
|
||||||
|
terminal.print(yellow("Generating new seed..."))
|
||||||
|
delay(500.milliseconds)
|
||||||
|
terminal.println(white("done"))
|
||||||
|
terminal.println()
|
||||||
|
terminal.println(green("Backup"))
|
||||||
|
terminal.println("This software is self-custodial, you have full control and responsibility over your funds.")
|
||||||
|
terminal.println("Your 12-words seed is located in ${FileSystem.SYSTEM.canonicalize(datadir)}, ${bold(red("make sure to do a backup or you risk losing your funds"))}.")
|
||||||
|
terminal.println()
|
||||||
|
terminal.println(green("How does it work?"))
|
||||||
|
terminal.println(
|
||||||
|
"""
|
||||||
|
When receiving a Lightning payment that doesn't fit within your existing channel, then:
|
||||||
|
- If the payment amount is large enough to cover mining fees and service fees for automated liquidity, then your channel will be created or enlarged right away.
|
||||||
|
- If the payment is too small, then the full amount is added to your fee credit. This credit will be used later to pay for future fees. ${bold(red("The fee credit is non-refundable"))}.
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
terminal.println()
|
||||||
|
terminal.println(
|
||||||
|
gray(
|
||||||
|
"""
|
||||||
|
Examples:
|
||||||
|
With the default settings, and assuming that current mining fees are 10k sat. The total fee for a
|
||||||
|
liquidity operation will be 10k sat (mining fee) + 20k sat (service fee for the 2m sat liquidity) = 30k sat.
|
||||||
|
|
||||||
|
${(underline + gray)("scenario A")}: you receive a continuous stream of tiny 100 sat payments
|
||||||
|
a) the first 299 incoming payments will be added to your fee credit
|
||||||
|
b) when receiving the 300th payment, a 2m sat channel will be created, with balance 0 sat on your side
|
||||||
|
c) the next 20 thousands payments will be received in your channel
|
||||||
|
d) back to a)
|
||||||
|
|
||||||
|
${(underline + gray)("scenario B")}: you receive a continuous stream of 50k sat payments
|
||||||
|
a) when receiving the first payment, a 1M sat channel will be created with balance 50k-30k=20k sat on your side
|
||||||
|
b) the next next 40 payments will be received in your channel, at that point balance is 2m sat
|
||||||
|
c) back to a)
|
||||||
|
|
||||||
|
In both scenarios, the total average fee is the same: 30k/2m = 1.5%.
|
||||||
|
You can reduce this average fee further, by choosing a higher liquidity amount (option ${bold(white("--auto-liquidity"))}),
|
||||||
|
in exchange for higher upfront costs. The higher the liquidity amount, the less significant the cost of
|
||||||
|
mining fee in relative terms.
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
terminal.println()
|
||||||
|
terminal.prompt("Please confirm by typing", choices = listOf("I understand that if I do not make a backup I risk losing my funds"), invalidChoiceMessage = "Please type those exact words:")
|
||||||
|
terminal.prompt(
|
||||||
|
"Please confirm by typing",
|
||||||
|
choices = listOf("I must not share the same seed with other phoenix instances (mobile or server) or I risk force closing my channels"),
|
||||||
|
invalidChoiceMessage = "Please type those exact words:"
|
||||||
|
)
|
||||||
|
terminal.prompt("Please confirm by typing", choices = listOf("I accept that the fee credit is non-refundable"), invalidChoiceMessage = "Please type those exact words:")
|
||||||
|
terminal.println()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo(cyan("datadir: ${FileSystem.SYSTEM.canonicalize(datadir)}"))
|
||||||
|
echo(cyan("chain: $chain"))
|
||||||
|
echo(cyan("autoLiquidity: ${liquidityOptions.autoLiquidity}"))
|
||||||
|
|
||||||
|
val scope = GlobalScope
|
||||||
|
val loggerFactory = LoggerFactory(
|
||||||
|
StaticConfig(minSeverity = Severity.Info, logWriterList = buildList {
|
||||||
|
// always log to file
|
||||||
|
add(FileLogWriter(datadir / "phoenix.log", scope))
|
||||||
|
// only log to console if verbose mode is enabled
|
||||||
|
if (verbose) add(CommonWriter())
|
||||||
|
})
|
||||||
|
)
|
||||||
|
val electrumServer = customElectrumServer ?: when (chain) {
|
||||||
|
Chain.Mainnet -> ServerAddress("electrum.acinq.co", 50001, TcpSocket.TLS.DISABLED)
|
||||||
|
Chain.Testnet -> ServerAddress("testnet1.electrum.acinq.co", 51001, TcpSocket.TLS.DISABLED)
|
||||||
|
else -> error("unsupported chain")
|
||||||
|
}
|
||||||
|
val lsp = LSP.from(chain)
|
||||||
|
val liquidityPolicy = LiquidityPolicy.Auto(
|
||||||
|
maxAbsoluteFee = liquidityOptions.maxAbsoluteFee,
|
||||||
|
maxRelativeFeeBasisPoints = liquidityOptions.maxRelativeFeeBasisPoint,
|
||||||
|
skipAbsoluteFeeCheck = false,
|
||||||
|
maxAllowedCredit = liquidityOptions.maxFeeCredit
|
||||||
|
)
|
||||||
|
val keyManager = LocalKeyManager(seed, chain, lsp.swapInXpub)
|
||||||
|
val nodeParams = NodeParams(chain, loggerFactory, keyManager)
|
||||||
|
.run {
|
||||||
|
copy(
|
||||||
|
zeroConfPeers = setOf(lsp.walletParams.trampolineNode.id),
|
||||||
|
liquidityPolicy = MutableStateFlow(liquidityPolicy),
|
||||||
|
features = features.copy(
|
||||||
|
activated = buildMap {
|
||||||
|
putAll(features.activated)
|
||||||
|
put(Feature.FeeCredit, FeatureSupport.Optional)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
echo(cyan("nodeid: ${nodeParams.nodeId}"))
|
||||||
|
|
||||||
|
val electrum = ElectrumClient(scope, loggerFactory)
|
||||||
|
val paymentsDb = SqlitePaymentsDb(loggerFactory, createPaymentsDbDriver(datadir))
|
||||||
|
val peer = Peer(
|
||||||
|
nodeParams = nodeParams, walletParams = lsp.walletParams, watcher = ElectrumWatcher(electrum, scope, loggerFactory), db = object : Databases {
|
||||||
|
override val channels: ChannelsDb
|
||||||
|
get() = SqliteChannelsDb(createAppDbDriver(datadir))
|
||||||
|
override val payments: PaymentsDb
|
||||||
|
get() = paymentsDb
|
||||||
|
}, socketBuilder = TcpSocket.Builder(), scope
|
||||||
|
)
|
||||||
|
|
||||||
|
val eventsFlow: SharedFlow<ApiType.ApiEvent> = MutableSharedFlow<ApiType.ApiEvent>().run {
|
||||||
|
scope.launch {
|
||||||
|
launch {
|
||||||
|
nodeParams.nodeEvents
|
||||||
|
.collect {
|
||||||
|
when {
|
||||||
|
it is PaymentEvents.PaymentReceived && it.amount > 0.msat -> emit(ApiType.PaymentReceived(it))
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
peer.eventsFlow
|
||||||
|
.collect {
|
||||||
|
when {
|
||||||
|
it is fr.acinq.lightning.io.PaymentSent -> emit(ApiType.PaymentSent(it))
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
asSharedFlow()
|
||||||
|
}
|
||||||
|
|
||||||
|
val listeners = scope.launch {
|
||||||
|
launch {
|
||||||
|
// drop initial CLOSED event
|
||||||
|
electrum.connectionStatus.dropWhile { it is ElectrumConnectionStatus.Closed }.collect {
|
||||||
|
when (it) {
|
||||||
|
is ElectrumConnectionStatus.Connecting -> echo(yellow("connecting to electrum server..."))
|
||||||
|
is ElectrumConnectionStatus.Connected -> echo(yellow("connected to electrum server"))
|
||||||
|
is ElectrumConnectionStatus.Closed -> echo(yellow("disconnected from electrum server"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
// drop initial CLOSED event
|
||||||
|
peer.connectionState.dropWhile { it is Connection.CLOSED }.collect {
|
||||||
|
when (it) {
|
||||||
|
Connection.ESTABLISHING -> echo(yellow("connecting to lightning peer..."))
|
||||||
|
Connection.ESTABLISHED -> echo(yellow("connected to lightning peer"))
|
||||||
|
is Connection.CLOSED -> echo(yellow("disconnected from lightning peer"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
nodeParams.nodeEvents
|
||||||
|
.filterIsInstance<PaymentEvents.PaymentReceived>()
|
||||||
|
.filter { it.amount > 0.msat }
|
||||||
|
.collect {
|
||||||
|
echo("received lightning payment: ${it.amount.truncateToSatoshi()} (${it.receivedWith.joinToString { part -> part::class.simpleName.toString().lowercase() }})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
nodeParams.nodeEvents
|
||||||
|
.filterIsInstance<LiquidityEvents.Decision.Rejected>()
|
||||||
|
.collect {
|
||||||
|
echo(yellow("lightning payment rejected: amount=${it.amount.truncateToSatoshi()} fee=${it.fee.truncateToSatoshi()} maxFee=${liquidityPolicy.maxAbsoluteFee}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
launch {
|
||||||
|
nodeParams.feeCredit
|
||||||
|
.drop(1) // we drop the initial value which is 0 sat
|
||||||
|
.collect { feeCredit -> echo("fee credit: $feeCredit") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runBlocking {
|
||||||
|
electrum.connect(electrumServer, TcpSocket.Builder())
|
||||||
|
peer.connect(connectTimeout = 10.seconds, handshakeTimeout = 10.seconds)
|
||||||
|
peer.connectionState.first { it == Connection.ESTABLISHED }
|
||||||
|
peer.registerFcmToken("super-${randomBytes32().toHex()}")
|
||||||
|
peer.setAutoLiquidityParams(liquidityOptions.autoLiquidity)
|
||||||
|
}
|
||||||
|
|
||||||
|
val server = embeddedServer(CIO, port = httpBindPort, host = httpBindIp,
|
||||||
|
configure = {
|
||||||
|
reuseAddress = true
|
||||||
|
},
|
||||||
|
module = {
|
||||||
|
Api(nodeParams, peer, eventsFlow, httpPassword, webHookUrl).run { module() }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val serverJob = scope.launch {
|
||||||
|
try {
|
||||||
|
server.start(wait = true)
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
if (t.cause?.message?.contains("Address already in use") == true) {
|
||||||
|
echo(t.cause?.message, err = true)
|
||||||
|
} else throw t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server.environment.monitor.subscribe(ServerReady) {
|
||||||
|
echo("listening on http://$httpBindIp:$httpBindPort")
|
||||||
|
}
|
||||||
|
server.environment.monitor.subscribe(ApplicationStopPreparing) {
|
||||||
|
echo(brightYellow("shutting down..."))
|
||||||
|
electrum.stop()
|
||||||
|
peer.disconnect()
|
||||||
|
server.stop()
|
||||||
|
listeners.cancel()
|
||||||
|
exitProcess(0)
|
||||||
|
}
|
||||||
|
server.environment.monitor.subscribe(ApplicationStopped) { echo(brightYellow("http server stopped")) }
|
||||||
|
|
||||||
|
runBlocking { serverJob.join() }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package fr.acinq.lightning.bin.conf
|
||||||
|
|
||||||
|
import okio.FileSystem
|
||||||
|
import okio.Path
|
||||||
|
|
||||||
|
fun readConfFile(confFile: Path): Map<String, String> = try {
|
||||||
|
buildMap {
|
||||||
|
if (FileSystem.SYSTEM.exists(confFile)) {
|
||||||
|
FileSystem.SYSTEM.read(confFile) {
|
||||||
|
while (true) {
|
||||||
|
val line = readUtf8Line() ?: break
|
||||||
|
line.split("=").run { put(first(), last()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
emptyMap()
|
||||||
|
}
|
79
src/commonMain/kotlin/fr/acinq/lightning/bin/conf/Lsp.kt
Normal file
79
src/commonMain/kotlin/fr/acinq/lightning/bin/conf/Lsp.kt
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
package fr.acinq.lightning.bin.conf
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.Chain
|
||||||
|
import fr.acinq.bitcoin.PublicKey
|
||||||
|
import fr.acinq.lightning.*
|
||||||
|
import fr.acinq.lightning.utils.msat
|
||||||
|
import fr.acinq.lightning.utils.sat
|
||||||
|
|
||||||
|
|
||||||
|
data class LSP(val walletParams: WalletParams, val swapInXpub: String) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private val trampolineFees = listOf(
|
||||||
|
TrampolineFees(
|
||||||
|
feeBase = 4.sat,
|
||||||
|
feeProportional = 4_000,
|
||||||
|
cltvExpiryDelta = CltvExpiryDelta(576)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private val invoiceDefaultRoutingFees = InvoiceDefaultRoutingFees(
|
||||||
|
feeBase = 1_000.msat,
|
||||||
|
feeProportional = 100,
|
||||||
|
cltvExpiryDelta = CltvExpiryDelta(144)
|
||||||
|
)
|
||||||
|
|
||||||
|
private val swapInParams = SwapInParams(
|
||||||
|
minConfirmations = DefaultSwapInParams.MinConfirmations,
|
||||||
|
maxConfirmations = DefaultSwapInParams.MaxConfirmations,
|
||||||
|
refundDelay = DefaultSwapInParams.RefundDelay,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun from(chain: Chain) = when (chain) {
|
||||||
|
is Chain.Mainnet -> LSP(
|
||||||
|
swapInXpub = "xpub69q3sDXXsLuHVbmTrhqmEqYqTTsXJKahdfawXaYuUt6muf1PbZBnvqzFcwiT8Abpc13hY8BFafakwpPbVkatg9egwiMjed1cRrPM19b2Ma7",
|
||||||
|
walletParams = WalletParams(
|
||||||
|
trampolineNode = NodeUri(PublicKey.fromHex("03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f"), "3.33.236.230", 9735),
|
||||||
|
trampolineFees,
|
||||||
|
invoiceDefaultRoutingFees,
|
||||||
|
swapInParams
|
||||||
|
)
|
||||||
|
)
|
||||||
|
is Chain.Testnet -> LSP(
|
||||||
|
swapInXpub = "tpubDAmCFB21J9ExKBRPDcVxSvGs9jtcf8U1wWWbS1xTYmnUsuUHPCoFdCnEGxLE3THSWcQE48GHJnyz8XPbYUivBMbLSMBifFd3G9KmafkM9og",
|
||||||
|
walletParams = WalletParams(
|
||||||
|
trampolineNode = NodeUri(PublicKey.fromHex("03933884aaf1d6b108397e5efe5c86bcf2d8ca8d2f700eda99db9214fc2712b134"), "13.248.222.197", 9735),
|
||||||
|
trampolineFees,
|
||||||
|
invoiceDefaultRoutingFees,
|
||||||
|
swapInParams
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else -> error("unsupported chain $chain")
|
||||||
|
}
|
||||||
|
|
||||||
|
// fun liquidityLeaseRate(amount: Satoshi): LiquidityAds.LeaseRate {
|
||||||
|
// // WARNING : THIS MUST BE KEPT IN SYNC WITH LSP OTHERWISE FUNDING REQUEST WILL BE REJECTED BY PHOENIX
|
||||||
|
// val fundingWeight = if (amount <= 100_000.sat) {
|
||||||
|
// 271 * 2 // 2-inputs (wpkh) / 0-change
|
||||||
|
// } else if (amount <= 250_000.sat) {
|
||||||
|
// 271 * 2 // 2-inputs (wpkh) / 0-change
|
||||||
|
// } else if (amount <= 500_000.sat) {
|
||||||
|
// 271 * 4 // 4-inputs (wpkh) / 0-change
|
||||||
|
// } else if (amount <= 1_000_000.sat) {
|
||||||
|
// 271 * 4 // 4-inputs (wpkh) / 0-change
|
||||||
|
// } else {
|
||||||
|
// 271 * 6 // 6-inputs (wpkh) / 0-change
|
||||||
|
// }
|
||||||
|
// return LiquidityAds.LeaseRate(
|
||||||
|
// leaseDuration = 0,
|
||||||
|
// fundingWeight = fundingWeight,
|
||||||
|
// leaseFeeProportional = 100, // 1%
|
||||||
|
// leaseFeeBase = 0.sat,
|
||||||
|
// maxRelayFeeProportional = 100,
|
||||||
|
// maxRelayFeeBase = 1_000.msat
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
24
src/commonMain/kotlin/fr/acinq/lightning/bin/conf/Seed.kt
Normal file
24
src/commonMain/kotlin/fr/acinq/lightning/bin/conf/Seed.kt
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package fr.acinq.lightning.bin.conf
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.ByteVector
|
||||||
|
import fr.acinq.bitcoin.MnemonicCode
|
||||||
|
import fr.acinq.lightning.Lightning.randomBytes
|
||||||
|
import fr.acinq.lightning.utils.toByteVector
|
||||||
|
import okio.FileSystem
|
||||||
|
import okio.Path
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return a pair with the seed and a boolean indicating whether the seed was newly generated
|
||||||
|
*/
|
||||||
|
fun getOrGenerateSeed(dir: Path): Pair<ByteVector, Boolean> {
|
||||||
|
val file = dir / "seed.dat"
|
||||||
|
val (mnemonics, new) = if (FileSystem.SYSTEM.exists(file)) {
|
||||||
|
FileSystem.SYSTEM.read(file) { readUtf8() } to false
|
||||||
|
} else {
|
||||||
|
val entropy = randomBytes(16)
|
||||||
|
val mnemonics = MnemonicCode.toMnemonics(entropy).joinToString(" ")
|
||||||
|
FileSystem.SYSTEM.write(file) { writeUtf8(mnemonics) }
|
||||||
|
mnemonics to true
|
||||||
|
}
|
||||||
|
return MnemonicCode.toSeed(mnemonics, "").toByteVector() to new
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
package fr.acinq.lightning.bin.db
|
||||||
|
|
||||||
|
import app.cash.sqldelight.db.SqlDriver
|
||||||
|
import fr.acinq.bitcoin.ByteVector32
|
||||||
|
import fr.acinq.lightning.CltvExpiry
|
||||||
|
import fr.acinq.lightning.channel.states.PersistedChannelState
|
||||||
|
import fr.acinq.lightning.db.ChannelsDb
|
||||||
|
import fr.acinq.lightning.serialization.Serialization
|
||||||
|
import fr.acinq.phoenix.db.ChannelsDatabase
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
internal class SqliteChannelsDb(private val driver: SqlDriver) : ChannelsDb {
|
||||||
|
|
||||||
|
private val database = ChannelsDatabase(driver)
|
||||||
|
private val queries = database.channelsDatabaseQueries
|
||||||
|
|
||||||
|
override suspend fun addOrUpdateChannel(state: PersistedChannelState) {
|
||||||
|
val channelId = state.channelId.toByteArray()
|
||||||
|
val data = Serialization.serialize(state)
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
queries.transaction {
|
||||||
|
queries.getChannel(channelId).executeAsOneOrNull()?.run {
|
||||||
|
queries.updateChannel(channel_id = this.channel_id, data_ = data)
|
||||||
|
} ?: run {
|
||||||
|
queries.insertChannel(channel_id = channelId, data_ = data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun removeChannel(channelId: ByteVector32) {
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
queries.deleteHtlcInfo(channel_id = channelId.toByteArray())
|
||||||
|
queries.closeLocalChannel(channel_id = channelId.toByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun listLocalChannels(): List<PersistedChannelState> = withContext(Dispatchers.Default) {
|
||||||
|
val bytes = queries.listLocalChannels().executeAsList()
|
||||||
|
bytes.mapNotNull {
|
||||||
|
when (val res = Serialization.deserialize(it)) {
|
||||||
|
is Serialization.DeserializationResult.Success -> res.state
|
||||||
|
is Serialization.DeserializationResult.UnknownVersion -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun addHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry) {
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
queries.insertHtlcInfo(
|
||||||
|
channel_id = channelId.toByteArray(),
|
||||||
|
commitment_number = commitmentNumber,
|
||||||
|
payment_hash = paymentHash.toByteArray(),
|
||||||
|
cltv_expiry = cltvExpiry.toLong())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun listHtlcInfos(channelId: ByteVector32, commitmentNumber: Long): List<Pair<ByteVector32, CltvExpiry>> {
|
||||||
|
return withContext(Dispatchers.Default) {
|
||||||
|
queries.listHtlcInfos(channel_id = channelId.toByteArray(), commitment_number = commitmentNumber, mapper = { payment_hash, cltv_expiry ->
|
||||||
|
ByteVector32(payment_hash) to CltvExpiry(cltv_expiry)
|
||||||
|
}).executeAsList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
driver.close()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,289 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2020 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package fr.acinq.lightning.bin.db
|
||||||
|
|
||||||
|
import app.cash.sqldelight.EnumColumnAdapter
|
||||||
|
import app.cash.sqldelight.db.SqlDriver
|
||||||
|
import fr.acinq.bitcoin.ByteVector32
|
||||||
|
import fr.acinq.bitcoin.Crypto
|
||||||
|
import fr.acinq.bitcoin.TxId
|
||||||
|
import fr.acinq.bitcoin.utils.Either
|
||||||
|
import fr.acinq.lightning.bin.db.payments.*
|
||||||
|
import fr.acinq.lightning.bin.db.payments.LinkTxToPaymentQueries
|
||||||
|
import fr.acinq.lightning.channel.ChannelException
|
||||||
|
import fr.acinq.lightning.db.*
|
||||||
|
import fr.acinq.lightning.logging.LoggerFactory
|
||||||
|
import fr.acinq.lightning.payment.FinalFailure
|
||||||
|
import fr.acinq.lightning.utils.*
|
||||||
|
import fr.acinq.lightning.wire.FailureMessage
|
||||||
|
import fr.acinq.phoenix.db.*
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class SqlitePaymentsDb(
|
||||||
|
loggerFactory: LoggerFactory,
|
||||||
|
private val driver: SqlDriver,
|
||||||
|
) : PaymentsDb {
|
||||||
|
|
||||||
|
private val log = loggerFactory.newLogger(this::class)
|
||||||
|
|
||||||
|
private val database = PaymentsDatabase(
|
||||||
|
driver = driver,
|
||||||
|
outgoing_payment_partsAdapter = Outgoing_payment_parts.Adapter(
|
||||||
|
part_routeAdapter = OutgoingQueries.hopDescAdapter,
|
||||||
|
part_status_typeAdapter = EnumColumnAdapter()
|
||||||
|
),
|
||||||
|
outgoing_paymentsAdapter = Outgoing_payments.Adapter(
|
||||||
|
status_typeAdapter = EnumColumnAdapter(),
|
||||||
|
details_typeAdapter = EnumColumnAdapter()
|
||||||
|
),
|
||||||
|
incoming_paymentsAdapter = Incoming_payments.Adapter(
|
||||||
|
origin_typeAdapter = EnumColumnAdapter(),
|
||||||
|
received_with_typeAdapter = EnumColumnAdapter()
|
||||||
|
),
|
||||||
|
outgoing_payment_closing_tx_partsAdapter = Outgoing_payment_closing_tx_parts.Adapter(
|
||||||
|
part_closing_info_typeAdapter = EnumColumnAdapter()
|
||||||
|
),
|
||||||
|
channel_close_outgoing_paymentsAdapter = Channel_close_outgoing_payments.Adapter(
|
||||||
|
closing_info_typeAdapter = EnumColumnAdapter()
|
||||||
|
),
|
||||||
|
inbound_liquidity_outgoing_paymentsAdapter = Inbound_liquidity_outgoing_payments.Adapter(
|
||||||
|
lease_typeAdapter = EnumColumnAdapter()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
internal val inQueries = IncomingQueries(database)
|
||||||
|
internal val outQueries = OutgoingQueries(database)
|
||||||
|
private val spliceOutQueries = SpliceOutgoingQueries(database)
|
||||||
|
private val channelCloseQueries = ChannelCloseOutgoingQueries(database)
|
||||||
|
private val cpfpQueries = SpliceCpfpOutgoingQueries(database)
|
||||||
|
private val linkTxToPaymentQueries = LinkTxToPaymentQueries(database)
|
||||||
|
private val inboundLiquidityQueries = InboundLiquidityQueries(database)
|
||||||
|
|
||||||
|
override suspend fun addOutgoingLightningParts(
|
||||||
|
parentId: UUID,
|
||||||
|
parts: List<LightningOutgoingPayment.Part>
|
||||||
|
) {
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
outQueries.addLightningParts(parentId, parts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun addOutgoingPayment(
|
||||||
|
outgoingPayment: OutgoingPayment
|
||||||
|
) {
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
database.transaction {
|
||||||
|
when (outgoingPayment) {
|
||||||
|
is LightningOutgoingPayment -> {
|
||||||
|
outQueries.addLightningOutgoingPayment(outgoingPayment)
|
||||||
|
}
|
||||||
|
is SpliceOutgoingPayment -> {
|
||||||
|
spliceOutQueries.addSpliceOutgoingPayment(outgoingPayment)
|
||||||
|
linkTxToPaymentQueries.linkTxToPayment(
|
||||||
|
txId = outgoingPayment.txId,
|
||||||
|
walletPaymentId = outgoingPayment.walletPaymentId()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is ChannelCloseOutgoingPayment -> {
|
||||||
|
channelCloseQueries.addChannelCloseOutgoingPayment(outgoingPayment)
|
||||||
|
linkTxToPaymentQueries.linkTxToPayment(
|
||||||
|
txId = outgoingPayment.txId,
|
||||||
|
walletPaymentId = outgoingPayment.walletPaymentId()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is SpliceCpfpOutgoingPayment -> {
|
||||||
|
cpfpQueries.addCpfpPayment(outgoingPayment)
|
||||||
|
linkTxToPaymentQueries.linkTxToPayment(outgoingPayment.txId, outgoingPayment.walletPaymentId())
|
||||||
|
}
|
||||||
|
is InboundLiquidityOutgoingPayment -> {
|
||||||
|
inboundLiquidityQueries.add(outgoingPayment)
|
||||||
|
linkTxToPaymentQueries.linkTxToPayment(outgoingPayment.txId, outgoingPayment.walletPaymentId())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun completeOutgoingPaymentOffchain(
|
||||||
|
id: UUID,
|
||||||
|
preimage: ByteVector32,
|
||||||
|
completedAt: Long
|
||||||
|
) {
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
outQueries.completePayment(id, LightningOutgoingPayment.Status.Completed.Succeeded.OffChain(preimage, completedAt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun completeOutgoingPaymentOffchain(
|
||||||
|
id: UUID,
|
||||||
|
finalFailure: FinalFailure,
|
||||||
|
completedAt: Long
|
||||||
|
) {
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
outQueries.completePayment(id, LightningOutgoingPayment.Status.Completed.Failed(finalFailure, completedAt))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun completeOutgoingLightningPart(
|
||||||
|
partId: UUID,
|
||||||
|
preimage: ByteVector32,
|
||||||
|
completedAt: Long
|
||||||
|
) {
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
outQueries.updateLightningPart(partId, preimage, completedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun completeOutgoingLightningPart(
|
||||||
|
partId: UUID,
|
||||||
|
failure: Either<ChannelException, FailureMessage>,
|
||||||
|
completedAt: Long
|
||||||
|
) {
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
outQueries.updateLightningPart(partId, failure, completedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getLightningOutgoingPayment(id: UUID): LightningOutgoingPayment? = withContext(Dispatchers.Default) {
|
||||||
|
outQueries.getPaymentStrict(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getLightningOutgoingPaymentFromPartId(partId: UUID): LightningOutgoingPayment? = withContext(Dispatchers.Default) {
|
||||||
|
outQueries.getPaymentFromPartId(partId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- list outgoing
|
||||||
|
|
||||||
|
override suspend fun listLightningOutgoingPayments(
|
||||||
|
paymentHash: ByteVector32
|
||||||
|
): List<LightningOutgoingPayment> = withContext(Dispatchers.Default) {
|
||||||
|
outQueries.listLightningOutgoingPayments(paymentHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- incoming payments
|
||||||
|
|
||||||
|
override suspend fun addIncomingPayment(
|
||||||
|
preimage: ByteVector32,
|
||||||
|
origin: IncomingPayment.Origin,
|
||||||
|
createdAt: Long
|
||||||
|
): IncomingPayment {
|
||||||
|
val paymentHash = Crypto.sha256(preimage).toByteVector32()
|
||||||
|
|
||||||
|
return withContext(Dispatchers.Default) {
|
||||||
|
database.transactionWithResult {
|
||||||
|
inQueries.addIncomingPayment(preimage, paymentHash, origin, createdAt)
|
||||||
|
inQueries.getIncomingPayment(paymentHash)!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun receivePayment(
|
||||||
|
paymentHash: ByteVector32,
|
||||||
|
receivedWith: List<IncomingPayment.ReceivedWith>,
|
||||||
|
receivedAt: Long
|
||||||
|
) {
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
database.transaction {
|
||||||
|
inQueries.receivePayment(paymentHash, receivedWith, receivedAt)
|
||||||
|
// if one received-with is on-chain, save the tx id to the db
|
||||||
|
receivedWith.filterIsInstance<IncomingPayment.ReceivedWith.OnChainIncomingPayment>().firstOrNull()?.let {
|
||||||
|
linkTxToPaymentQueries.linkTxToPayment(it.txId, WalletPaymentId.IncomingPaymentId(paymentHash))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun setLocked(txId: TxId) {
|
||||||
|
database.transaction {
|
||||||
|
val lockedAt = currentTimestampMillis()
|
||||||
|
linkTxToPaymentQueries.setLocked(txId, lockedAt)
|
||||||
|
linkTxToPaymentQueries.listWalletPaymentIdsForTx(txId).forEach { walletPaymentId ->
|
||||||
|
when (walletPaymentId) {
|
||||||
|
is WalletPaymentId.IncomingPaymentId -> {
|
||||||
|
inQueries.setLocked(walletPaymentId.paymentHash, lockedAt)
|
||||||
|
}
|
||||||
|
is WalletPaymentId.LightningOutgoingPaymentId -> {
|
||||||
|
// LN payments need not be locked
|
||||||
|
}
|
||||||
|
is WalletPaymentId.SpliceOutgoingPaymentId -> {
|
||||||
|
spliceOutQueries.setLocked(walletPaymentId.id, lockedAt)
|
||||||
|
}
|
||||||
|
is WalletPaymentId.ChannelCloseOutgoingPaymentId -> {
|
||||||
|
channelCloseQueries.setLocked(walletPaymentId.id, lockedAt)
|
||||||
|
}
|
||||||
|
is WalletPaymentId.SpliceCpfpOutgoingPaymentId -> {
|
||||||
|
cpfpQueries.setLocked(walletPaymentId.id, lockedAt)
|
||||||
|
}
|
||||||
|
is WalletPaymentId.InboundLiquidityOutgoingPaymentId -> {
|
||||||
|
inboundLiquidityQueries.setLocked(walletPaymentId.id, lockedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setConfirmed(txId: TxId) = withContext(Dispatchers.Default) {
|
||||||
|
database.transaction {
|
||||||
|
val confirmedAt = currentTimestampMillis()
|
||||||
|
linkTxToPaymentQueries.setConfirmed(txId, confirmedAt)
|
||||||
|
linkTxToPaymentQueries.listWalletPaymentIdsForTx(txId).forEach { walletPaymentId ->
|
||||||
|
when (walletPaymentId) {
|
||||||
|
is WalletPaymentId.IncomingPaymentId -> {
|
||||||
|
inQueries.setConfirmed(walletPaymentId.paymentHash, confirmedAt)
|
||||||
|
}
|
||||||
|
is WalletPaymentId.LightningOutgoingPaymentId -> {
|
||||||
|
// LN payments need not be confirmed
|
||||||
|
}
|
||||||
|
is WalletPaymentId.SpliceOutgoingPaymentId -> {
|
||||||
|
spliceOutQueries.setConfirmed(walletPaymentId.id, confirmedAt)
|
||||||
|
}
|
||||||
|
is WalletPaymentId.ChannelCloseOutgoingPaymentId -> {
|
||||||
|
channelCloseQueries.setConfirmed(walletPaymentId.id, confirmedAt)
|
||||||
|
}
|
||||||
|
is WalletPaymentId.SpliceCpfpOutgoingPaymentId -> {
|
||||||
|
cpfpQueries.setConfirmed(walletPaymentId.id, confirmedAt)
|
||||||
|
}
|
||||||
|
is WalletPaymentId.InboundLiquidityOutgoingPaymentId -> {
|
||||||
|
inboundLiquidityQueries.setConfirmed(walletPaymentId.id, confirmedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getIncomingPayment(
|
||||||
|
paymentHash: ByteVector32
|
||||||
|
): IncomingPayment? = withContext(Dispatchers.Default) {
|
||||||
|
inQueries.getIncomingPayment(paymentHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun listExpiredPayments(
|
||||||
|
fromCreatedAt: Long,
|
||||||
|
toCreatedAt: Long
|
||||||
|
): List<IncomingPayment> = withContext(Dispatchers.Default) {
|
||||||
|
inQueries.listExpiredPayments(fromCreatedAt, toCreatedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun removeIncomingPayment(
|
||||||
|
paymentHash: ByteVector32
|
||||||
|
): Boolean = withContext(Dispatchers.Default) {
|
||||||
|
inQueries.deleteIncomingPayment(paymentHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,119 @@
|
|||||||
|
package fr.acinq.lightning.bin.db
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.ByteVector32
|
||||||
|
import fr.acinq.lightning.db.*
|
||||||
|
import fr.acinq.lightning.utils.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class that helps to link an actual payment to a unique string. This is useful to store a reference
|
||||||
|
* to a payment in a single TEXT sql column.
|
||||||
|
*
|
||||||
|
* e.g.: incoming|b50ccb7e52ecc6f25b21eb23c2efdd1cfdb973ca12c7db9eef3d818dcc9b437c
|
||||||
|
* This is a unique identifier for an [IncomingPayment] with paymentHash=b50ccb7...b437c.
|
||||||
|
*
|
||||||
|
* It is common to reference these rows in other database tables via [dbType] or [dbId].
|
||||||
|
*
|
||||||
|
* @param dbType Long representing either incoming or outgoing/splice-outgoing/...
|
||||||
|
* @param dbId String representing the appropriate id for either table (payment hash or UUID).
|
||||||
|
*/
|
||||||
|
sealed class WalletPaymentId {
|
||||||
|
|
||||||
|
abstract val dbType: DbType
|
||||||
|
abstract val dbId: String
|
||||||
|
|
||||||
|
/** Use this to get a single (hashable) identifier for the row, for example within a hashmap or Cache. */
|
||||||
|
abstract val identifier: String
|
||||||
|
|
||||||
|
data class IncomingPaymentId(val paymentHash: ByteVector32) : WalletPaymentId() {
|
||||||
|
override val dbType: DbType = DbType.INCOMING
|
||||||
|
override val dbId: String = paymentHash.toHex()
|
||||||
|
override val identifier: String = "incoming|$dbId"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromString(id: String) = IncomingPaymentId(paymentHash = ByteVector32(id))
|
||||||
|
fun fromByteArray(id: ByteArray) = IncomingPaymentId(paymentHash = ByteVector32(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class LightningOutgoingPaymentId(val id: UUID) : WalletPaymentId() {
|
||||||
|
override val dbType: DbType = DbType.OUTGOING
|
||||||
|
override val dbId: String = id.toString()
|
||||||
|
override val identifier: String = "outgoing|$dbId"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromString(id: String) = LightningOutgoingPaymentId(id = UUID.fromString(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SpliceOutgoingPaymentId(val id: UUID) : WalletPaymentId() {
|
||||||
|
override val dbType: DbType = DbType.SPLICE_OUTGOING
|
||||||
|
override val dbId: String = id.toString()
|
||||||
|
override val identifier: String = "splice_outgoing|$dbId"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromString(id: String) = SpliceOutgoingPaymentId(id = UUID.fromString(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ChannelCloseOutgoingPaymentId(val id: UUID) : WalletPaymentId() {
|
||||||
|
override val dbType: DbType = DbType.CHANNEL_CLOSE_OUTGOING
|
||||||
|
override val dbId: String = id.toString()
|
||||||
|
override val identifier: String = "channel_close_outgoing|$dbId"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromString(id: String) = ChannelCloseOutgoingPaymentId(id = UUID.fromString(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class SpliceCpfpOutgoingPaymentId(val id: UUID) : WalletPaymentId() {
|
||||||
|
override val dbType: DbType = DbType.SPLICE_CPFP_OUTGOING
|
||||||
|
override val dbId: String = id.toString()
|
||||||
|
override val identifier: String = "splice_cpfp_outgoing|$dbId"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromString(id: String) = SpliceCpfpOutgoingPaymentId(id = UUID.fromString(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class InboundLiquidityOutgoingPaymentId(val id: UUID) : WalletPaymentId() {
|
||||||
|
override val dbType: DbType = DbType.INBOUND_LIQUIDITY_OUTGOING
|
||||||
|
override val dbId: String = id.toString()
|
||||||
|
override val identifier: String = "inbound_liquidity_outgoing|$dbId"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromString(id: String) = InboundLiquidityOutgoingPaymentId(id = UUID.fromString(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class DbType(val value: Long) {
|
||||||
|
INCOMING(1),
|
||||||
|
OUTGOING(2),
|
||||||
|
SPLICE_OUTGOING(3),
|
||||||
|
CHANNEL_CLOSE_OUTGOING(4),
|
||||||
|
SPLICE_CPFP_OUTGOING(5),
|
||||||
|
INBOUND_LIQUIDITY_OUTGOING(6),
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun create(type: Long, id: String): WalletPaymentId? {
|
||||||
|
return when (type) {
|
||||||
|
DbType.INCOMING.value -> IncomingPaymentId.fromString(id)
|
||||||
|
DbType.OUTGOING.value -> LightningOutgoingPaymentId.fromString(id)
|
||||||
|
DbType.SPLICE_OUTGOING.value -> SpliceOutgoingPaymentId.fromString(id)
|
||||||
|
DbType.CHANNEL_CLOSE_OUTGOING.value -> ChannelCloseOutgoingPaymentId.fromString(id)
|
||||||
|
DbType.SPLICE_CPFP_OUTGOING.value -> SpliceCpfpOutgoingPaymentId.fromString(id)
|
||||||
|
DbType.INBOUND_LIQUIDITY_OUTGOING.value -> InboundLiquidityOutgoingPaymentId.fromString(id)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun WalletPayment.walletPaymentId(): WalletPaymentId = when (this) {
|
||||||
|
is IncomingPayment -> WalletPaymentId.IncomingPaymentId(paymentHash = this.paymentHash)
|
||||||
|
is LightningOutgoingPayment -> WalletPaymentId.LightningOutgoingPaymentId(id = this.id)
|
||||||
|
is SpliceOutgoingPayment -> WalletPaymentId.SpliceOutgoingPaymentId(id = this.id)
|
||||||
|
is ChannelCloseOutgoingPayment -> WalletPaymentId.ChannelCloseOutgoingPaymentId(id = this.id)
|
||||||
|
is SpliceCpfpOutgoingPayment -> WalletPaymentId.SpliceCpfpOutgoingPaymentId(id = this.id)
|
||||||
|
is InboundLiquidityOutgoingPayment -> WalletPaymentId.InboundLiquidityOutgoingPaymentId(id = this.id)
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package fr.acinq.lightning.bin.db.payments
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.TxId
|
||||||
|
import fr.acinq.lightning.db.ChannelCloseOutgoingPayment
|
||||||
|
import fr.acinq.lightning.utils.UUID
|
||||||
|
import fr.acinq.lightning.utils.sat
|
||||||
|
import fr.acinq.lightning.utils.toByteVector32
|
||||||
|
import fr.acinq.phoenix.db.PaymentsDatabase
|
||||||
|
|
||||||
|
class ChannelCloseOutgoingQueries(val database: PaymentsDatabase) {
|
||||||
|
private val channelCloseQueries = database.channelCloseOutgoingPaymentsQueries
|
||||||
|
|
||||||
|
fun getChannelCloseOutgoingPayment(id: UUID): ChannelCloseOutgoingPayment? {
|
||||||
|
return channelCloseQueries.getChannelCloseOutgoing(id.toString(), Companion::mapChannelCloseOutgoingPayment).executeAsOneOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addChannelCloseOutgoingPayment(payment: ChannelCloseOutgoingPayment) {
|
||||||
|
val (closingInfoType, closingInfoBlob) = payment.mapClosingTypeToDb()
|
||||||
|
database.transaction {
|
||||||
|
channelCloseQueries.insertChannelCloseOutgoing(
|
||||||
|
id = payment.id.toString(),
|
||||||
|
recipient_amount_sat = payment.recipientAmount.sat,
|
||||||
|
address = payment.address,
|
||||||
|
is_default_address = if (payment.isSentToDefaultAddress) 1 else 0,
|
||||||
|
mining_fees_sat = payment.miningFees.sat,
|
||||||
|
tx_id = payment.txId.value.toByteArray(),
|
||||||
|
created_at = payment.createdAt,
|
||||||
|
confirmed_at = payment.confirmedAt,
|
||||||
|
locked_at = payment.lockedAt,
|
||||||
|
channel_id = payment.channelId.toByteArray(),
|
||||||
|
closing_info_type = closingInfoType,
|
||||||
|
closing_info_blob = closingInfoBlob,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setConfirmed(id: UUID, confirmedAt: Long) {
|
||||||
|
database.transaction {
|
||||||
|
channelCloseQueries.setConfirmed(confirmed_at = confirmedAt, id = id.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLocked(id: UUID, lockedAt: Long) {
|
||||||
|
database.transaction {
|
||||||
|
channelCloseQueries.setLocked(locked_at = lockedAt, id = id.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun mapChannelCloseOutgoingPayment(
|
||||||
|
id: String,
|
||||||
|
amount_sat: Long,
|
||||||
|
address: String,
|
||||||
|
is_default_address: Long,
|
||||||
|
mining_fees_sat: Long,
|
||||||
|
tx_id: ByteArray,
|
||||||
|
created_at: Long,
|
||||||
|
confirmed_at: Long?,
|
||||||
|
locked_at: Long?,
|
||||||
|
channel_id: ByteArray,
|
||||||
|
closing_info_type: OutgoingPartClosingInfoTypeVersion,
|
||||||
|
closing_info_blob: ByteArray
|
||||||
|
): ChannelCloseOutgoingPayment {
|
||||||
|
return ChannelCloseOutgoingPayment(
|
||||||
|
id = UUID.fromString(id),
|
||||||
|
recipientAmount = amount_sat.sat,
|
||||||
|
address = address,
|
||||||
|
isSentToDefaultAddress = is_default_address == 1L,
|
||||||
|
miningFees = mining_fees_sat.sat,
|
||||||
|
txId = TxId(tx_id),
|
||||||
|
createdAt = created_at,
|
||||||
|
confirmedAt = confirmed_at,
|
||||||
|
lockedAt = locked_at,
|
||||||
|
channelId = channel_id.toByteVector32(),
|
||||||
|
closingType = OutgoingPartClosingInfoData.deserialize(closing_info_type, closing_info_blob),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package fr.acinq.lightning.bin.db.payments
|
||||||
|
|
||||||
|
import io.ktor.utils.io.charsets.*
|
||||||
|
import io.ktor.utils.io.core.*
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.modules.SerializersModule
|
||||||
|
import kotlinx.serialization.modules.polymorphic
|
||||||
|
import kotlinx.serialization.modules.subclass
|
||||||
|
|
||||||
|
object DbTypesHelper {
|
||||||
|
/** Decode a byte array and apply a deserialization handler. */
|
||||||
|
fun <T> decodeBlob(blob: ByteArray, handler: (String, Json) -> T) = handler(String(bytes = blob, charset = Charsets.UTF_8), Json)
|
||||||
|
|
||||||
|
val module = SerializersModule {
|
||||||
|
polymorphic(IncomingReceivedWithData.Part::class) {
|
||||||
|
subclass(IncomingReceivedWithData.Part.Htlc.V0::class)
|
||||||
|
subclass(IncomingReceivedWithData.Part.NewChannel.V2::class)
|
||||||
|
subclass(IncomingReceivedWithData.Part.SpliceIn.V0::class)
|
||||||
|
subclass(IncomingReceivedWithData.Part.FeeCredit.V0::class)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val polymorphicFormat = Json { serializersModule = module }
|
||||||
|
}
|
@ -0,0 +1,103 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:UseSerializers(
|
||||||
|
ByteVectorSerializer::class,
|
||||||
|
ByteVector32Serializer::class,
|
||||||
|
ByteVector64Serializer::class,
|
||||||
|
SatoshiSerializer::class,
|
||||||
|
MilliSatoshiSerializer::class
|
||||||
|
)
|
||||||
|
|
||||||
|
package fr.acinq.lightning.bin.db.payments
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.ByteVector
|
||||||
|
import fr.acinq.bitcoin.ByteVector64
|
||||||
|
import fr.acinq.bitcoin.Satoshi
|
||||||
|
import fr.acinq.lightning.MilliSatoshi
|
||||||
|
import fr.acinq.lightning.db.InboundLiquidityOutgoingPayment
|
||||||
|
import fr.acinq.lightning.wire.LiquidityAds
|
||||||
|
import fr.acinq.lightning.bin.db.serializers.v1.ByteVector32Serializer
|
||||||
|
import fr.acinq.lightning.bin.db.serializers.v1.ByteVector64Serializer
|
||||||
|
import fr.acinq.lightning.bin.db.serializers.v1.ByteVectorSerializer
|
||||||
|
import fr.acinq.lightning.bin.db.serializers.v1.MilliSatoshiSerializer
|
||||||
|
import fr.acinq.lightning.bin.db.serializers.v1.SatoshiSerializer
|
||||||
|
import io.ktor.utils.io.charsets.Charsets
|
||||||
|
import io.ktor.utils.io.core.toByteArray
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.UseSerializers
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
enum class InboundLiquidityLeaseTypeVersion {
|
||||||
|
LEASE_V0,
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class InboundLiquidityLeaseData {
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class V0(
|
||||||
|
val amount: Satoshi,
|
||||||
|
val miningFees: Satoshi,
|
||||||
|
val serviceFee: Satoshi,
|
||||||
|
val sellerSig: ByteVector64,
|
||||||
|
val witnessFundingScript: ByteVector,
|
||||||
|
val witnessLeaseDuration: Int,
|
||||||
|
val witnessLeaseEnd: Int,
|
||||||
|
val witnessMaxRelayFeeProportional: Int,
|
||||||
|
val witnessMaxRelayFeeBase: MilliSatoshi
|
||||||
|
) : InboundLiquidityLeaseData()
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** Deserializes a json-encoded blob containing data for an [LiquidityAds.Lease] object. */
|
||||||
|
fun deserialize(
|
||||||
|
typeVersion: InboundLiquidityLeaseTypeVersion,
|
||||||
|
blob: ByteArray,
|
||||||
|
): LiquidityAds.Lease = DbTypesHelper.decodeBlob(blob) { json, format ->
|
||||||
|
when (typeVersion) {
|
||||||
|
InboundLiquidityLeaseTypeVersion.LEASE_V0 -> format.decodeFromString<V0>(json).let {
|
||||||
|
LiquidityAds.Lease(
|
||||||
|
amount = it.amount,
|
||||||
|
fees = LiquidityAds.LeaseFees(miningFee = it.miningFees, serviceFee = it.serviceFee),
|
||||||
|
sellerSig = it.sellerSig,
|
||||||
|
witness = LiquidityAds.LeaseWitness(
|
||||||
|
fundingScript = it.witnessFundingScript,
|
||||||
|
leaseDuration = it.witnessLeaseDuration,
|
||||||
|
leaseEnd = it.witnessLeaseEnd,
|
||||||
|
maxRelayFeeProportional = it.witnessMaxRelayFeeProportional,
|
||||||
|
maxRelayFeeBase = it.witnessMaxRelayFeeBase,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun InboundLiquidityOutgoingPayment.mapLeaseToDb() = InboundLiquidityLeaseTypeVersion.LEASE_V0 to
|
||||||
|
InboundLiquidityLeaseData.V0(
|
||||||
|
amount = lease.amount,
|
||||||
|
miningFees = lease.fees.miningFee,
|
||||||
|
serviceFee = lease.fees.serviceFee,
|
||||||
|
sellerSig = lease.sellerSig,
|
||||||
|
witnessFundingScript = lease.witness.fundingScript,
|
||||||
|
witnessLeaseDuration = lease.witness.leaseDuration,
|
||||||
|
witnessLeaseEnd = lease.witness.leaseEnd,
|
||||||
|
witnessMaxRelayFeeProportional = lease.witness.maxRelayFeeProportional,
|
||||||
|
witnessMaxRelayFeeBase = lease.witness.maxRelayFeeBase,
|
||||||
|
).let {
|
||||||
|
Json.encodeToString(it).toByteArray(Charsets.UTF_8)
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package fr.acinq.lightning.bin.db.payments
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.TxId
|
||||||
|
import fr.acinq.lightning.db.InboundLiquidityOutgoingPayment
|
||||||
|
import fr.acinq.lightning.utils.UUID
|
||||||
|
import fr.acinq.lightning.utils.sat
|
||||||
|
import fr.acinq.lightning.utils.toByteVector32
|
||||||
|
import fr.acinq.phoenix.db.PaymentsDatabase
|
||||||
|
|
||||||
|
class InboundLiquidityQueries(val database: PaymentsDatabase) {
|
||||||
|
private val queries = database.inboundLiquidityOutgoingQueries
|
||||||
|
|
||||||
|
fun add(payment: InboundLiquidityOutgoingPayment) {
|
||||||
|
database.transaction {
|
||||||
|
val (leaseType, leaseData) = payment.mapLeaseToDb()
|
||||||
|
queries.insert(
|
||||||
|
id = payment.id.toString(),
|
||||||
|
mining_fees_sat = payment.miningFees.sat,
|
||||||
|
channel_id = payment.channelId.toByteArray(),
|
||||||
|
tx_id = payment.txId.value.toByteArray(),
|
||||||
|
lease_type = leaseType,
|
||||||
|
lease_blob = leaseData,
|
||||||
|
created_at = payment.createdAt,
|
||||||
|
confirmed_at = payment.confirmedAt,
|
||||||
|
locked_at = payment.lockedAt,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(id: UUID): InboundLiquidityOutgoingPayment? {
|
||||||
|
return queries.get(id = id.toString(), mapper = Companion::mapPayment)
|
||||||
|
.executeAsOneOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setConfirmed(id: UUID, confirmedAt: Long) {
|
||||||
|
database.transaction {
|
||||||
|
queries.setConfirmed(confirmed_at = confirmedAt, id = id.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLocked(id: UUID, lockedAt: Long) {
|
||||||
|
database.transaction {
|
||||||
|
queries.setLocked(locked_at = lockedAt, id = id.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
fun mapPayment(
|
||||||
|
id: String,
|
||||||
|
mining_fees_sat: Long,
|
||||||
|
channel_id: ByteArray,
|
||||||
|
tx_id: ByteArray,
|
||||||
|
lease_type: InboundLiquidityLeaseTypeVersion,
|
||||||
|
lease_blob: ByteArray,
|
||||||
|
created_at: Long,
|
||||||
|
confirmed_at: Long?,
|
||||||
|
locked_at: Long?
|
||||||
|
): InboundLiquidityOutgoingPayment {
|
||||||
|
return InboundLiquidityOutgoingPayment(
|
||||||
|
id = UUID.fromString(id),
|
||||||
|
miningFees = mining_fees_sat.sat,
|
||||||
|
channelId = channel_id.toByteVector32(),
|
||||||
|
txId = TxId(tx_id),
|
||||||
|
lease = InboundLiquidityLeaseData.deserialize(lease_type, lease_blob),
|
||||||
|
createdAt = created_at,
|
||||||
|
confirmedAt = confirmed_at,
|
||||||
|
lockedAt = locked_at
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:UseSerializers(
|
||||||
|
OutpointSerializer::class,
|
||||||
|
ByteVector32Serializer::class,
|
||||||
|
)
|
||||||
|
|
||||||
|
package fr.acinq.lightning.bin.db.payments
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.ByteVector32
|
||||||
|
import fr.acinq.bitcoin.OutPoint
|
||||||
|
import fr.acinq.bitcoin.TxId
|
||||||
|
import fr.acinq.lightning.bin.db.payments.DbTypesHelper.decodeBlob
|
||||||
|
import fr.acinq.lightning.db.IncomingPayment
|
||||||
|
import fr.acinq.lightning.payment.Bolt11Invoice
|
||||||
|
import fr.acinq.lightning.bin.db.serializers.v1.ByteVector32Serializer
|
||||||
|
import fr.acinq.lightning.bin.db.serializers.v1.OutpointSerializer
|
||||||
|
import io.ktor.utils.io.charsets.*
|
||||||
|
import io.ktor.utils.io.core.*
|
||||||
|
import kotlinx.serialization.*
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
|
||||||
|
enum class IncomingOriginTypeVersion {
|
||||||
|
KEYSEND_V0,
|
||||||
|
INVOICE_V0,
|
||||||
|
SWAPIN_V0,
|
||||||
|
ONCHAIN_V0,
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class IncomingOriginData {
|
||||||
|
|
||||||
|
sealed class KeySend : IncomingOriginData() {
|
||||||
|
@Serializable
|
||||||
|
@SerialName("KEYSEND_V0")
|
||||||
|
object V0 : KeySend()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Invoice : IncomingOriginData() {
|
||||||
|
@Serializable
|
||||||
|
data class V0(val paymentRequest: String) : Invoice()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class SwapIn : IncomingOriginData() {
|
||||||
|
@Serializable
|
||||||
|
data class V0(val address: String?) : SwapIn()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class OnChain : IncomingOriginData() {
|
||||||
|
@Serializable
|
||||||
|
data class V0(@Serializable val txId: ByteVector32, val outpoints: List<@Serializable OutPoint>) : SwapIn()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun deserialize(typeVersion: IncomingOriginTypeVersion, blob: ByteArray): IncomingPayment.Origin = decodeBlob(blob) { json, format ->
|
||||||
|
when (typeVersion) {
|
||||||
|
IncomingOriginTypeVersion.KEYSEND_V0 -> IncomingPayment.Origin.KeySend
|
||||||
|
IncomingOriginTypeVersion.INVOICE_V0 -> format.decodeFromString<Invoice.V0>(json).let { IncomingPayment.Origin.Invoice(Bolt11Invoice.read(it.paymentRequest).get()) }
|
||||||
|
IncomingOriginTypeVersion.SWAPIN_V0 -> format.decodeFromString<SwapIn.V0>(json).let { IncomingPayment.Origin.SwapIn(it.address) }
|
||||||
|
IncomingOriginTypeVersion.ONCHAIN_V0 -> format.decodeFromString<OnChain.V0>(json).let {
|
||||||
|
IncomingPayment.Origin.OnChain(TxId(it.txId), it.outpoints.toSet())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun IncomingPayment.Origin.mapToDb(): Pair<IncomingOriginTypeVersion, ByteArray> = when (this) {
|
||||||
|
is IncomingPayment.Origin.KeySend -> IncomingOriginTypeVersion.KEYSEND_V0 to
|
||||||
|
Json.encodeToString(IncomingOriginData.KeySend.V0).toByteArray(Charsets.UTF_8)
|
||||||
|
is IncomingPayment.Origin.Invoice -> IncomingOriginTypeVersion.INVOICE_V0 to
|
||||||
|
Json.encodeToString(IncomingOriginData.Invoice.V0(paymentRequest.write())).toByteArray(Charsets.UTF_8)
|
||||||
|
is IncomingPayment.Origin.SwapIn -> IncomingOriginTypeVersion.SWAPIN_V0 to
|
||||||
|
Json.encodeToString(IncomingOriginData.SwapIn.V0(address)).toByteArray(Charsets.UTF_8)
|
||||||
|
is IncomingPayment.Origin.OnChain -> IncomingOriginTypeVersion.ONCHAIN_V0 to
|
||||||
|
Json.encodeToString(IncomingOriginData.OnChain.V0(txId.value, localInputs.toList())).toByteArray(Charsets.UTF_8)
|
||||||
|
}
|
@ -0,0 +1,201 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package fr.acinq.lightning.bin.db.payments
|
||||||
|
|
||||||
|
import app.cash.sqldelight.coroutines.asFlow
|
||||||
|
import app.cash.sqldelight.coroutines.mapToList
|
||||||
|
import fr.acinq.bitcoin.ByteVector32
|
||||||
|
import fr.acinq.bitcoin.byteVector32
|
||||||
|
import fr.acinq.lightning.db.IncomingPayment
|
||||||
|
import fr.acinq.lightning.utils.msat
|
||||||
|
import fr.acinq.phoenix.db.PaymentsDatabase
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.IO
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
class IncomingQueries(private val database: PaymentsDatabase) {
|
||||||
|
|
||||||
|
private val queries = database.incomingPaymentsQueries
|
||||||
|
|
||||||
|
fun addIncomingPayment(
|
||||||
|
preimage: ByteVector32,
|
||||||
|
paymentHash: ByteVector32,
|
||||||
|
origin: IncomingPayment.Origin,
|
||||||
|
createdAt: Long
|
||||||
|
) {
|
||||||
|
val (originType, originData) = origin.mapToDb()
|
||||||
|
queries.insert(
|
||||||
|
payment_hash = paymentHash.toByteArray(),
|
||||||
|
preimage = preimage.toByteArray(),
|
||||||
|
origin_type = originType,
|
||||||
|
origin_blob = originData,
|
||||||
|
created_at = createdAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun receivePayment(
|
||||||
|
paymentHash: ByteVector32,
|
||||||
|
receivedWith: List<IncomingPayment.ReceivedWith>,
|
||||||
|
receivedAt: Long
|
||||||
|
) {
|
||||||
|
database.transaction {
|
||||||
|
val paymentInDb = queries.get(
|
||||||
|
payment_hash = paymentHash.toByteArray(),
|
||||||
|
mapper = Companion::mapIncomingPayment
|
||||||
|
).executeAsOneOrNull() ?: throw IncomingPaymentNotFound(paymentHash)
|
||||||
|
val existingReceivedWith = paymentInDb.received?.receivedWith ?: emptySet()
|
||||||
|
val newReceivedWith = existingReceivedWith + receivedWith
|
||||||
|
val (receivedWithType, receivedWithBlob) = newReceivedWith.mapToDb() ?: (null to null)
|
||||||
|
queries.updateReceived(
|
||||||
|
received_at = receivedAt,
|
||||||
|
received_with_type = receivedWithType,
|
||||||
|
received_with_blob = receivedWithBlob,
|
||||||
|
payment_hash = paymentHash.toByteArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLocked(paymentHash: ByteVector32, lockedAt: Long) {
|
||||||
|
database.transaction {
|
||||||
|
val paymentInDb = queries.get(
|
||||||
|
payment_hash = paymentHash.toByteArray(),
|
||||||
|
mapper = Companion::mapIncomingPayment
|
||||||
|
).executeAsOneOrNull()
|
||||||
|
val newReceivedWith = paymentInDb?.received?.receivedWith?.map {
|
||||||
|
when (it) {
|
||||||
|
is IncomingPayment.ReceivedWith.NewChannel -> it.copy(lockedAt = lockedAt)
|
||||||
|
is IncomingPayment.ReceivedWith.SpliceIn -> it.copy(lockedAt = lockedAt)
|
||||||
|
else -> it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val (newReceivedWithType, newReceivedWithBlob) = newReceivedWith?.mapToDb()
|
||||||
|
?: (null to null)
|
||||||
|
queries.updateReceived(
|
||||||
|
// we override the previous received_at timestamp to trigger a refresh of the payment's cache data
|
||||||
|
// because the list-all query feeding the cache uses `received_at` for incoming payments
|
||||||
|
received_at = lockedAt,
|
||||||
|
received_with_type = newReceivedWithType,
|
||||||
|
received_with_blob = newReceivedWithBlob,
|
||||||
|
payment_hash = paymentHash.toByteArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setConfirmed(paymentHash: ByteVector32, confirmedAt: Long) {
|
||||||
|
database.transaction {
|
||||||
|
val paymentInDb = queries.get(
|
||||||
|
payment_hash = paymentHash.toByteArray(),
|
||||||
|
mapper = Companion::mapIncomingPayment
|
||||||
|
).executeAsOneOrNull()
|
||||||
|
val newReceivedWith = paymentInDb?.received?.receivedWith?.map {
|
||||||
|
when (it) {
|
||||||
|
is IncomingPayment.ReceivedWith.NewChannel -> it.copy(confirmedAt = confirmedAt)
|
||||||
|
is IncomingPayment.ReceivedWith.SpliceIn -> it.copy(confirmedAt = confirmedAt)
|
||||||
|
else -> it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val (newReceivedWithType, newReceivedWithBlob) = newReceivedWith?.mapToDb()
|
||||||
|
?: (null to null)
|
||||||
|
queries.updateReceived(
|
||||||
|
received_at = paymentInDb?.received?.receivedAt,
|
||||||
|
received_with_type = newReceivedWithType,
|
||||||
|
received_with_blob = newReceivedWithBlob,
|
||||||
|
payment_hash = paymentHash.toByteArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getIncomingPayment(paymentHash: ByteVector32): IncomingPayment? {
|
||||||
|
return queries.get(payment_hash = paymentHash.toByteArray(), Companion::mapIncomingPayment).executeAsOneOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOldestReceivedDate(): Long? {
|
||||||
|
return queries.getOldestReceivedDate().executeAsOneOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun listAllNotConfirmed(): Flow<List<IncomingPayment>> {
|
||||||
|
return queries.listAllNotConfirmed(Companion::mapIncomingPayment).asFlow().mapToList(Dispatchers.IO)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun listExpiredPayments(fromCreatedAt: Long, toCreatedAt: Long): List<IncomingPayment> {
|
||||||
|
return queries.listAllWithin(fromCreatedAt, toCreatedAt, Companion::mapIncomingPayment).executeAsList().filter {
|
||||||
|
it.received == null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Try to delete an incoming payment ; return true if an element was deleted, false otherwise. */
|
||||||
|
fun deleteIncomingPayment(paymentHash: ByteVector32): Boolean {
|
||||||
|
return database.transactionWithResult {
|
||||||
|
queries.delete(payment_hash = paymentHash.toByteArray())
|
||||||
|
queries.changes().executeAsOne() != 0L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun mapIncomingPayment(
|
||||||
|
@Suppress("UNUSED_PARAMETER") payment_hash: ByteArray,
|
||||||
|
preimage: ByteArray,
|
||||||
|
created_at: Long,
|
||||||
|
origin_type: IncomingOriginTypeVersion,
|
||||||
|
origin_blob: ByteArray,
|
||||||
|
@Suppress("UNUSED_PARAMETER") received_amount_msat: Long?,
|
||||||
|
received_at: Long?,
|
||||||
|
received_with_type: IncomingReceivedWithTypeVersion?,
|
||||||
|
received_with_blob: ByteArray?,
|
||||||
|
): IncomingPayment {
|
||||||
|
return IncomingPayment(
|
||||||
|
preimage = ByteVector32(preimage),
|
||||||
|
origin = IncomingOriginData.deserialize(origin_type, origin_blob),
|
||||||
|
received = mapIncomingReceived(received_at, received_with_type, received_with_blob),
|
||||||
|
createdAt = created_at
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapIncomingReceived(
|
||||||
|
received_at: Long?,
|
||||||
|
received_with_type: IncomingReceivedWithTypeVersion?,
|
||||||
|
received_with_blob: ByteArray?,
|
||||||
|
): IncomingPayment.Received? {
|
||||||
|
return when {
|
||||||
|
received_at == null && received_with_type == null && received_with_blob == null -> null
|
||||||
|
received_at != null && received_with_type != null && received_with_blob != null -> {
|
||||||
|
IncomingPayment.Received(
|
||||||
|
receivedWith = IncomingReceivedWithData.deserialize(received_with_type, received_with_blob),
|
||||||
|
receivedAt = received_at
|
||||||
|
)
|
||||||
|
}
|
||||||
|
received_at != null -> {
|
||||||
|
IncomingPayment.Received(
|
||||||
|
receivedWith = emptyList(),
|
||||||
|
receivedAt = received_at
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> throw UnreadableIncomingReceivedWith(received_at, received_with_type, received_with_blob)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapTxIdPaymentHash(
|
||||||
|
tx_id: ByteArray,
|
||||||
|
payment_hash: ByteArray
|
||||||
|
): Pair<ByteVector32, ByteVector32> {
|
||||||
|
return tx_id.byteVector32() to payment_hash.byteVector32()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class IncomingPaymentNotFound(paymentHash: ByteVector32) : RuntimeException("missing payment for payment_hash=$paymentHash")
|
||||||
|
class UnreadableIncomingReceivedWith(receivedAt: Long?, receivedWithTypeVersion: IncomingReceivedWithTypeVersion?, receivedWithBlob: ByteArray?) :
|
||||||
|
RuntimeException("unreadable received with data [ receivedAt=$receivedAt, receivedWithTypeVersion=$receivedWithTypeVersion, receivedWithBlob=$receivedWithBlob ]")
|
@ -0,0 +1,169 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:UseSerializers(
|
||||||
|
SatoshiSerializer::class,
|
||||||
|
MilliSatoshiSerializer::class,
|
||||||
|
ByteVector32Serializer::class,
|
||||||
|
UUIDSerializer::class,
|
||||||
|
)
|
||||||
|
|
||||||
|
package fr.acinq.lightning.bin.db.payments
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.ByteVector32
|
||||||
|
import fr.acinq.bitcoin.Satoshi
|
||||||
|
import fr.acinq.bitcoin.TxId
|
||||||
|
import fr.acinq.lightning.MilliSatoshi
|
||||||
|
import fr.acinq.lightning.db.IncomingPayment
|
||||||
|
import fr.acinq.lightning.bin.db.serializers.v1.ByteVector32Serializer
|
||||||
|
import fr.acinq.lightning.bin.db.serializers.v1.MilliSatoshiSerializer
|
||||||
|
import fr.acinq.lightning.bin.db.serializers.v1.UUIDSerializer
|
||||||
|
import fr.acinq.lightning.bin.db.serializers.v1.SatoshiSerializer
|
||||||
|
import io.ktor.utils.io.charsets.*
|
||||||
|
import io.ktor.utils.io.core.*
|
||||||
|
import kotlinx.serialization.*
|
||||||
|
import kotlinx.serialization.builtins.SetSerializer
|
||||||
|
|
||||||
|
|
||||||
|
enum class IncomingReceivedWithTypeVersion {
|
||||||
|
MULTIPARTS_V1,
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class IncomingReceivedWithData {
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed class Part : IncomingReceivedWithData() {
|
||||||
|
sealed class Htlc : Part() {
|
||||||
|
@Serializable
|
||||||
|
data class V0(
|
||||||
|
@Serializable val amount: MilliSatoshi,
|
||||||
|
@Serializable val channelId: ByteVector32,
|
||||||
|
val htlcId: Long
|
||||||
|
) : Htlc()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class NewChannel : Part() {
|
||||||
|
/** V2 supports dual funding. New fields: service/miningFees, channel id, funding tx id, and the confirmation/lock timestamps. Id is removed. */
|
||||||
|
@Serializable
|
||||||
|
data class V2(
|
||||||
|
@Serializable val amount: MilliSatoshi,
|
||||||
|
@Serializable val serviceFee: MilliSatoshi,
|
||||||
|
@Serializable val miningFee: Satoshi,
|
||||||
|
@Serializable val channelId: ByteVector32,
|
||||||
|
@Serializable val txId: ByteVector32,
|
||||||
|
@Serializable val confirmedAt: Long?,
|
||||||
|
@Serializable val lockedAt: Long?,
|
||||||
|
) : NewChannel()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class SpliceIn : Part() {
|
||||||
|
@Serializable
|
||||||
|
data class V0(
|
||||||
|
@Serializable val amount: MilliSatoshi,
|
||||||
|
@Serializable val serviceFee: MilliSatoshi,
|
||||||
|
@Serializable val miningFee: Satoshi,
|
||||||
|
@Serializable val channelId: ByteVector32,
|
||||||
|
@Serializable val txId: ByteVector32,
|
||||||
|
@Serializable val confirmedAt: Long?,
|
||||||
|
@Serializable val lockedAt: Long?,
|
||||||
|
) : SpliceIn()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class FeeCredit : Part() {
|
||||||
|
@Serializable
|
||||||
|
data class V0(
|
||||||
|
val amount: MilliSatoshi
|
||||||
|
) : FeeCredit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** Deserializes a received-with blob from the database using the given [typeVersion]. */
|
||||||
|
fun deserialize(
|
||||||
|
typeVersion: IncomingReceivedWithTypeVersion,
|
||||||
|
blob: ByteArray,
|
||||||
|
): List<IncomingPayment.ReceivedWith> = DbTypesHelper.decodeBlob(blob) { json, _ ->
|
||||||
|
when (typeVersion) {
|
||||||
|
IncomingReceivedWithTypeVersion.MULTIPARTS_V1 -> DbTypesHelper.polymorphicFormat.decodeFromString(SetSerializer(PolymorphicSerializer(Part::class)), json).map {
|
||||||
|
when (it) {
|
||||||
|
is Part.Htlc.V0 -> IncomingPayment.ReceivedWith.LightningPayment(
|
||||||
|
amount = it.amount,
|
||||||
|
channelId = it.channelId,
|
||||||
|
htlcId = it.htlcId
|
||||||
|
)
|
||||||
|
is Part.NewChannel.V2 -> IncomingPayment.ReceivedWith.NewChannel(
|
||||||
|
amount = it.amount,
|
||||||
|
serviceFee = it.serviceFee,
|
||||||
|
miningFee = it.miningFee,
|
||||||
|
channelId = it.channelId,
|
||||||
|
txId = TxId(it.txId),
|
||||||
|
confirmedAt = it.confirmedAt,
|
||||||
|
lockedAt = it.lockedAt,
|
||||||
|
)
|
||||||
|
is Part.SpliceIn.V0 -> IncomingPayment.ReceivedWith.SpliceIn(
|
||||||
|
amount = it.amount,
|
||||||
|
serviceFee = it.serviceFee,
|
||||||
|
miningFee = it.miningFee,
|
||||||
|
channelId = it.channelId,
|
||||||
|
txId = TxId(it.txId),
|
||||||
|
confirmedAt = it.confirmedAt,
|
||||||
|
lockedAt = it.lockedAt,
|
||||||
|
)
|
||||||
|
is Part.FeeCredit.V0 -> IncomingPayment.ReceivedWith.FeeCreditPayment(
|
||||||
|
amount = it.amount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Only serialize received_with into the [IncomingReceivedWithTypeVersion.MULTIPARTS_V1] type. */
|
||||||
|
fun List<IncomingPayment.ReceivedWith>.mapToDb(): Pair<IncomingReceivedWithTypeVersion, ByteArray>? = map {
|
||||||
|
when (it) {
|
||||||
|
is IncomingPayment.ReceivedWith.LightningPayment -> IncomingReceivedWithData.Part.Htlc.V0(
|
||||||
|
amount = it.amount,
|
||||||
|
channelId = it.channelId,
|
||||||
|
htlcId = it.htlcId
|
||||||
|
)
|
||||||
|
is IncomingPayment.ReceivedWith.NewChannel -> IncomingReceivedWithData.Part.NewChannel.V2(
|
||||||
|
amount = it.amount,
|
||||||
|
serviceFee = it.serviceFee,
|
||||||
|
miningFee = it.miningFee,
|
||||||
|
channelId = it.channelId,
|
||||||
|
txId = it.txId.value,
|
||||||
|
confirmedAt = it.confirmedAt,
|
||||||
|
lockedAt = it.lockedAt,
|
||||||
|
)
|
||||||
|
is IncomingPayment.ReceivedWith.SpliceIn -> IncomingReceivedWithData.Part.SpliceIn.V0(
|
||||||
|
amount = it.amount,
|
||||||
|
serviceFee = it.serviceFee,
|
||||||
|
miningFee = it.miningFee,
|
||||||
|
channelId = it.channelId,
|
||||||
|
txId = it.txId.value,
|
||||||
|
confirmedAt = it.confirmedAt,
|
||||||
|
lockedAt = it.lockedAt,
|
||||||
|
)
|
||||||
|
is IncomingPayment.ReceivedWith.FeeCreditPayment -> IncomingReceivedWithData.Part.FeeCredit.V0(
|
||||||
|
amount = it.amount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.takeIf { it.isNotEmpty() }?.toSet()?.let {
|
||||||
|
IncomingReceivedWithTypeVersion.MULTIPARTS_V1 to DbTypesHelper.polymorphicFormat.encodeToString(
|
||||||
|
SetSerializer(PolymorphicSerializer(IncomingReceivedWithData.Part::class)), it
|
||||||
|
).toByteArray(Charsets.UTF_8)
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package fr.acinq.lightning.bin.db.payments
|
||||||
|
|
||||||
|
import app.cash.sqldelight.coroutines.asFlow
|
||||||
|
import app.cash.sqldelight.coroutines.mapToList
|
||||||
|
import fr.acinq.bitcoin.TxId
|
||||||
|
import fr.acinq.lightning.bin.db.WalletPaymentId
|
||||||
|
import fr.acinq.phoenix.db.PaymentsDatabase
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.IO
|
||||||
|
import kotlinx.coroutines.flow.*
|
||||||
|
|
||||||
|
class LinkTxToPaymentQueries(val database: PaymentsDatabase) {
|
||||||
|
private val linkTxQueries = database.linkTxToPaymentQueries
|
||||||
|
|
||||||
|
fun listUnconfirmedTxs(): Flow<List<ByteArray>> {
|
||||||
|
return linkTxQueries.listUnconfirmed().asFlow().mapToList(Dispatchers.IO)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun listWalletPaymentIdsForTx(txId: TxId): List<WalletPaymentId> {
|
||||||
|
return linkTxQueries.getPaymentIdForTx(tx_id = txId.value.toByteArray()).executeAsList()
|
||||||
|
.mapNotNull { WalletPaymentId.create(it.type, it.id) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun linkTxToPayment(txId: TxId, walletPaymentId: WalletPaymentId) {
|
||||||
|
linkTxQueries.linkTxToPayment(tx_id = txId.value.toByteArray(), type = walletPaymentId.dbType.value, id = walletPaymentId.dbId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setConfirmed(txId: TxId, confirmedAt: Long) {
|
||||||
|
linkTxQueries.setConfirmed(tx_id = txId.value.toByteArray(), confirmed_at = confirmedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLocked(txId: TxId, lockedAt: Long) {
|
||||||
|
linkTxQueries.setLocked(tx_id = txId.value.toByteArray(), locked_at = lockedAt)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:UseSerializers(
|
||||||
|
SatoshiSerializer::class,
|
||||||
|
ByteVector32Serializer::class,
|
||||||
|
)
|
||||||
|
|
||||||
|
package fr.acinq.lightning.bin.db.payments
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.ByteVector32
|
||||||
|
import fr.acinq.bitcoin.Satoshi
|
||||||
|
import fr.acinq.lightning.bin.db.serializers.v1.ByteVector32Serializer
|
||||||
|
import fr.acinq.lightning.bin.db.serializers.v1.SatoshiSerializer
|
||||||
|
import fr.acinq.lightning.db.LightningOutgoingPayment
|
||||||
|
import fr.acinq.lightning.payment.Bolt11Invoice
|
||||||
|
import io.ktor.utils.io.charsets.*
|
||||||
|
import io.ktor.utils.io.core.*
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.UseSerializers
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
|
||||||
|
enum class OutgoingDetailsTypeVersion {
|
||||||
|
NORMAL_V0,
|
||||||
|
KEYSEND_V0,
|
||||||
|
SWAPOUT_V0,
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class OutgoingDetailsData {
|
||||||
|
|
||||||
|
sealed class Normal : OutgoingDetailsData() {
|
||||||
|
@Serializable
|
||||||
|
data class V0(val paymentRequest: String) : Normal()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class KeySend : OutgoingDetailsData() {
|
||||||
|
@Serializable
|
||||||
|
data class V0(@Serializable val preimage: ByteVector32) : KeySend()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class SwapOut : OutgoingDetailsData() {
|
||||||
|
@Serializable
|
||||||
|
data class V0(val address: String, val paymentRequest: String, @Serializable val swapOutFee: Satoshi) : SwapOut()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/** Deserialize the details of an outgoing payment. Return null if the details is for a legacy channel closing payment (see [deserializeLegacyClosingDetails]). */
|
||||||
|
fun deserialize(typeVersion: OutgoingDetailsTypeVersion, blob: ByteArray): LightningOutgoingPayment.Details? = DbTypesHelper.decodeBlob(blob) { json, format ->
|
||||||
|
when (typeVersion) {
|
||||||
|
OutgoingDetailsTypeVersion.NORMAL_V0 -> format.decodeFromString<Normal.V0>(json).let { LightningOutgoingPayment.Details.Normal(Bolt11Invoice.read(it.paymentRequest).get()) }
|
||||||
|
OutgoingDetailsTypeVersion.KEYSEND_V0 -> format.decodeFromString<KeySend.V0>(json).let { LightningOutgoingPayment.Details.KeySend(it.preimage) }
|
||||||
|
OutgoingDetailsTypeVersion.SWAPOUT_V0 -> format.decodeFromString<SwapOut.V0>(json).let { LightningOutgoingPayment.Details.SwapOut(it.address, Bolt11Invoice.read(it.paymentRequest).get(), it.swapOutFee) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LightningOutgoingPayment.Details.mapToDb(): Pair<OutgoingDetailsTypeVersion, ByteArray> = when (this) {
|
||||||
|
is LightningOutgoingPayment.Details.Normal -> OutgoingDetailsTypeVersion.NORMAL_V0 to
|
||||||
|
Json.encodeToString(OutgoingDetailsData.Normal.V0(paymentRequest.write())).toByteArray(Charsets.UTF_8)
|
||||||
|
is LightningOutgoingPayment.Details.KeySend -> OutgoingDetailsTypeVersion.KEYSEND_V0 to
|
||||||
|
Json.encodeToString(OutgoingDetailsData.KeySend.V0(preimage)).toByteArray(Charsets.UTF_8)
|
||||||
|
is LightningOutgoingPayment.Details.SwapOut -> OutgoingDetailsTypeVersion.SWAPOUT_V0 to
|
||||||
|
Json.encodeToString(OutgoingDetailsData.SwapOut.V0(address, paymentRequest.write(), swapOutFee)).toByteArray(Charsets.UTF_8)
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package fr.acinq.lightning.bin.db.payments
|
||||||
|
|
||||||
|
import fr.acinq.lightning.db.ChannelCloseOutgoingPayment
|
||||||
|
import fr.acinq.lightning.db.ChannelClosingType
|
||||||
|
import io.ktor.utils.io.charsets.*
|
||||||
|
import io.ktor.utils.io.core.*
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
|
||||||
|
enum class OutgoingPartClosingInfoTypeVersion {
|
||||||
|
// basic type, containing only a [ChannelClosingType] field
|
||||||
|
CLOSING_INFO_V0,
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class OutgoingPartClosingInfoData {
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class V0(val closingType: ChannelClosingType)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun deserialize(typeVersion: OutgoingPartClosingInfoTypeVersion, blob: ByteArray): ChannelClosingType = DbTypesHelper.decodeBlob(blob) { json, format ->
|
||||||
|
when (typeVersion) {
|
||||||
|
OutgoingPartClosingInfoTypeVersion.CLOSING_INFO_V0 -> format.decodeFromString<V0>(json).closingType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ChannelCloseOutgoingPayment.mapClosingTypeToDb() = OutgoingPartClosingInfoTypeVersion.CLOSING_INFO_V0 to
|
||||||
|
Json.encodeToString(OutgoingPartClosingInfoData.V0(this.closingType)).toByteArray(Charsets.UTF_8)
|
@ -0,0 +1,72 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:UseSerializers(
|
||||||
|
ByteVector32Serializer::class,
|
||||||
|
)
|
||||||
|
|
||||||
|
package fr.acinq.lightning.bin.db.payments
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.ByteVector32
|
||||||
|
import fr.acinq.lightning.bin.db.serializers.v1.ByteVector32Serializer
|
||||||
|
import fr.acinq.lightning.db.LightningOutgoingPayment
|
||||||
|
import io.ktor.utils.io.charsets.*
|
||||||
|
import io.ktor.utils.io.core.*
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.UseSerializers
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
|
||||||
|
enum class OutgoingPartStatusTypeVersion {
|
||||||
|
SUCCEEDED_V0,
|
||||||
|
FAILED_V0,
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class OutgoingPartStatusData {
|
||||||
|
|
||||||
|
sealed class Succeeded : OutgoingPartStatusData() {
|
||||||
|
@Serializable
|
||||||
|
data class V0(@Serializable val preimage: ByteVector32) : Succeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Failed : OutgoingPartStatusData() {
|
||||||
|
@Serializable
|
||||||
|
data class V0(val remoteFailureCode: Int?, val details: String) : Failed()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun deserialize(
|
||||||
|
typeVersion: OutgoingPartStatusTypeVersion,
|
||||||
|
blob: ByteArray, completedAt: Long
|
||||||
|
): LightningOutgoingPayment.Part.Status = DbTypesHelper.decodeBlob(blob) { json, format ->
|
||||||
|
when (typeVersion) {
|
||||||
|
OutgoingPartStatusTypeVersion.SUCCEEDED_V0 -> format.decodeFromString<Succeeded.V0>(json).let {
|
||||||
|
LightningOutgoingPayment.Part.Status.Succeeded(it.preimage, completedAt)
|
||||||
|
}
|
||||||
|
OutgoingPartStatusTypeVersion.FAILED_V0 -> format.decodeFromString<Failed.V0>(json).let {
|
||||||
|
LightningOutgoingPayment.Part.Status.Failed(it.remoteFailureCode, it.details, completedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LightningOutgoingPayment.Part.Status.Succeeded.mapToDb() = OutgoingPartStatusTypeVersion.SUCCEEDED_V0 to
|
||||||
|
Json.encodeToString(OutgoingPartStatusData.Succeeded.V0(preimage)).toByteArray(Charsets.UTF_8)
|
||||||
|
|
||||||
|
fun LightningOutgoingPayment.Part.Status.Failed.mapToDb() = OutgoingPartStatusTypeVersion.FAILED_V0 to
|
||||||
|
Json.encodeToString(OutgoingPartStatusData.Failed.V0(remoteFailureCode, details)).toByteArray(Charsets.UTF_8)
|
@ -0,0 +1,373 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package fr.acinq.lightning.bin.db.payments
|
||||||
|
|
||||||
|
import app.cash.sqldelight.ColumnAdapter
|
||||||
|
import fr.acinq.bitcoin.ByteVector32
|
||||||
|
import fr.acinq.bitcoin.PublicKey
|
||||||
|
import fr.acinq.bitcoin.utils.Either
|
||||||
|
import fr.acinq.lightning.MilliSatoshi
|
||||||
|
import fr.acinq.lightning.ShortChannelId
|
||||||
|
import fr.acinq.lightning.channel.ChannelException
|
||||||
|
import fr.acinq.lightning.db.ChannelCloseOutgoingPayment
|
||||||
|
import fr.acinq.lightning.db.HopDesc
|
||||||
|
import fr.acinq.lightning.db.LightningOutgoingPayment
|
||||||
|
import fr.acinq.lightning.db.OutgoingPayment
|
||||||
|
import fr.acinq.lightning.payment.OutgoingPaymentFailure
|
||||||
|
import fr.acinq.lightning.utils.*
|
||||||
|
import fr.acinq.lightning.wire.FailureMessage
|
||||||
|
import fr.acinq.phoenix.db.PaymentsDatabase
|
||||||
|
import fr.acinq.secp256k1.Hex
|
||||||
|
|
||||||
|
class OutgoingQueries(val database: PaymentsDatabase) {
|
||||||
|
|
||||||
|
private val queries = database.outgoingPaymentsQueries
|
||||||
|
|
||||||
|
fun addLightningParts(parentId: UUID, parts: List<LightningOutgoingPayment.Part>) {
|
||||||
|
if (parts.isEmpty()) return
|
||||||
|
database.transaction {
|
||||||
|
parts.map {
|
||||||
|
// This will throw an exception if the sqlite foreign-key-constraint is violated.
|
||||||
|
queries.insertLightningPart(
|
||||||
|
part_id = it.id.toString(),
|
||||||
|
part_parent_id = parentId.toString(),
|
||||||
|
part_amount_msat = it.amount.msat,
|
||||||
|
part_route = it.route,
|
||||||
|
part_created_at = it.createdAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addLightningOutgoingPayment(payment: LightningOutgoingPayment) {
|
||||||
|
val (detailsTypeVersion, detailsData) = payment.details.mapToDb()
|
||||||
|
database.transaction(noEnclosing = false) {
|
||||||
|
queries.insertPayment(
|
||||||
|
id = payment.id.toString(),
|
||||||
|
recipient_amount_msat = payment.recipientAmount.msat,
|
||||||
|
recipient_node_id = payment.recipient.toString(),
|
||||||
|
payment_hash = payment.details.paymentHash.toByteArray(),
|
||||||
|
created_at = payment.createdAt,
|
||||||
|
details_type = detailsTypeVersion,
|
||||||
|
details_blob = detailsData
|
||||||
|
)
|
||||||
|
payment.parts.map {
|
||||||
|
queries.insertLightningPart(
|
||||||
|
part_id = it.id.toString(),
|
||||||
|
part_parent_id = payment.id.toString(),
|
||||||
|
part_amount_msat = it.amount.msat,
|
||||||
|
part_route = it.route,
|
||||||
|
part_created_at = it.createdAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun completePayment(id: UUID, completed: LightningOutgoingPayment.Status.Completed): Boolean {
|
||||||
|
var result = true
|
||||||
|
database.transaction {
|
||||||
|
val (statusType, statusBlob) = completed.mapToDb()
|
||||||
|
queries.updatePayment(
|
||||||
|
id = id.toString(),
|
||||||
|
completed_at = completed.completedAt,
|
||||||
|
status_type = statusType,
|
||||||
|
status_blob = statusBlob
|
||||||
|
)
|
||||||
|
if (queries.changes().executeAsOne() != 1L) {
|
||||||
|
result = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateLightningPart(
|
||||||
|
partId: UUID,
|
||||||
|
preimage: ByteVector32,
|
||||||
|
completedAt: Long
|
||||||
|
): Boolean {
|
||||||
|
var result = true
|
||||||
|
val (statusTypeVersion, statusData) = LightningOutgoingPayment.Part.Status.Succeeded(preimage).mapToDb()
|
||||||
|
database.transaction {
|
||||||
|
queries.updateLightningPart(
|
||||||
|
part_id = partId.toString(),
|
||||||
|
part_status_type = statusTypeVersion,
|
||||||
|
part_status_blob = statusData,
|
||||||
|
part_completed_at = completedAt
|
||||||
|
)
|
||||||
|
if (queries.changes().executeAsOne() != 1L) {
|
||||||
|
result = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateLightningPart(
|
||||||
|
partId: UUID,
|
||||||
|
failure: Either<ChannelException, FailureMessage>,
|
||||||
|
completedAt: Long
|
||||||
|
): Boolean {
|
||||||
|
var result = true
|
||||||
|
val (statusTypeVersion, statusData) = OutgoingPaymentFailure.convertFailure(failure).mapToDb()
|
||||||
|
database.transaction {
|
||||||
|
queries.updateLightningPart(
|
||||||
|
part_id = partId.toString(),
|
||||||
|
part_status_type = statusTypeVersion,
|
||||||
|
part_status_blob = statusData,
|
||||||
|
part_completed_at = completedAt
|
||||||
|
)
|
||||||
|
if (queries.changes().executeAsOne() != 1L) {
|
||||||
|
result = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/** This method will ignore any parts that are not proper [LightningOutgoingPayment]. */
|
||||||
|
fun getPaymentFromPartId(partId: UUID): LightningOutgoingPayment? {
|
||||||
|
return queries.getLightningPart(part_id = partId.toString()).executeAsOneOrNull()?.let { part ->
|
||||||
|
queries.getPayment(id = part.part_parent_id, Companion::mapLightningOutgoingPayment).executeAsList()
|
||||||
|
}?.filterIsInstance<LightningOutgoingPayment>()?.let {
|
||||||
|
// first ignore any legacy channel closing, then group by parent id
|
||||||
|
groupByRawLightningOutgoing(it).firstOrNull()
|
||||||
|
}?.let {
|
||||||
|
filterUselessParts(it)
|
||||||
|
// resulting payment must contain the request part id, or should be null
|
||||||
|
.takeIf { p -> p.parts.map { it.id }.contains(partId) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPaymentWithoutParts(id: UUID): LightningOutgoingPayment? {
|
||||||
|
return queries.getPaymentWithoutParts(
|
||||||
|
id = id.toString(),
|
||||||
|
mapper = Companion::mapLightningOutgoingPaymentWithoutParts
|
||||||
|
).executeAsOneOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a [LightningOutgoingPayment] for this id - if instead we find legacy converted to a new type (such as
|
||||||
|
* [ChannelCloseOutgoingPayment], this payment is ignored and we return null instead.
|
||||||
|
*/
|
||||||
|
fun getPaymentStrict(id: UUID): LightningOutgoingPayment? = queries.getPayment(
|
||||||
|
id = id.toString(),
|
||||||
|
mapper = Companion::mapLightningOutgoingPayment
|
||||||
|
).executeAsList().let { parts ->
|
||||||
|
// only take regular LN payments parts, and group them
|
||||||
|
parts.filterIsInstance<LightningOutgoingPayment>().let {
|
||||||
|
groupByRawLightningOutgoing(it).firstOrNull()
|
||||||
|
}?.let {
|
||||||
|
filterUselessParts(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* May return a [ChannelCloseOutgoingPayment] instead of the expected [LightningOutgoingPayment]. That's because
|
||||||
|
* channel closing used to be stored as [LightningOutgoingPayment] with special closing parts. We convert those to
|
||||||
|
* the propert object type.
|
||||||
|
*/
|
||||||
|
fun getPaymentRelaxed(id: UUID): OutgoingPayment? = queries.getPayment(
|
||||||
|
id = id.toString(),
|
||||||
|
mapper = Companion::mapLightningOutgoingPayment
|
||||||
|
).executeAsList().let { parts ->
|
||||||
|
// this payment may be a legacy channel closing - otherwise, only take regular LN payment parts, and group them
|
||||||
|
parts.firstOrNull { it is ChannelCloseOutgoingPayment } ?: parts.filterIsInstance<LightningOutgoingPayment>().let {
|
||||||
|
groupByRawLightningOutgoing(it).firstOrNull()
|
||||||
|
}?.let {
|
||||||
|
filterUselessParts(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getOldestCompletedDate(): Long? {
|
||||||
|
return queries.getOldestCompletedDate().executeAsOneOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun listLightningOutgoingPayments(paymentHash: ByteVector32): List<LightningOutgoingPayment> {
|
||||||
|
return queries.listPaymentsForPaymentHash(paymentHash.toByteArray(), Companion::mapLightningOutgoingPayment).executeAsList()
|
||||||
|
.filterIsInstance<LightningOutgoingPayment>()
|
||||||
|
.let { groupByRawLightningOutgoing(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Group a list of outgoing payments by parent id and parts. */
|
||||||
|
private fun groupByRawLightningOutgoing(payments: List<LightningOutgoingPayment>) = payments
|
||||||
|
.takeIf { it.isNotEmpty() }
|
||||||
|
?.groupBy { it.id }
|
||||||
|
?.values
|
||||||
|
?.map { group -> group.first().copy(parts = group.flatMap { it.parts }) }
|
||||||
|
?: emptyList()
|
||||||
|
|
||||||
|
/** Get a payment without its failed/pending parts. */
|
||||||
|
private fun filterUselessParts(payment: LightningOutgoingPayment): LightningOutgoingPayment = when (payment.status) {
|
||||||
|
is LightningOutgoingPayment.Status.Completed.Succeeded.OffChain -> {
|
||||||
|
payment.copy(parts = payment.parts.filter {
|
||||||
|
it.status is LightningOutgoingPayment.Part.Status.Succeeded
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else -> payment
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
|
fun mapLightningOutgoingPaymentWithoutParts(
|
||||||
|
id: String,
|
||||||
|
recipient_amount_msat: Long,
|
||||||
|
recipient_node_id: String,
|
||||||
|
payment_hash: ByteArray,
|
||||||
|
details_type: OutgoingDetailsTypeVersion,
|
||||||
|
details_blob: ByteArray,
|
||||||
|
created_at: Long,
|
||||||
|
completed_at: Long?,
|
||||||
|
status_type: OutgoingStatusTypeVersion?,
|
||||||
|
status_blob: ByteArray?
|
||||||
|
): LightningOutgoingPayment {
|
||||||
|
val details = OutgoingDetailsData.deserialize(details_type, details_blob)
|
||||||
|
return if (details != null) {
|
||||||
|
LightningOutgoingPayment(
|
||||||
|
id = UUID.fromString(id),
|
||||||
|
recipientAmount = MilliSatoshi(recipient_amount_msat),
|
||||||
|
recipient = PublicKey.parse(Hex.decode(recipient_node_id)),
|
||||||
|
details = details,
|
||||||
|
parts = listOf(),
|
||||||
|
status = mapPaymentStatus(status_type, status_blob, completed_at),
|
||||||
|
createdAt = created_at
|
||||||
|
)
|
||||||
|
} else throw IllegalArgumentException("cannot handle closing payment at this stage, use LegacyChannelCloseHelper")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
|
fun mapLightningOutgoingPayment(
|
||||||
|
id: String,
|
||||||
|
recipient_amount_msat: Long,
|
||||||
|
recipient_node_id: String,
|
||||||
|
payment_hash: ByteArray,
|
||||||
|
details_type: OutgoingDetailsTypeVersion,
|
||||||
|
details_blob: ByteArray,
|
||||||
|
created_at: Long,
|
||||||
|
completed_at: Long?,
|
||||||
|
status_type: OutgoingStatusTypeVersion?,
|
||||||
|
status_blob: ByteArray?,
|
||||||
|
// lightning parts data, may be null
|
||||||
|
lightning_part_id: String?,
|
||||||
|
lightning_part_amount_msat: Long?,
|
||||||
|
lightning_part_route: List<HopDesc>?,
|
||||||
|
lightning_part_created_at: Long?,
|
||||||
|
lightning_part_completed_at: Long?,
|
||||||
|
lightning_part_status_type: OutgoingPartStatusTypeVersion?,
|
||||||
|
lightning_part_status_blob: ByteArray?,
|
||||||
|
// closing tx parts data, may be null
|
||||||
|
closingtx_part_id: String?,
|
||||||
|
closingtx_part_tx_id: ByteArray?,
|
||||||
|
closingtx_part_amount_sat: Long?,
|
||||||
|
closingtx_part_closing_info_type: OutgoingPartClosingInfoTypeVersion?,
|
||||||
|
closingtx_part_closing_info_blob: ByteArray?,
|
||||||
|
closingtx_part_created_at: Long?
|
||||||
|
): OutgoingPayment {
|
||||||
|
|
||||||
|
val parts = if (lightning_part_id != null && lightning_part_amount_msat != null && lightning_part_route != null && lightning_part_created_at != null) {
|
||||||
|
listOf(
|
||||||
|
mapLightningPart(
|
||||||
|
id = lightning_part_id,
|
||||||
|
amountMsat = lightning_part_amount_msat,
|
||||||
|
route = lightning_part_route,
|
||||||
|
createdAt = lightning_part_created_at,
|
||||||
|
completedAt = lightning_part_completed_at,
|
||||||
|
statusType = lightning_part_status_type,
|
||||||
|
statusBlob = lightning_part_status_blob
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else emptyList()
|
||||||
|
|
||||||
|
return mapLightningOutgoingPaymentWithoutParts(
|
||||||
|
id = id,
|
||||||
|
recipient_amount_msat = recipient_amount_msat,
|
||||||
|
recipient_node_id = recipient_node_id,
|
||||||
|
payment_hash = payment_hash,
|
||||||
|
details_type = details_type,
|
||||||
|
details_blob = details_blob,
|
||||||
|
created_at = created_at,
|
||||||
|
completed_at = completed_at,
|
||||||
|
status_type = status_type,
|
||||||
|
status_blob = status_blob
|
||||||
|
).copy(
|
||||||
|
parts = parts
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapLightningPart(
|
||||||
|
id: String,
|
||||||
|
amountMsat: Long,
|
||||||
|
route: List<HopDesc>,
|
||||||
|
createdAt: Long,
|
||||||
|
completedAt: Long?,
|
||||||
|
statusType: OutgoingPartStatusTypeVersion?,
|
||||||
|
statusBlob: ByteArray?
|
||||||
|
): LightningOutgoingPayment.Part {
|
||||||
|
return LightningOutgoingPayment.Part(
|
||||||
|
id = UUID.fromString(id),
|
||||||
|
amount = MilliSatoshi(amountMsat),
|
||||||
|
route = route,
|
||||||
|
status = mapLightningPartStatus(
|
||||||
|
statusType = statusType,
|
||||||
|
statusBlob = statusBlob,
|
||||||
|
completedAt = completedAt
|
||||||
|
),
|
||||||
|
createdAt = createdAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapPaymentStatus(
|
||||||
|
statusType: OutgoingStatusTypeVersion?,
|
||||||
|
statusBlob: ByteArray?,
|
||||||
|
completedAt: Long?,
|
||||||
|
): LightningOutgoingPayment.Status = when {
|
||||||
|
completedAt == null && statusType == null && statusBlob == null -> LightningOutgoingPayment.Status.Pending
|
||||||
|
completedAt != null && statusType != null && statusBlob != null -> OutgoingStatusData.deserialize(statusType, statusBlob, completedAt)
|
||||||
|
else -> throw UnhandledOutgoingStatus(completedAt, statusType, statusBlob)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mapLightningPartStatus(
|
||||||
|
statusType: OutgoingPartStatusTypeVersion?,
|
||||||
|
statusBlob: ByteArray?,
|
||||||
|
completedAt: Long?,
|
||||||
|
): LightningOutgoingPayment.Part.Status = when {
|
||||||
|
completedAt == null && statusType == null && statusBlob == null -> LightningOutgoingPayment.Part.Status.Pending
|
||||||
|
completedAt != null && statusType != null && statusBlob != null -> OutgoingPartStatusData.deserialize(statusType, statusBlob, completedAt)
|
||||||
|
else -> throw UnhandledOutgoingPartStatus(statusType, statusBlob, completedAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
val hopDescAdapter: ColumnAdapter<List<HopDesc>, String> = object : ColumnAdapter<List<HopDesc>, String> {
|
||||||
|
override fun decode(databaseValue: String): List<HopDesc> = when {
|
||||||
|
databaseValue.isEmpty() -> listOf()
|
||||||
|
else -> databaseValue.split(";").map { hop ->
|
||||||
|
val els = hop.split(":")
|
||||||
|
val n1 = PublicKey.parse(Hex.decode(els[0]))
|
||||||
|
val n2 = PublicKey.parse(Hex.decode(els[1]))
|
||||||
|
val cid = els[2].takeIf { it.isNotBlank() }?.run { ShortChannelId(this) }
|
||||||
|
HopDesc(n1, n2, cid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun encode(value: List<HopDesc>): String = value.joinToString(";") {
|
||||||
|
"${it.nodeId}:${it.nextNodeId}:${it.shortChannelId ?: ""}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class UnhandledOutgoingStatus(val completedAt: Long?, val statusTypeVersion: OutgoingStatusTypeVersion?, val statusData: ByteArray?) :
|
||||||
|
RuntimeException("cannot map outgoing payment status data with completed_at=$completedAt status_type=$statusTypeVersion status=$statusData")
|
||||||
|
|
||||||
|
data class UnhandledOutgoingPartStatus(val status_type: OutgoingPartStatusTypeVersion?, val status_blob: ByteArray?, val completedAt: Long?) :
|
||||||
|
RuntimeException("cannot map outgoing part status data [ completed_at=$completedAt status_type=$status_type status_blob=$status_blob]")
|
@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:UseSerializers(
|
||||||
|
SatoshiSerializer::class,
|
||||||
|
ByteVector32Serializer::class,
|
||||||
|
)
|
||||||
|
|
||||||
|
package fr.acinq.lightning.bin.db.payments
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.ByteVector32
|
||||||
|
import fr.acinq.bitcoin.Satoshi
|
||||||
|
import fr.acinq.lightning.bin.db.payments.DbTypesHelper.decodeBlob
|
||||||
|
import fr.acinq.lightning.bin.db.serializers.v1.ByteVector32Serializer
|
||||||
|
import fr.acinq.lightning.bin.db.serializers.v1.SatoshiSerializer
|
||||||
|
import fr.acinq.lightning.db.LightningOutgoingPayment
|
||||||
|
import fr.acinq.lightning.payment.FinalFailure
|
||||||
|
import io.ktor.utils.io.charsets.*
|
||||||
|
import io.ktor.utils.io.core.*
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.UseSerializers
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
enum class OutgoingStatusTypeVersion {
|
||||||
|
SUCCEEDED_OFFCHAIN_V0,
|
||||||
|
FAILED_V0,
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class OutgoingStatusData {
|
||||||
|
|
||||||
|
sealed class SucceededOffChain : OutgoingStatusData() {
|
||||||
|
@Serializable
|
||||||
|
data class V0(@Serializable val preimage: ByteVector32) : SucceededOffChain()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class SucceededOnChain : OutgoingStatusData() {
|
||||||
|
@Serializable
|
||||||
|
data class V0(
|
||||||
|
val txIds: List<@Serializable ByteVector32>,
|
||||||
|
@Serializable val claimed: Satoshi,
|
||||||
|
val closingType: String
|
||||||
|
) : SucceededOnChain()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
object V1 : SucceededOnChain()
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class Failed : OutgoingStatusData() {
|
||||||
|
@Serializable
|
||||||
|
data class V0(val reason: String) : Failed()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/** Extract valuable data from old outgoing payments status that represent closing transactions. */
|
||||||
|
fun deserializeLegacyClosingStatus(blob: ByteArray): SucceededOnChain.V0 = decodeBlob(blob) { json, format ->
|
||||||
|
val data = format.decodeFromString<SucceededOnChain.V0>(json)
|
||||||
|
data
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deserialize(typeVersion: OutgoingStatusTypeVersion, blob: ByteArray, completedAt: Long): LightningOutgoingPayment.Status = decodeBlob(blob) { json, format ->
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
when (typeVersion) {
|
||||||
|
OutgoingStatusTypeVersion.SUCCEEDED_OFFCHAIN_V0 -> format.decodeFromString<SucceededOffChain.V0>(json).let {
|
||||||
|
LightningOutgoingPayment.Status.Completed.Succeeded.OffChain(it.preimage, completedAt)
|
||||||
|
}
|
||||||
|
OutgoingStatusTypeVersion.FAILED_V0 -> format.decodeFromString<Failed.V0>(json).let {
|
||||||
|
LightningOutgoingPayment.Status.Completed.Failed(deserializeFinalFailure(it.reason), completedAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun serializeFinalFailure(failure: FinalFailure): String = failure::class.simpleName ?: "UnknownError"
|
||||||
|
|
||||||
|
private fun deserializeFinalFailure(failure: String): FinalFailure = when (failure) {
|
||||||
|
FinalFailure.InvalidPaymentAmount::class.simpleName -> FinalFailure.InvalidPaymentAmount
|
||||||
|
FinalFailure.InvalidPaymentId::class.simpleName -> FinalFailure.InvalidPaymentId
|
||||||
|
FinalFailure.NoAvailableChannels::class.simpleName -> FinalFailure.NoAvailableChannels
|
||||||
|
FinalFailure.InsufficientBalance::class.simpleName -> FinalFailure.InsufficientBalance
|
||||||
|
FinalFailure.NoRouteToRecipient::class.simpleName -> FinalFailure.NoRouteToRecipient
|
||||||
|
FinalFailure.RecipientUnreachable::class.simpleName -> FinalFailure.RecipientUnreachable
|
||||||
|
FinalFailure.RetryExhausted::class.simpleName -> FinalFailure.RetryExhausted
|
||||||
|
FinalFailure.WalletRestarted::class.simpleName -> FinalFailure.WalletRestarted
|
||||||
|
else -> FinalFailure.UnknownError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LightningOutgoingPayment.Status.Completed.mapToDb(): Pair<OutgoingStatusTypeVersion, ByteArray> = when (this) {
|
||||||
|
is LightningOutgoingPayment.Status.Completed.Succeeded.OffChain -> OutgoingStatusTypeVersion.SUCCEEDED_OFFCHAIN_V0 to
|
||||||
|
Json.encodeToString(OutgoingStatusData.SucceededOffChain.V0(preimage)).toByteArray(Charsets.UTF_8)
|
||||||
|
is LightningOutgoingPayment.Status.Completed.Failed -> OutgoingStatusTypeVersion.FAILED_V0 to
|
||||||
|
Json.encodeToString(OutgoingStatusData.Failed.V0(OutgoingStatusData.serializeFinalFailure(reason))).toByteArray(Charsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package fr.acinq.lightning.bin.db.payments
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.TxId
|
||||||
|
import fr.acinq.lightning.db.SpliceCpfpOutgoingPayment
|
||||||
|
import fr.acinq.lightning.utils.UUID
|
||||||
|
import fr.acinq.lightning.utils.sat
|
||||||
|
import fr.acinq.lightning.utils.toByteVector32
|
||||||
|
import fr.acinq.phoenix.db.PaymentsDatabase
|
||||||
|
|
||||||
|
class SpliceCpfpOutgoingQueries(val database: PaymentsDatabase) {
|
||||||
|
private val cpfpQueries = database.spliceCpfpOutgoingPaymentsQueries
|
||||||
|
|
||||||
|
fun addCpfpPayment(payment: SpliceCpfpOutgoingPayment) {
|
||||||
|
database.transaction {
|
||||||
|
cpfpQueries.insertCpfp(
|
||||||
|
id = payment.id.toString(),
|
||||||
|
mining_fees_sat = payment.miningFees.sat,
|
||||||
|
channel_id = payment.channelId.toByteArray(),
|
||||||
|
tx_id = payment.txId.value.toByteArray(),
|
||||||
|
created_at = payment.createdAt,
|
||||||
|
confirmed_at = payment.confirmedAt,
|
||||||
|
locked_at = payment.lockedAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCpfp(id: UUID): SpliceCpfpOutgoingPayment? {
|
||||||
|
return cpfpQueries.getCpfp(
|
||||||
|
id = id.toString(),
|
||||||
|
mapper = Companion::mapCpfp
|
||||||
|
).executeAsOneOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setConfirmed(id: UUID, confirmedAt: Long) {
|
||||||
|
database.transaction {
|
||||||
|
cpfpQueries.setConfirmed(confirmed_at = confirmedAt, id = id.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLocked(id: UUID, lockedAt: Long) {
|
||||||
|
database.transaction {
|
||||||
|
cpfpQueries.setLocked(locked_at = lockedAt, id = id.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
fun mapCpfp(
|
||||||
|
id: String,
|
||||||
|
mining_fees_sat: Long,
|
||||||
|
channel_id: ByteArray,
|
||||||
|
tx_id: ByteArray,
|
||||||
|
created_at: Long,
|
||||||
|
confirmed_at: Long?,
|
||||||
|
locked_at: Long?
|
||||||
|
): SpliceCpfpOutgoingPayment {
|
||||||
|
return SpliceCpfpOutgoingPayment(
|
||||||
|
id = UUID.fromString(id),
|
||||||
|
miningFees = mining_fees_sat.sat,
|
||||||
|
channelId = channel_id.toByteVector32(),
|
||||||
|
txId = TxId(tx_id),
|
||||||
|
createdAt = created_at,
|
||||||
|
confirmedAt = confirmed_at,
|
||||||
|
lockedAt = locked_at
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2023 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package fr.acinq.lightning.bin.db.payments
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.TxId
|
||||||
|
import fr.acinq.lightning.db.SpliceOutgoingPayment
|
||||||
|
import fr.acinq.lightning.utils.UUID
|
||||||
|
import fr.acinq.lightning.utils.sat
|
||||||
|
import fr.acinq.lightning.utils.toByteVector32
|
||||||
|
import fr.acinq.phoenix.db.PaymentsDatabase
|
||||||
|
|
||||||
|
class SpliceOutgoingQueries(val database: PaymentsDatabase) {
|
||||||
|
private val spliceOutQueries = database.spliceOutgoingPaymentsQueries
|
||||||
|
|
||||||
|
fun addSpliceOutgoingPayment(payment: SpliceOutgoingPayment) {
|
||||||
|
database.transaction {
|
||||||
|
spliceOutQueries.insertSpliceOutgoing(
|
||||||
|
id = payment.id.toString(),
|
||||||
|
recipient_amount_sat = payment.recipientAmount.sat,
|
||||||
|
address = payment.address,
|
||||||
|
mining_fees_sat = payment.miningFees.sat,
|
||||||
|
channel_id = payment.channelId.toByteArray(),
|
||||||
|
tx_id = payment.txId.value.toByteArray(),
|
||||||
|
created_at = payment.createdAt,
|
||||||
|
confirmed_at = payment.confirmedAt,
|
||||||
|
locked_at = payment.lockedAt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSpliceOutPayment(id: UUID): SpliceOutgoingPayment? {
|
||||||
|
return spliceOutQueries.getSpliceOutgoing(
|
||||||
|
id = id.toString(),
|
||||||
|
mapper = Companion::mapSpliceOutgoingPayment
|
||||||
|
).executeAsOneOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setConfirmed(id: UUID, confirmedAt: Long) {
|
||||||
|
database.transaction {
|
||||||
|
spliceOutQueries.setConfirmed(confirmed_at = confirmedAt, id = id.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLocked(id: UUID, lockedAt: Long) {
|
||||||
|
database.transaction {
|
||||||
|
spliceOutQueries.setLocked(locked_at = lockedAt, id = id.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun mapSpliceOutgoingPayment(
|
||||||
|
id: String,
|
||||||
|
recipient_amount_sat: Long,
|
||||||
|
address: String,
|
||||||
|
mining_fees_sat: Long,
|
||||||
|
tx_id: ByteArray,
|
||||||
|
channel_id: ByteArray,
|
||||||
|
created_at: Long,
|
||||||
|
confirmed_at: Long?,
|
||||||
|
locked_at: Long?
|
||||||
|
): SpliceOutgoingPayment {
|
||||||
|
return SpliceOutgoingPayment(
|
||||||
|
id = UUID.fromString(id),
|
||||||
|
recipientAmount = recipient_amount_sat.sat,
|
||||||
|
address = address,
|
||||||
|
miningFees = mining_fees_sat.sat,
|
||||||
|
txId = TxId(tx_id),
|
||||||
|
channelId = channel_id.toByteVector32(),
|
||||||
|
createdAt = created_at,
|
||||||
|
confirmedAt = confirmed_at,
|
||||||
|
lockedAt = locked_at
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package fr.acinq.lightning.bin.db.serializers.v1
|
||||||
|
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
|
||||||
|
abstract class AbstractStringSerializer<T>(
|
||||||
|
name: String,
|
||||||
|
private val toString: (T) -> String,
|
||||||
|
private val fromString: (String) -> T
|
||||||
|
) : KSerializer<T> {
|
||||||
|
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor(name, PrimitiveKind.STRING)
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: T) {
|
||||||
|
encoder.encodeString(toString(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): T {
|
||||||
|
return fromString(decoder.decodeString())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package fr.acinq.lightning.bin.db.serializers.v1
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.ByteVector
|
||||||
|
import fr.acinq.bitcoin.ByteVector32
|
||||||
|
import fr.acinq.bitcoin.ByteVector64
|
||||||
|
import fr.acinq.lightning.bin.db.serializers.v1.AbstractStringSerializer
|
||||||
|
|
||||||
|
|
||||||
|
object ByteVector32Serializer : AbstractStringSerializer<ByteVector32>(
|
||||||
|
name = "ByteVector32",
|
||||||
|
toString = ByteVector32::toHex,
|
||||||
|
fromString = ::ByteVector32
|
||||||
|
)
|
||||||
|
|
||||||
|
object ByteVector64Serializer : AbstractStringSerializer<ByteVector64>(
|
||||||
|
name = "ByteVector64",
|
||||||
|
toString = ByteVector64::toHex,
|
||||||
|
fromString = ::ByteVector64
|
||||||
|
)
|
||||||
|
|
||||||
|
object ByteVectorSerializer : AbstractStringSerializer<ByteVector>(
|
||||||
|
name = "ByteVector",
|
||||||
|
toString = ByteVector::toHex,
|
||||||
|
fromString = ::ByteVector
|
||||||
|
)
|
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package fr.acinq.lightning.bin.db.serializers.v1
|
||||||
|
|
||||||
|
import fr.acinq.lightning.MilliSatoshi
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
|
||||||
|
object MilliSatoshiSerializer : KSerializer<MilliSatoshi> {
|
||||||
|
// we are using a surrogate for legacy reasons.
|
||||||
|
@Serializable
|
||||||
|
private data class MilliSatoshiSurrogate(val msat: Long)
|
||||||
|
|
||||||
|
override val descriptor: SerialDescriptor = MilliSatoshiSurrogate.serializer().descriptor
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: MilliSatoshi) {
|
||||||
|
val surrogate = MilliSatoshiSurrogate(msat = value.msat)
|
||||||
|
return encoder.encodeSerializableValue(MilliSatoshiSurrogate.serializer(), surrogate)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): MilliSatoshi {
|
||||||
|
val surrogate = decoder.decodeSerializableValue(MilliSatoshiSurrogate.serializer())
|
||||||
|
return MilliSatoshi(msat = surrogate.msat)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package fr.acinq.lightning.bin.db.serializers.v1
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.OutPoint
|
||||||
|
import fr.acinq.bitcoin.TxHash
|
||||||
|
|
||||||
|
class OutpointSerializer : AbstractStringSerializer<OutPoint>(
|
||||||
|
name = "Outpoint",
|
||||||
|
fromString = { serialized ->
|
||||||
|
serialized.split(":").let {
|
||||||
|
OutPoint(hash = TxHash(it[0]), index = it[1].toLong())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toString = { outpoint -> "${outpoint.hash}:${outpoint.index}" }
|
||||||
|
)
|
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package fr.acinq.lightning.bin.db.serializers.v1
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.Satoshi
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
|
||||||
|
object SatoshiSerializer : KSerializer<Satoshi> {
|
||||||
|
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Satoshi", PrimitiveKind.LONG)
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: Satoshi) {
|
||||||
|
encoder.encodeLong(value.toLong())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): Satoshi {
|
||||||
|
return Satoshi(decoder.decodeLong())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 ACINQ SAS
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package fr.acinq.lightning.bin.db.serializers.v1
|
||||||
|
|
||||||
|
import fr.acinq.lightning.utils.UUID
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
|
||||||
|
|
||||||
|
object UUIDSerializer : KSerializer<UUID> {
|
||||||
|
@Serializable
|
||||||
|
private data class UUIDSurrogate(val mostSignificantBits: Long, val leastSignificantBits: Long)
|
||||||
|
|
||||||
|
override val descriptor: SerialDescriptor = UUIDSurrogate.serializer().descriptor
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: UUID) {
|
||||||
|
val surrogate = UUIDSurrogate(value.mostSignificantBits, value.leastSignificantBits)
|
||||||
|
return encoder.encodeSerializableValue(UUIDSurrogate.serializer(), surrogate)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): UUID {
|
||||||
|
val surrogate = decoder.decodeSerializableValue(UUIDSurrogate.serializer())
|
||||||
|
return UUID(surrogate.mostSignificantBits, surrogate.leastSignificantBits)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
@file:UseSerializers(
|
||||||
|
// This is used by Kotlin at compile time to resolve serializers (defined in this file)
|
||||||
|
// in order to build serializers for other classes (also defined in this file).
|
||||||
|
// If we used @Serializable annotations directly on the actual classes, Kotlin would be
|
||||||
|
// able to resolve serializers by itself. It is verbose, but it allows us to contain
|
||||||
|
// serialization code in this file.
|
||||||
|
JsonSerializers.SatoshiSerializer::class,
|
||||||
|
JsonSerializers.MilliSatoshiSerializer::class,
|
||||||
|
JsonSerializers.ByteVector32Serializer::class,
|
||||||
|
JsonSerializers.PublicKeySerializer::class,
|
||||||
|
JsonSerializers.TxIdSerializer::class,
|
||||||
|
)
|
||||||
|
|
||||||
|
package fr.acinq.lightning.bin.json
|
||||||
|
|
||||||
|
import fr.acinq.bitcoin.ByteVector32
|
||||||
|
import fr.acinq.bitcoin.PublicKey
|
||||||
|
import fr.acinq.bitcoin.Satoshi
|
||||||
|
import fr.acinq.bitcoin.TxId
|
||||||
|
import fr.acinq.lightning.channel.states.ChannelState
|
||||||
|
import fr.acinq.lightning.channel.states.ChannelStateWithCommitments
|
||||||
|
import fr.acinq.lightning.db.LightningOutgoingPayment
|
||||||
|
import fr.acinq.lightning.json.JsonSerializers
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.UseSerializers
|
||||||
|
|
||||||
|
sealed class ApiType {
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Channel internal constructor(
|
||||||
|
val state: String,
|
||||||
|
val channelId: ByteVector32? = null,
|
||||||
|
val balanceSat: Satoshi? = null,
|
||||||
|
val inboundLiquiditySat: Satoshi? = null,
|
||||||
|
val capacitySat: Satoshi? = null,
|
||||||
|
val fundingTxId: TxId? = null
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun from(channel: ChannelState) = when {
|
||||||
|
channel is ChannelStateWithCommitments -> Channel(
|
||||||
|
state = channel.stateName,
|
||||||
|
channelId = channel.channelId,
|
||||||
|
balanceSat = channel.commitments.availableBalanceForSend().truncateToSatoshi(),
|
||||||
|
inboundLiquiditySat = channel.commitments.availableBalanceForReceive().truncateToSatoshi(),
|
||||||
|
capacitySat = channel.commitments.active.first().fundingAmount,
|
||||||
|
fundingTxId = channel.commitments.active.first().fundingTxId
|
||||||
|
)
|
||||||
|
else -> Channel(state = channel.stateName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class NodeInfo(
|
||||||
|
val nodeId: PublicKey,
|
||||||
|
val channels: List<Channel>
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Balance(@SerialName("amountSat") val amount: Satoshi, @SerialName("feeCreditSat") val feeCredit: Satoshi) : ApiType()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GeneratedInvoice(@SerialName("amountSat") val amount: Satoshi?, val paymentHash: ByteVector32, val serialized: String) : ApiType()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
sealed class ApiEvent : ApiType()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("payment_received")
|
||||||
|
data class PaymentReceived(@SerialName("amountSat") val amount: Satoshi, val paymentHash: ByteVector32) : ApiEvent() {
|
||||||
|
constructor(event: fr.acinq.lightning.PaymentEvents.PaymentReceived) : this(event.amount.truncateToSatoshi(), event.paymentHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("payment_sent")
|
||||||
|
data class PaymentSent(@SerialName("recipientAmountSat") val recipientAmount: Satoshi, @SerialName("routingFeeSat") val routingFee: Satoshi, val paymentHash: ByteVector32, val paymentPreimage: ByteVector32) : ApiEvent() {
|
||||||
|
constructor(event: fr.acinq.lightning.io.PaymentSent) : this(
|
||||||
|
event.payment.recipientAmount.truncateToSatoshi(),
|
||||||
|
event.payment.routingFee.truncateToSatoshi(),
|
||||||
|
event.payment.paymentHash,
|
||||||
|
(event.payment.status as LightningOutgoingPayment.Status.Completed.Succeeded.OffChain).preimage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@SerialName("payment_failed")
|
||||||
|
data class PaymentFailed(val paymentHash: ByteVector32, val reason: String) : ApiType() {
|
||||||
|
constructor(event: fr.acinq.lightning.io.PaymentNotSent) : this(event.request.paymentHash, event.reason.reason.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package fr.acinq.lightning.bin.logs
|
||||||
|
|
||||||
|
import co.touchlab.kermit.*
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.consumeAsFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import okio.FileSystem
|
||||||
|
import okio.Path
|
||||||
|
import okio.buffer
|
||||||
|
|
||||||
|
class FileLogWriter(private val logFile: Path, scope: CoroutineScope, private val messageStringFormatter: MessageStringFormatter = DefaultFormatter) : LogWriter() {
|
||||||
|
private val mailbox: Channel<String> = Channel(Channel.BUFFERED)
|
||||||
|
|
||||||
|
override fun log(severity: Severity, message: String, tag: String, throwable: Throwable?) {
|
||||||
|
mailbox.trySend(messageStringFormatter.formatMessage(severity, Tag(tag), Message(message)))
|
||||||
|
throwable?.run { mailbox.trySend(stackTraceToString()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
scope.launch {
|
||||||
|
val sink = FileSystem.SYSTEM.appendingSink(logFile).buffer()
|
||||||
|
mailbox.consumeAsFlow().collect { logLine ->
|
||||||
|
val sb = StringBuilder()
|
||||||
|
sb.append(logLine)
|
||||||
|
sb.appendLine()
|
||||||
|
sink.writeUtf8(sb.toString())
|
||||||
|
sink.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
194
src/commonMain/kotlin/fr/acinq/lightning/cli/PhoenixCli.kt
Normal file
194
src/commonMain/kotlin/fr/acinq/lightning/cli/PhoenixCli.kt
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
package fr.acinq.lightning.cli
|
||||||
|
|
||||||
|
import com.github.ajalt.clikt.core.CliktCommand
|
||||||
|
import com.github.ajalt.clikt.core.context
|
||||||
|
import com.github.ajalt.clikt.core.requireObject
|
||||||
|
import com.github.ajalt.clikt.core.subcommands
|
||||||
|
import com.github.ajalt.clikt.output.MordantHelpFormatter
|
||||||
|
import com.github.ajalt.clikt.parameters.options.*
|
||||||
|
import com.github.ajalt.clikt.parameters.types.int
|
||||||
|
import com.github.ajalt.clikt.parameters.types.long
|
||||||
|
import com.github.ajalt.clikt.sources.MapValueSource
|
||||||
|
import fr.acinq.bitcoin.Base58Check
|
||||||
|
import fr.acinq.bitcoin.Bech32
|
||||||
|
import fr.acinq.bitcoin.ByteVector32
|
||||||
|
import fr.acinq.lightning.bin.conf.readConfFile
|
||||||
|
import fr.acinq.lightning.bin.homeDirectory
|
||||||
|
import fr.acinq.lightning.payment.Bolt11Invoice
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.engine.cio.*
|
||||||
|
import io.ktor.client.plugins.auth.*
|
||||||
|
import io.ktor.client.plugins.auth.providers.*
|
||||||
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.request.forms.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
|
import io.ktor.server.util.*
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
fun main(args: Array<String>) =
|
||||||
|
PhoenixCli()
|
||||||
|
.subcommands(GetInfo(), GetBalance(), ListChannels(), CreateInvoice(), PayInvoice(), SendToAddress(), CloseChannel())
|
||||||
|
.main(args)
|
||||||
|
|
||||||
|
data class HttpConf(val baseUrl: Url, val httpClient: HttpClient)
|
||||||
|
|
||||||
|
class PhoenixCli : CliktCommand() {
|
||||||
|
private val datadir = homeDirectory / ".phoenix"
|
||||||
|
private val confFile = datadir / "phoenix.conf"
|
||||||
|
|
||||||
|
private val httpBindIp by option("--http-bind-ip", help = "Bind ip for the http api").default("127.0.0.1")
|
||||||
|
private val httpBindPort by option("--http-bind-port", help = "Bind port for the http api").int().default(9740)
|
||||||
|
private val httpPassword by option("--http-password", help = "Password for the http api").required()
|
||||||
|
|
||||||
|
init {
|
||||||
|
context {
|
||||||
|
valueSource = MapValueSource(readConfFile(confFile))
|
||||||
|
helpFormatter = { MordantHelpFormatter(it, showDefaultValues = true) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
currentContext.obj = HttpConf(
|
||||||
|
baseUrl = Url(
|
||||||
|
url {
|
||||||
|
protocol = URLProtocol.HTTP
|
||||||
|
host = httpBindIp
|
||||||
|
port = httpBindPort
|
||||||
|
}
|
||||||
|
),
|
||||||
|
httpClient = HttpClient(CIO) {
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json(json = Json {
|
||||||
|
prettyPrint = true
|
||||||
|
isLenient = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
install(Auth) {
|
||||||
|
basic {
|
||||||
|
credentials {
|
||||||
|
BasicAuthCredentials("phoenix-cli", httpPassword)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GetInfo : CliktCommand(name = "getinfo", help = "Show basic info about your node") {
|
||||||
|
private val commonOptions by requireObject<HttpConf>()
|
||||||
|
override fun run() {
|
||||||
|
runBlocking {
|
||||||
|
val res = commonOptions.httpClient.get(
|
||||||
|
url = commonOptions.baseUrl / "getinfo"
|
||||||
|
)
|
||||||
|
echo(res.bodyAsText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GetBalance : CliktCommand(name = "getbalance", help = "Returns your current balance") {
|
||||||
|
private val commonOptions by requireObject<HttpConf>()
|
||||||
|
override fun run() {
|
||||||
|
runBlocking {
|
||||||
|
val res = commonOptions.httpClient.get(
|
||||||
|
url = commonOptions.baseUrl / "getbalance"
|
||||||
|
)
|
||||||
|
echo(res.bodyAsText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListChannels : CliktCommand(name = "listchannels", help = "List all channels") {
|
||||||
|
private val commonOptions by requireObject<HttpConf>()
|
||||||
|
override fun run() {
|
||||||
|
runBlocking {
|
||||||
|
val res = commonOptions.httpClient.get(
|
||||||
|
url = commonOptions.baseUrl / "listchannels"
|
||||||
|
)
|
||||||
|
echo(res.bodyAsText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CreateInvoice : CliktCommand(name = "createinvoice", help = "Create a Lightning invoice", printHelpOnEmptyArgs = true) {
|
||||||
|
private val commonOptions by requireObject<HttpConf>()
|
||||||
|
private val amountSat by option("--amountSat").long()
|
||||||
|
private val description by option("--description", "--desc").required()
|
||||||
|
override fun run() {
|
||||||
|
runBlocking {
|
||||||
|
val res = commonOptions.httpClient.submitForm(
|
||||||
|
url = (commonOptions.baseUrl / "createinvoice").toString(),
|
||||||
|
formParameters = parameters {
|
||||||
|
amountSat?.let { append("amountSat", amountSat.toString()) }
|
||||||
|
append("description", description)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
echo(res.bodyAsText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PayInvoice : CliktCommand(name = "payinvoice", help = "Pay a Lightning invoice", printHelpOnEmptyArgs = true) {
|
||||||
|
private val commonOptions by requireObject<HttpConf>()
|
||||||
|
private val amountSat by option("--amountSat").long()
|
||||||
|
private val invoice by option("--invoice").required().check { Bolt11Invoice.read(it).isSuccess }
|
||||||
|
override fun run() {
|
||||||
|
runBlocking {
|
||||||
|
val res = commonOptions.httpClient.submitForm(
|
||||||
|
url = (commonOptions.baseUrl / "payinvoice").toString(),
|
||||||
|
formParameters = parameters {
|
||||||
|
amountSat?.let { append("amountSat", amountSat.toString()) }
|
||||||
|
append("invoice", invoice)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
echo(res.bodyAsText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SendToAddress : CliktCommand(name = "sendtoaddress", help = "Send to a Bitcoin address", printHelpOnEmptyArgs = true) {
|
||||||
|
private val commonOptions by requireObject<HttpConf>()
|
||||||
|
private val amountSat by option("--amountSat").long().required()
|
||||||
|
private val address by option("--address").required().check { runCatching { Base58Check.decode(it) }.isSuccess || runCatching { Bech32.decodeWitnessAddress(it) }.isSuccess }
|
||||||
|
private val feerateSatByte by option("--feerateSatByte").int().required()
|
||||||
|
override fun run() {
|
||||||
|
runBlocking {
|
||||||
|
val res = commonOptions.httpClient.submitForm(
|
||||||
|
url = (commonOptions.baseUrl / "sendtoaddress").toString(),
|
||||||
|
formParameters = parameters {
|
||||||
|
append("amountSat", amountSat.toString())
|
||||||
|
append("address", address)
|
||||||
|
append("feerateSatByte", feerateSatByte.toString())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
echo(res.bodyAsText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CloseChannel : CliktCommand(name = "closechannel", help = "Close all channels", printHelpOnEmptyArgs = true) {
|
||||||
|
private val commonOptions by requireObject<HttpConf>()
|
||||||
|
private val channelId by option("--channelId").convert { ByteVector32.fromValidHex(it) }.required()
|
||||||
|
private val address by option("--address").required().check { runCatching { Base58Check.decode(it) }.isSuccess || runCatching { Bech32.decodeWitnessAddress(it) }.isSuccess }
|
||||||
|
private val feerateSatByte by option("--feerateSatByte").int().required()
|
||||||
|
override fun run() {
|
||||||
|
runBlocking {
|
||||||
|
val res = commonOptions.httpClient.submitForm(
|
||||||
|
url = (commonOptions.baseUrl / "closechannel").toString(),
|
||||||
|
formParameters = parameters {
|
||||||
|
append("channelId", channelId.toHex())
|
||||||
|
append("address", address)
|
||||||
|
append("feerateSatByte", feerateSatByte.toString())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
echo(res.bodyAsText())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun Url.div(path: String) = Url(URLBuilder(this).appendPathSegments(path))
|
@ -0,0 +1,48 @@
|
|||||||
|
import kotlin.Boolean;
|
||||||
|
|
||||||
|
PRAGMA foreign_keys = 1;
|
||||||
|
|
||||||
|
-- channels table
|
||||||
|
-- note: boolean are stored as INTEGER, with 0=false
|
||||||
|
CREATE TABLE IF NOT EXISTS local_channels (
|
||||||
|
channel_id BLOB NOT NULL PRIMARY KEY,
|
||||||
|
data BLOB NOT NULL,
|
||||||
|
is_closed INTEGER AS Boolean DEFAULT 0 NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- htlcs info table
|
||||||
|
CREATE TABLE IF NOT EXISTS htlc_infos (
|
||||||
|
channel_id BLOB NOT NULL,
|
||||||
|
commitment_number INTEGER NOT NULL,
|
||||||
|
payment_hash BLOB NOT NULL,
|
||||||
|
cltv_expiry INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY(channel_id) REFERENCES local_channels(channel_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS htlc_infos_idx ON htlc_infos(channel_id, commitment_number);
|
||||||
|
|
||||||
|
-- channels queries
|
||||||
|
getChannel:
|
||||||
|
SELECT * FROM local_channels WHERE channel_id=?;
|
||||||
|
|
||||||
|
updateChannel:
|
||||||
|
UPDATE local_channels SET data=? WHERE channel_id=?;
|
||||||
|
|
||||||
|
insertChannel:
|
||||||
|
INSERT INTO local_channels VALUES (?, ?, 0);
|
||||||
|
|
||||||
|
closeLocalChannel:
|
||||||
|
UPDATE local_channels SET is_closed=1 WHERE channel_id=?;
|
||||||
|
|
||||||
|
listLocalChannels:
|
||||||
|
SELECT data FROM local_channels WHERE is_closed=0;
|
||||||
|
|
||||||
|
-- htlcs info queries
|
||||||
|
insertHtlcInfo:
|
||||||
|
INSERT INTO htlc_infos VALUES (?, ?, ?, ?);
|
||||||
|
|
||||||
|
listHtlcInfos:
|
||||||
|
SELECT payment_hash, cltv_expiry FROM htlc_infos WHERE channel_id=? AND commitment_number=?;
|
||||||
|
|
||||||
|
deleteHtlcInfo:
|
||||||
|
DELETE FROM htlc_infos WHERE channel_id=?;
|
@ -0,0 +1,38 @@
|
|||||||
|
import fr.acinq.lightning.bin.db.payments.OutgoingPartClosingInfoTypeVersion;
|
||||||
|
|
||||||
|
-- Store in a flat row outgoing payments standing for channel-closing.
|
||||||
|
-- There are no complex json columns like in the outgoing_payments table.
|
||||||
|
-- This table replaces the legacy outgoing_payment_closing_tx_parts table.
|
||||||
|
CREATE TABLE IF NOT EXISTS channel_close_outgoing_payments (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
recipient_amount_sat INTEGER NOT NULL,
|
||||||
|
address TEXT NOT NULL,
|
||||||
|
is_default_address INTEGER NOT NULL,
|
||||||
|
mining_fees_sat INTEGER NOT NULL,
|
||||||
|
tx_id BLOB NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
confirmed_at INTEGER DEFAULT NULL,
|
||||||
|
locked_at INTEGER DEFAULT NULL,
|
||||||
|
channel_id BLOB NOT NULL,
|
||||||
|
closing_info_type TEXT AS OutgoingPartClosingInfoTypeVersion NOT NULL,
|
||||||
|
closing_info_blob BLOB NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
insertChannelCloseOutgoing:
|
||||||
|
INSERT INTO channel_close_outgoing_payments (
|
||||||
|
id, recipient_amount_sat, address, is_default_address, mining_fees_sat, tx_id, created_at, confirmed_at, locked_at, channel_id, closing_info_type, closing_info_blob
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||||
|
|
||||||
|
setConfirmed:
|
||||||
|
UPDATE channel_close_outgoing_payments SET confirmed_at=? WHERE id=?;
|
||||||
|
|
||||||
|
setLocked:
|
||||||
|
UPDATE channel_close_outgoing_payments SET locked_at=? WHERE id=?;
|
||||||
|
|
||||||
|
getChannelCloseOutgoing:
|
||||||
|
SELECT id, recipient_amount_sat, address, is_default_address, mining_fees_sat, tx_id, created_at, confirmed_at, locked_at, channel_id, closing_info_type, closing_info_blob
|
||||||
|
FROM channel_close_outgoing_payments
|
||||||
|
WHERE id=?;
|
||||||
|
|
||||||
|
deleteChannelCloseOutgoing:
|
||||||
|
DELETE FROM channel_close_outgoing_payments WHERE id=?;
|
@ -0,0 +1,34 @@
|
|||||||
|
import fr.acinq.lightning.bin.db.payments.InboundLiquidityLeaseTypeVersion;
|
||||||
|
|
||||||
|
-- Stores in a flat row payments standing for an inbound liquidity request (which are done through a splice).
|
||||||
|
-- The lease data are stored in a complex column, as a json-encoded blob. See InboundLiquidityLeaseType file.
|
||||||
|
CREATE TABLE IF NOT EXISTS inbound_liquidity_outgoing_payments (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
mining_fees_sat INTEGER NOT NULL,
|
||||||
|
channel_id BLOB NOT NULL,
|
||||||
|
tx_id BLOB NOT NULL,
|
||||||
|
lease_type TEXT AS InboundLiquidityLeaseTypeVersion NOT NULL,
|
||||||
|
lease_blob BLOB NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
confirmed_at INTEGER DEFAULT NULL,
|
||||||
|
locked_at INTEGER DEFAULT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
insert:
|
||||||
|
INSERT INTO inbound_liquidity_outgoing_payments (
|
||||||
|
id, mining_fees_sat, channel_id, tx_id, lease_type, lease_blob, created_at, confirmed_at, locked_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||||
|
|
||||||
|
setConfirmed:
|
||||||
|
UPDATE inbound_liquidity_outgoing_payments SET confirmed_at=? WHERE id=?;
|
||||||
|
|
||||||
|
setLocked:
|
||||||
|
UPDATE inbound_liquidity_outgoing_payments SET locked_at=? WHERE id=?;
|
||||||
|
|
||||||
|
get:
|
||||||
|
SELECT id, mining_fees_sat, channel_id, tx_id, lease_type, lease_blob, created_at, confirmed_at, locked_at
|
||||||
|
FROM inbound_liquidity_outgoing_payments
|
||||||
|
WHERE id=?;
|
||||||
|
|
||||||
|
delete:
|
||||||
|
DELETE FROM inbound_liquidity_outgoing_payments WHERE id=?;
|
@ -0,0 +1,98 @@
|
|||||||
|
import fr.acinq.lightning.bin.db.payments.IncomingOriginTypeVersion;
|
||||||
|
import fr.acinq.lightning.bin.db.payments.IncomingReceivedWithTypeVersion;
|
||||||
|
|
||||||
|
-- incoming payments
|
||||||
|
CREATE TABLE IF NOT EXISTS incoming_payments (
|
||||||
|
payment_hash BLOB NOT NULL PRIMARY KEY,
|
||||||
|
preimage BLOB NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
-- origin
|
||||||
|
origin_type TEXT AS IncomingOriginTypeVersion NOT NULL,
|
||||||
|
origin_blob BLOB NOT NULL,
|
||||||
|
-- this field is legacy, the amount received is the sum of the received-with parts
|
||||||
|
received_amount_msat INTEGER DEFAULT NULL,
|
||||||
|
-- timestamp when the payment has been received
|
||||||
|
received_at INTEGER DEFAULT NULL,
|
||||||
|
-- received-with parts
|
||||||
|
received_with_type TEXT AS IncomingReceivedWithTypeVersion DEFAULT NULL,
|
||||||
|
received_with_blob BLOB DEFAULT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create indexes to optimize the queries in AggregatedQueries.
|
||||||
|
-- Tip: Use "explain query plan" to ensure they're actually being used.
|
||||||
|
CREATE INDEX IF NOT EXISTS incoming_payments_filter_idx
|
||||||
|
ON incoming_payments(received_at)
|
||||||
|
WHERE received_at IS NOT NULL;
|
||||||
|
|
||||||
|
-- queries
|
||||||
|
|
||||||
|
insert:
|
||||||
|
INSERT INTO incoming_payments (
|
||||||
|
payment_hash,
|
||||||
|
preimage,
|
||||||
|
created_at,
|
||||||
|
origin_type,
|
||||||
|
origin_blob)
|
||||||
|
VALUES (?, ?, ?, ?, ?);
|
||||||
|
|
||||||
|
updateReceived:
|
||||||
|
UPDATE incoming_payments
|
||||||
|
SET received_at=?,
|
||||||
|
received_with_type=?,
|
||||||
|
received_with_blob=?
|
||||||
|
WHERE payment_hash = ?;
|
||||||
|
|
||||||
|
insertAndReceive:
|
||||||
|
INSERT INTO incoming_payments (
|
||||||
|
payment_hash,
|
||||||
|
preimage,
|
||||||
|
created_at,
|
||||||
|
origin_type, origin_blob,
|
||||||
|
received_at,
|
||||||
|
received_with_type,
|
||||||
|
received_with_blob)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?);
|
||||||
|
|
||||||
|
get:
|
||||||
|
SELECT payment_hash, preimage, created_at, origin_type, origin_blob, received_amount_msat, received_at, received_with_type, received_with_blob
|
||||||
|
FROM incoming_payments
|
||||||
|
WHERE payment_hash=?;
|
||||||
|
|
||||||
|
getOldestReceivedDate:
|
||||||
|
SELECT received_at
|
||||||
|
FROM incoming_payments AS r
|
||||||
|
WHERE received_at IS NOT NULL
|
||||||
|
ORDER BY r.received_at ASC
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
listAllWithin:
|
||||||
|
SELECT payment_hash, preimage, created_at, origin_type, origin_blob, received_amount_msat, received_at, received_with_type, received_with_blob
|
||||||
|
FROM incoming_payments
|
||||||
|
WHERE created_at BETWEEN :from AND :to
|
||||||
|
ORDER BY
|
||||||
|
coalesce(received_at, created_at) DESC,
|
||||||
|
payment_hash DESC;
|
||||||
|
|
||||||
|
listAllNotConfirmed:
|
||||||
|
SELECT incoming_payments.payment_hash, preimage, created_at, origin_type, origin_blob, received_amount_msat, received_at, received_with_type, received_with_blob
|
||||||
|
FROM incoming_payments
|
||||||
|
LEFT OUTER JOIN link_tx_to_payments
|
||||||
|
ON link_tx_to_payments.type = 1
|
||||||
|
AND link_tx_to_payments.confirmed_at IS NULL
|
||||||
|
AND link_tx_to_payments.id = incoming_payments.payment_hash
|
||||||
|
WHERE received_at IS NOT NULL
|
||||||
|
;
|
||||||
|
|
||||||
|
scanCompleted:
|
||||||
|
SELECT payment_hash,
|
||||||
|
received_at
|
||||||
|
FROM incoming_payments
|
||||||
|
WHERE received_at IS NOT NULL;
|
||||||
|
|
||||||
|
delete:
|
||||||
|
DELETE FROM incoming_payments
|
||||||
|
WHERE payment_hash = ?;
|
||||||
|
|
||||||
|
-- use this in a `transaction` block to know how many rows were changed after an UPDATE
|
||||||
|
changes:
|
||||||
|
SELECT changes();
|
@ -0,0 +1,29 @@
|
|||||||
|
-- This table links an on-chain transaction to one/many incoming or outgoing payment
|
||||||
|
-- * tx_id => hex identifier of an on-chain transaction
|
||||||
|
-- * type => tracks the type of a payment. The value is an int as defined in the DbType enum
|
||||||
|
-- * id => the identifier of the payment, can be a payment hash (incoming) or a UUID (outgoing)
|
||||||
|
CREATE TABLE IF NOT EXISTS link_tx_to_payments (
|
||||||
|
tx_id BLOB NOT NULL,
|
||||||
|
type INTEGER NOT NULL,
|
||||||
|
id TEXT NOT NULL,
|
||||||
|
confirmed_at INTEGER DEFAULT NULL,
|
||||||
|
locked_at INTEGER DEFAULT NULL,
|
||||||
|
PRIMARY KEY (tx_id, type, id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS link_tx_to_payments_txid ON link_tx_to_payments(tx_id);
|
||||||
|
|
||||||
|
listUnconfirmed:
|
||||||
|
SELECT DISTINCT(tx_id) FROM link_tx_to_payments WHERE confirmed_at IS NULL;
|
||||||
|
|
||||||
|
getPaymentIdForTx:
|
||||||
|
SELECT tx_id, type, id FROM link_tx_to_payments WHERE tx_id=?;
|
||||||
|
|
||||||
|
linkTxToPayment:
|
||||||
|
INSERT INTO link_tx_to_payments(tx_id, type, id) VALUES (?, ?, ?);
|
||||||
|
|
||||||
|
setConfirmed:
|
||||||
|
UPDATE link_tx_to_payments SET confirmed_at=? WHERE tx_id=?;
|
||||||
|
|
||||||
|
setLocked:
|
||||||
|
UPDATE link_tx_to_payments SET locked_at=? WHERE tx_id=?;
|
@ -0,0 +1,228 @@
|
|||||||
|
import fr.acinq.lightning.db.HopDesc;
|
||||||
|
import fr.acinq.lightning.bin.db.payments.OutgoingDetailsTypeVersion;
|
||||||
|
import fr.acinq.lightning.bin.db.payments.OutgoingPartClosingInfoTypeVersion;
|
||||||
|
import fr.acinq.lightning.bin.db.payments.OutgoingPartStatusTypeVersion;
|
||||||
|
import fr.acinq.lightning.bin.db.payments.OutgoingStatusTypeVersion;
|
||||||
|
import kotlin.collections.List;
|
||||||
|
|
||||||
|
PRAGMA foreign_keys = 1;
|
||||||
|
|
||||||
|
-- outgoing payments
|
||||||
|
-- Stores an outgoing payment in a flat row. Some columns can be null.
|
||||||
|
CREATE TABLE IF NOT EXISTS outgoing_payments (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
recipient_amount_msat INTEGER NOT NULL,
|
||||||
|
recipient_node_id TEXT NOT NULL,
|
||||||
|
payment_hash BLOB NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
-- details
|
||||||
|
details_type TEXT AS OutgoingDetailsTypeVersion NOT NULL,
|
||||||
|
details_blob BLOB NOT NULL,
|
||||||
|
-- status
|
||||||
|
completed_at INTEGER DEFAULT NULL,
|
||||||
|
status_type TEXT AS OutgoingStatusTypeVersion DEFAULT NULL,
|
||||||
|
status_blob BLOB DEFAULT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create indexes to optimize the queries in AggregatedQueries.
|
||||||
|
-- Tip: Use "explain query plan" to ensure they're actually being used.
|
||||||
|
CREATE INDEX IF NOT EXISTS outgoing_payments_filter_idx
|
||||||
|
ON outgoing_payments(completed_at);
|
||||||
|
|
||||||
|
-- Stores the lightning parts that make up a lightning payment
|
||||||
|
CREATE TABLE IF NOT EXISTS outgoing_payment_parts (
|
||||||
|
part_id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
part_parent_id TEXT NOT NULL,
|
||||||
|
part_amount_msat INTEGER NOT NULL,
|
||||||
|
part_route TEXT AS List<HopDesc> NOT NULL,
|
||||||
|
part_created_at INTEGER NOT NULL,
|
||||||
|
-- status
|
||||||
|
part_completed_at INTEGER DEFAULT NULL,
|
||||||
|
part_status_type TEXT AS OutgoingPartStatusTypeVersion DEFAULT NULL,
|
||||||
|
part_status_blob BLOB DEFAULT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY(part_parent_id) REFERENCES outgoing_payments(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- !! This table is legacy, and will only contain old payments. See ChannelCloseOutgoingPayment.sq for the new table.
|
||||||
|
-- Stores the transactions that close a channel
|
||||||
|
CREATE TABLE IF NOT EXISTS outgoing_payment_closing_tx_parts (
|
||||||
|
part_id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
part_parent_id TEXT NOT NULL,
|
||||||
|
part_tx_id BLOB NOT NULL,
|
||||||
|
part_amount_sat INTEGER NOT NULL,
|
||||||
|
part_closing_info_type TEXT AS OutgoingPartClosingInfoTypeVersion NOT NULL,
|
||||||
|
part_closing_info_blob BLOB NOT NULL,
|
||||||
|
part_created_at INTEGER NOT NULL,
|
||||||
|
|
||||||
|
FOREIGN KEY(part_parent_id) REFERENCES outgoing_payments(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- A FOREIGN KEY does NOT create an implicit index.
|
||||||
|
-- One would expect it to, but it doesn't.
|
||||||
|
-- As per the docs (https://sqlite.org/foreignkeys.html):
|
||||||
|
-- > Indices are not required for child key columns but they are almost always beneficial.
|
||||||
|
-- > [...] So, in most real systems, an index should be created on the child key columns
|
||||||
|
-- > of each foreign key constraint.
|
||||||
|
CREATE INDEX IF NOT EXISTS parent_id_idx ON outgoing_payment_parts(part_parent_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS parent_id_idx ON outgoing_payment_closing_tx_parts(part_parent_id);
|
||||||
|
|
||||||
|
-- queries for outgoing payments
|
||||||
|
|
||||||
|
hasPayment:
|
||||||
|
SELECT COUNT(*) FROM outgoing_payments
|
||||||
|
WHERE id = ?;
|
||||||
|
|
||||||
|
insertPayment:
|
||||||
|
INSERT INTO outgoing_payments (
|
||||||
|
id,
|
||||||
|
recipient_amount_msat,
|
||||||
|
recipient_node_id,
|
||||||
|
payment_hash,
|
||||||
|
created_at,
|
||||||
|
details_type,
|
||||||
|
details_blob)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||||
|
|
||||||
|
updatePayment:
|
||||||
|
UPDATE outgoing_payments SET completed_at=?, status_type=?, status_blob=? WHERE id=?;
|
||||||
|
|
||||||
|
scanCompleted:
|
||||||
|
SELECT id, completed_at
|
||||||
|
FROM outgoing_payments
|
||||||
|
WHERE completed_at IS NOT NULL;
|
||||||
|
|
||||||
|
deletePayment:
|
||||||
|
DELETE FROM outgoing_payments WHERE id = ?;
|
||||||
|
|
||||||
|
-- queries for lightning parts
|
||||||
|
|
||||||
|
countLightningPart:
|
||||||
|
SELECT COUNT(*) FROM outgoing_payment_parts WHERE part_id = ?;
|
||||||
|
|
||||||
|
insertLightningPart:
|
||||||
|
INSERT INTO outgoing_payment_parts (
|
||||||
|
part_id,
|
||||||
|
part_parent_id,
|
||||||
|
part_amount_msat,
|
||||||
|
part_route,
|
||||||
|
part_created_at)
|
||||||
|
VALUES (?, ?, ?, ?, ?);
|
||||||
|
|
||||||
|
updateLightningPart:
|
||||||
|
UPDATE outgoing_payment_parts
|
||||||
|
SET part_status_type=?,
|
||||||
|
part_status_blob=?,
|
||||||
|
part_completed_at=?
|
||||||
|
WHERE part_id=?;
|
||||||
|
|
||||||
|
getLightningPart:
|
||||||
|
SELECT * FROM outgoing_payment_parts WHERE part_id=?;
|
||||||
|
|
||||||
|
deleteLightningPartsForParentId:
|
||||||
|
DELETE FROM outgoing_payment_parts WHERE part_parent_id = ?;
|
||||||
|
|
||||||
|
-- queries for closing tx parts
|
||||||
|
|
||||||
|
countClosingTxPart:
|
||||||
|
SELECT COUNT(*) FROM outgoing_payment_closing_tx_parts WHERE part_id = ?;
|
||||||
|
|
||||||
|
insertClosingTxPart:
|
||||||
|
INSERT INTO outgoing_payment_closing_tx_parts (
|
||||||
|
part_id,
|
||||||
|
part_parent_id,
|
||||||
|
part_tx_id,
|
||||||
|
part_amount_sat,
|
||||||
|
part_closing_info_type,
|
||||||
|
part_closing_info_blob,
|
||||||
|
part_created_at
|
||||||
|
) VALUES (:id, :parent_id, :tx_id, :amount_msat, :closing_info_type, :closing_info_blob, :created_at);
|
||||||
|
|
||||||
|
-- queries mixing outgoing payments and parts
|
||||||
|
|
||||||
|
getPaymentWithoutParts:
|
||||||
|
SELECT id,
|
||||||
|
recipient_amount_msat,
|
||||||
|
recipient_node_id,
|
||||||
|
payment_hash,
|
||||||
|
details_type,
|
||||||
|
details_blob,
|
||||||
|
created_at,
|
||||||
|
completed_at,
|
||||||
|
status_type,
|
||||||
|
status_blob
|
||||||
|
FROM outgoing_payments
|
||||||
|
WHERE id=?;
|
||||||
|
|
||||||
|
getOldestCompletedDate:
|
||||||
|
SELECT completed_at
|
||||||
|
FROM outgoing_payments AS o
|
||||||
|
WHERE completed_at IS NOT NULL
|
||||||
|
ORDER BY o.completed_at ASC
|
||||||
|
LIMIT 1;
|
||||||
|
|
||||||
|
getPayment:
|
||||||
|
SELECT parent.id,
|
||||||
|
parent.recipient_amount_msat,
|
||||||
|
parent.recipient_node_id,
|
||||||
|
parent.payment_hash,
|
||||||
|
parent.details_type,
|
||||||
|
parent.details_blob,
|
||||||
|
parent.created_at,
|
||||||
|
parent.completed_at,
|
||||||
|
parent.status_type,
|
||||||
|
parent.status_blob,
|
||||||
|
-- lightning parts
|
||||||
|
lightning_parts.part_id AS lightning_part_id,
|
||||||
|
lightning_parts.part_amount_msat AS lightning_part_amount_msat,
|
||||||
|
lightning_parts.part_route AS lightning_part_route,
|
||||||
|
lightning_parts.part_created_at AS lightning_part_created_at,
|
||||||
|
lightning_parts.part_completed_at AS lightning_part_completed_at,
|
||||||
|
lightning_parts.part_status_type AS lightning_part_status_type,
|
||||||
|
lightning_parts.part_status_blob AS lightning_part_status_blob,
|
||||||
|
-- closing tx parts
|
||||||
|
closing_parts.part_id AS closingtx_part_id,
|
||||||
|
closing_parts.part_tx_id AS closingtx_tx_id,
|
||||||
|
closing_parts.part_amount_sat AS closingtx_amount_sat,
|
||||||
|
closing_parts.part_closing_info_type AS closingtx_info_type,
|
||||||
|
closing_parts.part_closing_info_blob AS closingtx_info_blob,
|
||||||
|
closing_parts.part_created_at AS closingtx_created_at
|
||||||
|
FROM outgoing_payments AS parent
|
||||||
|
LEFT OUTER JOIN outgoing_payment_parts AS lightning_parts ON lightning_parts.part_parent_id = parent.id
|
||||||
|
LEFT OUTER JOIN outgoing_payment_closing_tx_parts AS closing_parts ON closing_parts.part_parent_id = parent.id
|
||||||
|
WHERE parent.id=?;
|
||||||
|
|
||||||
|
listPaymentsForPaymentHash:
|
||||||
|
SELECT parent.id,
|
||||||
|
parent.recipient_amount_msat,
|
||||||
|
parent.recipient_node_id,
|
||||||
|
parent.payment_hash,
|
||||||
|
parent.details_type,
|
||||||
|
parent.details_blob,
|
||||||
|
parent.created_at,
|
||||||
|
parent.completed_at,
|
||||||
|
parent.status_type,
|
||||||
|
parent.status_blob,
|
||||||
|
-- lightning parts
|
||||||
|
lightning_parts.part_id AS lightning_part_id,
|
||||||
|
lightning_parts.part_amount_msat AS lightning_part_amount_msat,
|
||||||
|
lightning_parts.part_route AS lightning_part_route,
|
||||||
|
lightning_parts.part_created_at AS lightning_part_created_at,
|
||||||
|
lightning_parts.part_completed_at AS lightning_part_completed_at,
|
||||||
|
lightning_parts.part_status_type AS lightning_part_status_type,
|
||||||
|
lightning_parts.part_status_blob AS lightning_part_status_blob,
|
||||||
|
-- closing tx parts
|
||||||
|
closing_parts.part_id AS closingtx_part_id,
|
||||||
|
closing_parts.part_tx_id AS closingtx_tx_id,
|
||||||
|
closing_parts.part_amount_sat AS closingtx_amount_sat,
|
||||||
|
closing_parts.part_closing_info_type AS closingtx_info_type,
|
||||||
|
closing_parts.part_closing_info_blob AS closingtx_info_blob,
|
||||||
|
closing_parts.part_created_at AS closingtx_created_at
|
||||||
|
FROM outgoing_payments AS parent
|
||||||
|
LEFT OUTER JOIN outgoing_payment_parts AS lightning_parts ON lightning_parts.part_parent_id = parent.id
|
||||||
|
LEFT OUTER JOIN outgoing_payment_closing_tx_parts AS closing_parts ON closing_parts.part_parent_id = parent.id
|
||||||
|
WHERE payment_hash=?;
|
||||||
|
|
||||||
|
-- use this in a `transaction` block to know how many rows were changed after an UPDATE
|
||||||
|
changes:
|
||||||
|
SELECT changes();
|
@ -0,0 +1,32 @@
|
|||||||
|
import fr.acinq.lightning.bin.db.payments.OutgoingPartClosingInfoTypeVersion;
|
||||||
|
|
||||||
|
-- Store in a flat row the outgoing payments standing for a CPFP (which are done throuh a splice).
|
||||||
|
-- There are no complex json columns like in the outgoing_payments table.
|
||||||
|
CREATE TABLE IF NOT EXISTS splice_cpfp_outgoing_payments (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
mining_fees_sat INTEGER NOT NULL,
|
||||||
|
channel_id BLOB NOT NULL,
|
||||||
|
tx_id BLOB NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
confirmed_at INTEGER DEFAULT NULL,
|
||||||
|
locked_at INTEGER DEFAULT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
insertCpfp:
|
||||||
|
INSERT INTO splice_cpfp_outgoing_payments (
|
||||||
|
id, mining_fees_sat, channel_id, tx_id, created_at, confirmed_at, locked_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?);
|
||||||
|
|
||||||
|
setConfirmed:
|
||||||
|
UPDATE splice_cpfp_outgoing_payments SET confirmed_at=? WHERE id=?;
|
||||||
|
|
||||||
|
setLocked:
|
||||||
|
UPDATE splice_cpfp_outgoing_payments SET locked_at=? WHERE id=?;
|
||||||
|
|
||||||
|
getCpfp:
|
||||||
|
SELECT id, mining_fees_sat, channel_id, tx_id, created_at, confirmed_at, locked_at
|
||||||
|
FROM splice_cpfp_outgoing_payments
|
||||||
|
WHERE id=?;
|
||||||
|
|
||||||
|
deleteCpfp:
|
||||||
|
DELETE FROM splice_cpfp_outgoing_payments WHERE id=?;
|
@ -0,0 +1,32 @@
|
|||||||
|
-- store a splice-out payment in a flat row
|
||||||
|
-- there are no complex json columns like in the outgoing_payments table
|
||||||
|
CREATE TABLE IF NOT EXISTS splice_outgoing_payments (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
recipient_amount_sat INTEGER NOT NULL,
|
||||||
|
address TEXT NOT NULL,
|
||||||
|
mining_fees_sat INTEGER NOT NULL,
|
||||||
|
tx_id BLOB NOT NULL,
|
||||||
|
channel_id BLOB NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
confirmed_at INTEGER DEFAULT NULL,
|
||||||
|
locked_at INTEGER DEFAULT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
insertSpliceOutgoing:
|
||||||
|
INSERT INTO splice_outgoing_payments (
|
||||||
|
id, recipient_amount_sat, address, mining_fees_sat, tx_id, channel_id, created_at, confirmed_at, locked_at
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||||
|
|
||||||
|
setConfirmed:
|
||||||
|
UPDATE splice_outgoing_payments SET confirmed_at=? WHERE id=?;
|
||||||
|
|
||||||
|
setLocked:
|
||||||
|
UPDATE splice_outgoing_payments SET locked_at=? WHERE id=?;
|
||||||
|
|
||||||
|
getSpliceOutgoing:
|
||||||
|
SELECT id, recipient_amount_sat, address, mining_fees_sat, tx_id, channel_id, created_at, confirmed_at, locked_at
|
||||||
|
FROM splice_outgoing_payments
|
||||||
|
WHERE id=?;
|
||||||
|
|
||||||
|
deleteSpliceOutgoing:
|
||||||
|
DELETE FROM splice_outgoing_payments WHERE id=?;
|
24
src/jvmMain/kotlin/fr/acinq/lightning/bin/Actuals.kt
Normal file
24
src/jvmMain/kotlin/fr/acinq/lightning/bin/Actuals.kt
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package fr.acinq.lightning.bin
|
||||||
|
|
||||||
|
import app.cash.sqldelight.db.SqlDriver
|
||||||
|
import app.cash.sqldelight.driver.jdbc.sqlite.JdbcSqliteDriver
|
||||||
|
import fr.acinq.phoenix.db.ChannelsDatabase
|
||||||
|
import fr.acinq.phoenix.db.PaymentsDatabase
|
||||||
|
import okio.Path
|
||||||
|
import okio.Path.Companion.toPath
|
||||||
|
|
||||||
|
actual val homeDirectory: Path = System.getProperty("user.home").toPath()
|
||||||
|
|
||||||
|
actual fun createAppDbDriver(dir: Path): SqlDriver {
|
||||||
|
val path = dir / "phoenix.db"
|
||||||
|
val driver = JdbcSqliteDriver("jdbc:sqlite:$path")
|
||||||
|
ChannelsDatabase.Schema.create(driver)
|
||||||
|
return driver
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun createPaymentsDbDriver(dir: Path): SqlDriver {
|
||||||
|
val path = dir / "payments.db"
|
||||||
|
val driver = JdbcSqliteDriver("jdbc:sqlite:$path")
|
||||||
|
PaymentsDatabase.Schema.create(driver)
|
||||||
|
return driver
|
||||||
|
}
|
27
src/nativeMain/kotlin/fr/acinq/lightning/bin/Actuals.kt
Normal file
27
src/nativeMain/kotlin/fr/acinq/lightning/bin/Actuals.kt
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package fr.acinq.lightning.bin
|
||||||
|
|
||||||
|
import app.cash.sqldelight.db.SqlDriver
|
||||||
|
import app.cash.sqldelight.driver.native.NativeSqliteDriver
|
||||||
|
import fr.acinq.phoenix.db.ChannelsDatabase
|
||||||
|
import fr.acinq.phoenix.db.PaymentsDatabase
|
||||||
|
import kotlinx.cinterop.ExperimentalForeignApi
|
||||||
|
import kotlinx.cinterop.toKString
|
||||||
|
import okio.Path
|
||||||
|
import okio.Path.Companion.toPath
|
||||||
|
import platform.posix.getenv
|
||||||
|
import platform.posix.setenv
|
||||||
|
|
||||||
|
@OptIn(ExperimentalForeignApi::class)
|
||||||
|
actual val homeDirectory: Path = setenv("KTOR_LOG_LEVEL", "WARN", 1).let { getenv("HOME")?.toKString()!!.toPath() }
|
||||||
|
|
||||||
|
actual fun createAppDbDriver(dir: Path): SqlDriver {
|
||||||
|
return NativeSqliteDriver(ChannelsDatabase.Schema, "phoenix.db",
|
||||||
|
onConfiguration = { it.copy(extendedConfig = it.extendedConfig.copy(basePath = dir.toString())) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual fun createPaymentsDbDriver(dir: Path): SqlDriver {
|
||||||
|
return NativeSqliteDriver(PaymentsDatabase.Schema, "payments.db",
|
||||||
|
onConfiguration = { it.copy(extendedConfig = it.extendedConfig.copy(basePath = dir.toString())) }
|
||||||
|
)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user