BOLT12 receive (#882)

* Add BOLT12 receive payment handling

* Handle BOLT12 invoice requests via WS

* Fix invoice request subscription on stream initialisation

* Store the BOLT12 offer used to receive a payment

* Address review feedback

* Separate into create BOLT12 invoice fn

* Update all BOLT12 offers when webhook URL changes

* Deprecate Lightning for Bolt11Invoice
This commit is contained in:
Ross Savage
2025-04-29 13:43:45 +02:00
committed by GitHub
parent 586a349b75
commit 5b69c7beb2
66 changed files with 2892 additions and 451 deletions

View File

@@ -48,7 +48,7 @@ jobs:
- name: Clippy bindings
working-directory: lib/bindings
run: cargo clippy --all-targets -- -D warnings
run: cargo clippy --all-targets -- -A deprecated -D warnings
- name: Clippy core
working-directory: lib/core
@@ -216,7 +216,7 @@ jobs:
with:
version: "27.2"
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Start regtest environment
working-directory: regtest
run: sh start.sh

View File

@@ -12,7 +12,7 @@ fmt:
clippy: cargo-clippy wasm-clippy
cargo-clippy:
cd lib/bindings && cargo clippy --all-targets -- -D warnings
cd lib/bindings && cargo clippy --all-targets -- -A deprecated -D warnings
cd lib/core && cargo clippy --all-targets -- -D warnings
cd cli && cargo clippy -- -D warnings

82
cli/Cargo.lock generated
View File

@@ -637,7 +637,7 @@ checksum = "829a082bd3761fde7476dc2ed85ca56c11628948460ece621e4f56fef5046567"
[[package]]
name = "boltz-client"
version = "0.3.0"
source = "git+https://github.com/danielgranhao/boltz-rust?rev=9c703b20a86cb8a4d9a0d3d6dc2f610a0389fe7a#9c703b20a86cb8a4d9a0d3d6dc2f610a0389fe7a"
source = "git+https://github.com/dangeross/boltz-rust?rev=0ffdc77b3db6a14484b1a948b78d506a6aebbafe#0ffdc77b3db6a14484b1a948b78d506a6aebbafe"
dependencies = [
"async-trait",
"bip39",
@@ -648,7 +648,7 @@ dependencies = [
"getrandom 0.2.14",
"hex",
"js-sys",
"lightning-invoice 0.32.0",
"lightning 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"log",
"macros",
"reqwest 0.12.12",
@@ -2577,9 +2577,25 @@ dependencies = [
"dnssec-prover",
"hashbrown 0.13.2",
"libm",
"lightning-invoice 0.33.2",
"lightning-types 0.2.0",
"possiblyrandom",
"lightning-invoice 0.33.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lightning-types 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"possiblyrandom 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "lightning"
version = "0.1.2"
source = "git+https://github.com/breez/rust-lightning?rev=1ed173a39d4a415bab8171c3348b459f51f3b5e6#1ed173a39d4a415bab8171c3348b459f51f3b5e6"
dependencies = [
"bech32 0.11.0",
"bitcoin 0.32.5",
"dnssec-prover",
"hashbrown 0.13.2",
"libm",
"lightning-invoice 0.33.2 (git+https://github.com/breez/rust-lightning?rev=1ed173a39d4a415bab8171c3348b459f51f3b5e6)",
"lightning-types 0.2.0 (git+https://github.com/breez/rust-lightning?rev=1ed173a39d4a415bab8171c3348b459f51f3b5e6)",
"musig2",
"possiblyrandom 0.2.0 (git+https://github.com/breez/rust-lightning?rev=1ed173a39d4a415bab8171c3348b459f51f3b5e6)",
]
[[package]]
@@ -2596,17 +2612,6 @@ dependencies = [
"secp256k1 0.24.3",
]
[[package]]
name = "lightning-invoice"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ab9f6ea77e20e3129235e62a2e6bd64ed932363df104e864ee65ccffb54a8f"
dependencies = [
"bech32 0.9.1",
"bitcoin 0.32.5",
"lightning-types 0.1.0",
]
[[package]]
name = "lightning-invoice"
version = "0.33.2"
@@ -2615,18 +2620,17 @@ checksum = "11209f386879b97198b2bfc9e9c1e5d42870825c6bd4376f17f95357244d6600"
dependencies = [
"bech32 0.11.0",
"bitcoin 0.32.5",
"lightning-types 0.2.0",
"lightning-types 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "lightning-types"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1083b8d9137000edf3bfcb1ff011c0d25e0cdd2feb98cc21d6765e64a494148f"
name = "lightning-invoice"
version = "0.33.2"
source = "git+https://github.com/breez/rust-lightning?rev=1ed173a39d4a415bab8171c3348b459f51f3b5e6#1ed173a39d4a415bab8171c3348b459f51f3b5e6"
dependencies = [
"bech32 0.9.1",
"bech32 0.11.0",
"bitcoin 0.32.5",
"hex-conservative 0.2.1",
"lightning-types 0.2.0 (git+https://github.com/breez/rust-lightning?rev=1ed173a39d4a415bab8171c3348b459f51f3b5e6)",
]
[[package]]
@@ -2638,6 +2642,14 @@ dependencies = [
"bitcoin 0.32.5",
]
[[package]]
name = "lightning-types"
version = "0.2.0"
source = "git+https://github.com/breez/rust-lightning?rev=1ed173a39d4a415bab8171c3348b459f51f3b5e6#1ed173a39d4a415bab8171c3348b459f51f3b5e6"
dependencies = [
"bitcoin 0.32.5",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
@@ -2758,7 +2770,7 @@ dependencies = [
[[package]]
name = "macros"
version = "0.0.0"
source = "git+https://github.com/danielgranhao/boltz-rust?rev=9c703b20a86cb8a4d9a0d3d6dc2f610a0389fe7a#9c703b20a86cb8a4d9a0d3d6dc2f610a0389fe7a"
source = "git+https://github.com/dangeross/boltz-rust?rev=0ffdc77b3db6a14484b1a948b78d506a6aebbafe#0ffdc77b3db6a14484b1a948b78d506a6aebbafe"
dependencies = [
"proc-macro2",
"quote",
@@ -2894,6 +2906,14 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
[[package]]
name = "musig2"
version = "0.1.0"
source = "git+https://github.com/arik-so/rust-musig2?rev=6f95a05718cbb44d8fe3fa6021aea8117aa38d50#6f95a05718cbb44d8fe3fa6021aea8117aa38d50"
dependencies = [
"bitcoin 0.32.5",
]
[[package]]
name = "native-tls"
version = "0.2.14"
@@ -3258,6 +3278,14 @@ dependencies = [
"getrandom 0.2.14",
]
[[package]]
name = "possiblyrandom"
version = "0.2.0"
source = "git+https://github.com/breez/rust-lightning?rev=1ed173a39d4a415bab8171c3348b459f51f3b5e6#1ed173a39d4a415bab8171c3348b459f51f3b5e6"
dependencies = [
"getrandom 0.2.14",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
@@ -4162,7 +4190,7 @@ dependencies = [
[[package]]
name = "sdk-common"
version = "0.6.2"
source = "git+https://github.com/breez/breez-sdk?rev=223002e47a3b1be8306f68e9b8094ee2884c9568#223002e47a3b1be8306f68e9b8094ee2884c9568"
source = "git+https://github.com/breez/breez-sdk?rev=4e1165bd0f78af6c716c52ab8ba401cfaac76f55#4e1165bd0f78af6c716c52ab8ba401cfaac76f55"
dependencies = [
"aes",
"anyhow",
@@ -4178,7 +4206,7 @@ dependencies = [
"hickory-resolver",
"lazy_static",
"lightning 0.0.118",
"lightning 0.1.2",
"lightning 0.1.2 (git+https://github.com/breez/rust-lightning?rev=1ed173a39d4a415bab8171c3348b459f51f3b5e6)",
"lightning-invoice 0.26.0",
"log",
"maybe-sync",
@@ -4207,7 +4235,7 @@ dependencies = [
[[package]]
name = "sdk-macros"
version = "0.6.2"
source = "git+https://github.com/breez/breez-sdk?rev=223002e47a3b1be8306f68e9b8094ee2884c9568#223002e47a3b1be8306f68e9b8094ee2884c9568"
source = "git+https://github.com/breez/breez-sdk?rev=4e1165bd0f78af6c716c52ab8ba401cfaac76f55#4e1165bd0f78af6c716c52ab8ba401cfaac76f55"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -83,9 +83,9 @@ pub(crate) enum Command {
},
/// Receive a payment directly or via a swap
ReceivePayment {
/// The method to use when receiving. Either "lightning", "bitcoin" or "liquid"
/// The method to use when receiving. Either "invoice", "offer", "bitcoin" or "liquid"
#[arg(short = 'm', long = "method")]
payment_method: Option<PaymentMethod>,
payment_method: Option<String>,
/// Optional description for the invoice
#[clap(short = 'd', long = "description")]
@@ -314,6 +314,16 @@ pub(crate) async fn handle_command(
description,
use_description_hash,
} => {
let payment_method =
payment_method.map_or(Ok(PaymentMethod::Bolt11Invoice), |method| {
match method.as_str() {
"invoice" => Ok(PaymentMethod::Bolt11Invoice),
"offer" => Ok(PaymentMethod::Bolt12Offer),
"bitcoin" => Ok(PaymentMethod::BitcoinAddress),
"liquid" => Ok(PaymentMethod::LiquidAddress),
_ => Err(anyhow!("Invalid payment method")),
}
})?;
let amount = match asset_id {
Some(asset_id) => Some(ReceiveAmount::Asset {
asset_id,
@@ -325,7 +335,7 @@ pub(crate) async fn handle_command(
};
let prepare_response = sdk
.prepare_receive_payment(&PrepareReceiveRequest {
payment_method: payment_method.unwrap_or(PaymentMethod::Lightning),
payment_method,
amount: amount.clone(),
})
.await?;
@@ -359,6 +369,9 @@ pub(crate) async fn handle_command(
match sdk.parse(&response.destination).await? {
InputType::Bolt11 { invoice } => result.push_str(&build_qr_text(&invoice.bolt11)),
InputType::Bolt12Offer { offer, .. } => {
result.push_str(&build_qr_text(&offer.offer))
}
InputType::LiquidAddress { address } => {
result.push_str(&build_qr_text(&address.to_uri().map_err(|e| {
anyhow!("Could not build BIP21 from address data: {e:?}")

82
lib/Cargo.lock generated
View File

@@ -760,7 +760,7 @@ checksum = "829a082bd3761fde7476dc2ed85ca56c11628948460ece621e4f56fef5046567"
[[package]]
name = "boltz-client"
version = "0.3.0"
source = "git+https://github.com/danielgranhao/boltz-rust?rev=9c703b20a86cb8a4d9a0d3d6dc2f610a0389fe7a#9c703b20a86cb8a4d9a0d3d6dc2f610a0389fe7a"
source = "git+https://github.com/dangeross/boltz-rust?rev=0ffdc77b3db6a14484b1a948b78d506a6aebbafe#0ffdc77b3db6a14484b1a948b78d506a6aebbafe"
dependencies = [
"async-trait",
"bip39",
@@ -771,7 +771,7 @@ dependencies = [
"getrandom 0.2.15",
"hex",
"js-sys",
"lightning-invoice 0.32.0",
"lightning 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"log",
"macros",
"reqwest 0.12.12",
@@ -2955,9 +2955,25 @@ dependencies = [
"dnssec-prover",
"hashbrown 0.13.2",
"libm",
"lightning-invoice 0.33.2",
"lightning-types 0.2.0",
"possiblyrandom",
"lightning-invoice 0.33.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lightning-types 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"possiblyrandom 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "lightning"
version = "0.1.2"
source = "git+https://github.com/breez/rust-lightning?rev=1ed173a39d4a415bab8171c3348b459f51f3b5e6#1ed173a39d4a415bab8171c3348b459f51f3b5e6"
dependencies = [
"bech32 0.11.0",
"bitcoin 0.32.5",
"dnssec-prover",
"hashbrown 0.13.2",
"libm",
"lightning-invoice 0.33.2 (git+https://github.com/breez/rust-lightning?rev=1ed173a39d4a415bab8171c3348b459f51f3b5e6)",
"lightning-types 0.2.0 (git+https://github.com/breez/rust-lightning?rev=1ed173a39d4a415bab8171c3348b459f51f3b5e6)",
"musig2",
"possiblyrandom 0.2.0 (git+https://github.com/breez/rust-lightning?rev=1ed173a39d4a415bab8171c3348b459f51f3b5e6)",
]
[[package]]
@@ -2974,17 +2990,6 @@ dependencies = [
"secp256k1 0.24.3",
]
[[package]]
name = "lightning-invoice"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ab9f6ea77e20e3129235e62a2e6bd64ed932363df104e864ee65ccffb54a8f"
dependencies = [
"bech32 0.9.1",
"bitcoin 0.32.5",
"lightning-types 0.1.0",
]
[[package]]
name = "lightning-invoice"
version = "0.33.2"
@@ -2993,18 +2998,17 @@ checksum = "11209f386879b97198b2bfc9e9c1e5d42870825c6bd4376f17f95357244d6600"
dependencies = [
"bech32 0.11.0",
"bitcoin 0.32.5",
"lightning-types 0.2.0",
"lightning-types 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "lightning-types"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1083b8d9137000edf3bfcb1ff011c0d25e0cdd2feb98cc21d6765e64a494148f"
name = "lightning-invoice"
version = "0.33.2"
source = "git+https://github.com/breez/rust-lightning?rev=1ed173a39d4a415bab8171c3348b459f51f3b5e6#1ed173a39d4a415bab8171c3348b459f51f3b5e6"
dependencies = [
"bech32 0.9.1",
"bech32 0.11.0",
"bitcoin 0.32.5",
"hex-conservative 0.2.1",
"lightning-types 0.2.0 (git+https://github.com/breez/rust-lightning?rev=1ed173a39d4a415bab8171c3348b459f51f3b5e6)",
]
[[package]]
@@ -3016,6 +3020,14 @@ dependencies = [
"bitcoin 0.32.5",
]
[[package]]
name = "lightning-types"
version = "0.2.0"
source = "git+https://github.com/breez/rust-lightning?rev=1ed173a39d4a415bab8171c3348b459f51f3b5e6#1ed173a39d4a415bab8171c3348b459f51f3b5e6"
dependencies = [
"bitcoin 0.32.5",
]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
@@ -3184,7 +3196,7 @@ dependencies = [
[[package]]
name = "macros"
version = "0.0.0"
source = "git+https://github.com/danielgranhao/boltz-rust?rev=9c703b20a86cb8a4d9a0d3d6dc2f610a0389fe7a#9c703b20a86cb8a4d9a0d3d6dc2f610a0389fe7a"
source = "git+https://github.com/dangeross/boltz-rust?rev=0ffdc77b3db6a14484b1a948b78d506a6aebbafe#0ffdc77b3db6a14484b1a948b78d506a6aebbafe"
dependencies = [
"proc-macro2",
"quote",
@@ -3334,6 +3346,14 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03"
[[package]]
name = "musig2"
version = "0.1.0"
source = "git+https://github.com/arik-so/rust-musig2?rev=6f95a05718cbb44d8fe3fa6021aea8117aa38d50#6f95a05718cbb44d8fe3fa6021aea8117aa38d50"
dependencies = [
"bitcoin 0.32.5",
]
[[package]]
name = "native-tls"
version = "0.2.14"
@@ -3702,6 +3722,14 @@ dependencies = [
"getrandom 0.2.15",
]
[[package]]
name = "possiblyrandom"
version = "0.2.0"
source = "git+https://github.com/breez/rust-lightning?rev=1ed173a39d4a415bab8171c3348b459f51f3b5e6#1ed173a39d4a415bab8171c3348b459f51f3b5e6"
dependencies = [
"getrandom 0.2.15",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
@@ -4688,7 +4716,7 @@ checksum = "584e070911c7017da6cb2eb0788d09f43d789029b5877d3e5ecc8acf86ceee21"
[[package]]
name = "sdk-common"
version = "0.6.2"
source = "git+https://github.com/breez/breez-sdk?rev=223002e47a3b1be8306f68e9b8094ee2884c9568#223002e47a3b1be8306f68e9b8094ee2884c9568"
source = "git+https://github.com/breez/breez-sdk?rev=4e1165bd0f78af6c716c52ab8ba401cfaac76f55#4e1165bd0f78af6c716c52ab8ba401cfaac76f55"
dependencies = [
"aes",
"anyhow",
@@ -4704,7 +4732,7 @@ dependencies = [
"hickory-resolver",
"lazy_static",
"lightning 0.0.118",
"lightning 0.1.2",
"lightning 0.1.2 (git+https://github.com/breez/rust-lightning?rev=1ed173a39d4a415bab8171c3348b459f51f3b5e6)",
"lightning-invoice 0.26.0",
"log",
"maybe-sync",
@@ -4733,7 +4761,7 @@ dependencies = [
[[package]]
name = "sdk-macros"
version = "0.6.2"
source = "git+https://github.com/breez/breez-sdk?rev=223002e47a3b1be8306f68e9b8094ee2884c9568#223002e47a3b1be8306f68e9b8094ee2884c9568"
source = "git+https://github.com/breez/breez-sdk?rev=4e1165bd0f78af6c716c52ab8ba401cfaac76f55#4e1165bd0f78af6c716c52ab8ba401cfaac76f55"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -37,8 +37,8 @@ anyhow = "1.0"
log = "0.4.20"
once_cell = "1.19"
serde = { version = "1.0", features = ["derive"] }
sdk-common = { git = "https://github.com/breez/breez-sdk", rev = "223002e47a3b1be8306f68e9b8094ee2884c9568", features = ["liquid"] }
sdk-macros = { git = "https://github.com/breez/breez-sdk", rev = "223002e47a3b1be8306f68e9b8094ee2884c9568" }
sdk-common = { git = "https://github.com/breez/breez-sdk", rev = "4e1165bd0f78af6c716c52ab8ba401cfaac76f55", features = ["liquid"] }
sdk-macros = { git = "https://github.com/breez/breez-sdk", rev = "4e1165bd0f78af6c716c52ab8ba401cfaac76f55" }
thiserror = "1.0"
[patch.crates-io]

View File

@@ -23,6 +23,7 @@ object Constants {
@Suppress("unused")
const val MESSAGE_DATA_PAYLOAD = "notification_payload"
const val MESSAGE_TYPE_INVOICE_REQUEST = "invoice_request"
const val MESSAGE_TYPE_LNURL_PAY_INFO = "lnurlpay_info"
const val MESSAGE_TYPE_LNURL_PAY_INVOICE = "lnurlpay_invoice"
const val MESSAGE_TYPE_SWAP_UPDATED = "swap_updated"
@@ -42,6 +43,10 @@ object Constants {
"foreground_service_notification_channel_name"
const val FOREGROUND_SERVICE_NOTIFICATION_TITLE =
"foreground_service_notification_title"
const val INVOICE_REQUEST_NOTIFICATION_TITLE =
"invoice_request_notification_title"
const val INVOICE_REQUEST_NOTIFICATION_FAILURE_TITLE =
"invoice_request_notification_failure_title"
const val LNURL_PAY_INFO_NOTIFICATION_TITLE =
"lnurl_pay_info_notification_title"
const val LNURL_PAY_INVOICE_NOTIFICATION_TITLE =
@@ -93,6 +98,10 @@ object Constants {
"Running in the background"
const val DEFAULT_LNURL_PAY_INFO_NOTIFICATION_TITLE =
"Retrieving Payment Information"
const val DEFAULT_INVOICE_REQUEST_NOTIFICATION_TITLE =
"Fetching Invoice"
const val DEFAULT_INVOICE_REQUEST_NOTIFICATION_FAILURE_TITLE =
"Invoice Request Failed"
const val DEFAULT_LNURL_PAY_INVOICE_NOTIFICATION_TITLE =
"Fetching Invoice"
const val DEFAULT_LNURL_PAY_METADATA_PLAIN_TEXT =

View File

@@ -11,6 +11,7 @@ import breez_sdk_liquid.EventListener
import breez_sdk_liquid.Logger
import breez_sdk_liquid.SdkEvent
import breez_sdk_liquid_notification.BreezSdkLiquidConnector.Companion.connectSDK
import breez_sdk_liquid_notification.Constants.MESSAGE_TYPE_INVOICE_REQUEST
import breez_sdk_liquid_notification.Constants.MESSAGE_TYPE_LNURL_PAY_INFO
import breez_sdk_liquid_notification.Constants.MESSAGE_TYPE_LNURL_PAY_INVOICE
import breez_sdk_liquid_notification.Constants.MESSAGE_TYPE_SWAP_UPDATED
@@ -21,6 +22,7 @@ import breez_sdk_liquid_notification.Constants.SHUTDOWN_DELAY_MS
import breez_sdk_liquid_notification.NotificationHelper.Companion.notifyForegroundService
import breez_sdk_liquid_notification.NotificationHelper.Companion.cancelNotification
import breez_sdk_liquid_notification.job.Job
import breez_sdk_liquid_notification.job.InvoiceRequestJob
import breez_sdk_liquid_notification.job.LnurlPayInfoJob
import breez_sdk_liquid_notification.job.LnurlPayInvoiceJob
import breez_sdk_liquid_notification.job.SwapUpdatedJob
@@ -151,6 +153,14 @@ abstract class ForegroundService :
Message.createFromIntent(intent)?.let { message ->
message.payload?.let { payload ->
when (message.type) {
MESSAGE_TYPE_INVOICE_REQUEST ->
InvoiceRequestJob(
applicationContext,
this,
payload,
logger,
)
MESSAGE_TYPE_SWAP_UPDATED ->
SwapUpdatedJob(
applicationContext,

View File

@@ -0,0 +1,81 @@
package breez_sdk_liquid_notification.job
import android.content.Context
import breez_sdk_liquid.CreateBolt12InvoiceRequest
import breez_sdk_liquid.SdkEvent
import breez_sdk_liquid.BindingLiquidSdk
import breez_sdk_liquid_notification.Constants.DEFAULT_INVOICE_REQUEST_NOTIFICATION_FAILURE_TITLE
import breez_sdk_liquid_notification.Constants.DEFAULT_INVOICE_REQUEST_NOTIFICATION_TITLE
import breez_sdk_liquid_notification.Constants.INVOICE_REQUEST_NOTIFICATION_FAILURE_TITLE
import breez_sdk_liquid_notification.Constants.INVOICE_REQUEST_NOTIFICATION_TITLE
import breez_sdk_liquid_notification.Constants.NOTIFICATION_CHANNEL_REPLACEABLE
import breez_sdk_liquid_notification.NotificationHelper.Companion.notifyChannel
import breez_sdk_liquid_notification.ResourceHelper.Companion.getString
import breez_sdk_liquid_notification.SdkForegroundService
import breez_sdk_liquid_notification.ServiceLogger
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@Serializable
data class InvoiceRequestRequest(
@SerialName("offer") val offer: String,
@SerialName("invoice_request") val invoiceRequest: String,
@SerialName("reply_url") val replyURL: String,
)
@Serializable
data class InvoiceRequestResponse(
val invoice: String,
)
class InvoiceRequestJob(
private val context: Context,
private val fgService: SdkForegroundService,
private val payload: String,
private val logger: ServiceLogger,
) : Job {
companion object {
private const val TAG = "InvoiceRequestJob"
}
override fun start(liquidSDK: BindingLiquidSdk) {
var request: InvoiceRequestRequest? = null
try {
request = Json.decodeFromString(InvoiceRequestRequest.serializer(), payload)
val createBolt12InvoiceResponse =
liquidSDK.createBolt12Invoice(
CreateBolt12InvoiceRequest(request.offer, request.invoiceRequest),
)
val response = InvoiceRequestResponse(createBolt12InvoiceResponse.invoice)
val success = replyServer(Json.encodeToString(response), request.replyURL)
notifyChannel(
context,
NOTIFICATION_CHANNEL_REPLACEABLE,
getString(
context,
if (success) INVOICE_REQUEST_NOTIFICATION_TITLE else INVOICE_REQUEST_NOTIFICATION_FAILURE_TITLE,
if (success) DEFAULT_INVOICE_REQUEST_NOTIFICATION_TITLE else DEFAULT_INVOICE_REQUEST_NOTIFICATION_FAILURE_TITLE,
),
)
} catch (e: Exception) {
logger.log(TAG, "Failed to process invoice request: ${e.message}", "WARN")
notifyChannel(
context,
NOTIFICATION_CHANNEL_REPLACEABLE,
getString(
context,
INVOICE_REQUEST_NOTIFICATION_FAILURE_TITLE,
DEFAULT_INVOICE_REQUEST_NOTIFICATION_FAILURE_TITLE,
),
)
}
fgService.onFinished(this)
}
override fun onEvent(e: SdkEvent) {}
override fun onShutdown() {}
}

View File

@@ -2,6 +2,10 @@ package breez_sdk_liquid_notification.job
import breez_sdk_liquid.BindingLiquidSdk
import breez_sdk_liquid.EventListener
import java.io.DataOutputStream
import java.net.HttpURLConnection
import java.net.URL
interface Job : EventListener {
/** When the notification service is connected to the Breez Liquid SDK
@@ -13,4 +17,23 @@ interface Job : EventListener {
* to cleanup the job.
*/
fun onShutdown()
fun replyServer(
payload: String,
replyURL: String,
): Boolean {
val url = URL(replyURL)
val response = payload.toByteArray()
with(url.openConnection() as HttpURLConnection) {
requestMethod = "POST"
doOutput = true
useCaches = false
setRequestProperty("Content-Type", "application/json")
setRequestProperty("Content-Length", response.size.toString())
DataOutputStream(outputStream).use { it.write(response, 0, response.size) }
return responseCode == 200
}
}
}

View File

@@ -18,25 +18,6 @@ data class LnurlErrorResponse(
interface LnurlPayJob : Job {
override fun onEvent(e: SdkEvent) {}
fun replyServer(
payload: String,
replyURL: String,
): Boolean {
val url = URL(replyURL)
val response = payload.toByteArray()
with(url.openConnection() as HttpURLConnection) {
requestMethod = "POST"
doOutput = true
useCaches = false
setRequestProperty("Content-Type", "application/json")
setRequestProperty("Content-Length", response.size.toString())
DataOutputStream(outputStream).use { it.write(response, 0, response.size) }
return responseCode == 200
}
}
fun fail(
withError: String?,
replyURL: String,

View File

@@ -79,6 +79,11 @@ typedef struct wire_cst_check_message_request {
struct wire_cst_list_prim_u_8_strict *signature;
} wire_cst_check_message_request;
typedef struct wire_cst_create_bolt_12_invoice_request {
struct wire_cst_list_prim_u_8_strict *offer;
struct wire_cst_list_prim_u_8_strict *invoice_request;
} wire_cst_create_bolt_12_invoice_request;
typedef struct wire_cst_fetch_payment_proposed_fees_request {
struct wire_cst_list_prim_u_8_strict *swap_id;
} wire_cst_fetch_payment_proposed_fees_request;
@@ -431,8 +436,8 @@ typedef struct wire_cst_prepare_send_request {
typedef struct wire_cst_prepare_receive_response {
int32_t payment_method;
struct wire_cst_receive_amount *amount;
uint64_t fees_sat;
struct wire_cst_receive_amount *amount;
uint64_t *min_payer_amount_sat;
uint64_t *max_payer_amount_sat;
double *swapper_feerate;
@@ -839,6 +844,10 @@ typedef struct wire_cst_check_message_response {
bool is_valid;
} wire_cst_check_message_response;
typedef struct wire_cst_create_bolt_12_invoice_response {
struct wire_cst_list_prim_u_8_strict *invoice;
} wire_cst_create_bolt_12_invoice_response;
typedef struct wire_cst_wallet_info {
uint64_t balance_sat;
uint64_t pending_send_sat;
@@ -1242,6 +1251,10 @@ void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_buy_bitcoin(int
WireSyncRust2DartDco frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_check_message(uintptr_t that,
struct wire_cst_check_message_request *req);
void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_create_bolt12_invoice(int64_t port_,
uintptr_t that,
struct wire_cst_create_bolt_12_invoice_request *req);
void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_disconnect(int64_t port_,
uintptr_t that);
@@ -1400,6 +1413,8 @@ struct wire_cst_check_message_request *frbgen_breez_liquid_cst_new_box_autoadd_c
struct wire_cst_connect_request *frbgen_breez_liquid_cst_new_box_autoadd_connect_request(void);
struct wire_cst_create_bolt_12_invoice_request *frbgen_breez_liquid_cst_new_box_autoadd_create_bolt_12_invoice_request(void);
double *frbgen_breez_liquid_cst_new_box_autoadd_f_64(double value);
struct wire_cst_fetch_payment_proposed_fees_request *frbgen_breez_liquid_cst_new_box_autoadd_fetch_payment_proposed_fees_request(void);
@@ -1530,6 +1545,7 @@ static int64_t dummy_method_to_enforce_bundling(void) {
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_buy_bitcoin_request);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_check_message_request);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_connect_request);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_create_bolt_12_invoice_request);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_f_64);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_fetch_payment_proposed_fees_request);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_cst_new_box_autoadd_get_payment_request);
@@ -1595,6 +1611,7 @@ static int64_t dummy_method_to_enforce_bundling(void) {
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_backup);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_buy_bitcoin);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_check_message);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_create_bolt12_invoice);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_disconnect);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_empty_wallet_cache);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_fetch_fiat_rates);

View File

@@ -387,6 +387,11 @@ RustBuffer uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_buy_bitco
RustBuffer uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_check_message(void*_Nonnull ptr, RustBuffer req, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_BREEZ_SDK_LIQUID_BINDINGS_FN_METHOD_BINDINGLIQUIDSDK_CREATE_BOLT12_INVOICE
#define UNIFFI_FFIDEF_UNIFFI_BREEZ_SDK_LIQUID_BINDINGS_FN_METHOD_BINDINGLIQUIDSDK_CREATE_BOLT12_INVOICE
RustBuffer uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_create_bolt12_invoice(void*_Nonnull ptr, RustBuffer req, RustCallStatus *_Nonnull out_status
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_BREEZ_SDK_LIQUID_BINDINGS_FN_METHOD_BINDINGLIQUIDSDK_DISCONNECT
#define UNIFFI_FFIDEF_UNIFFI_BREEZ_SDK_LIQUID_BINDINGS_FN_METHOD_BINDINGLIQUIDSDK_DISCONNECT
void uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_disconnect(void*_Nonnull ptr, RustCallStatus *_Nonnull out_status
@@ -925,6 +930,12 @@ uint16_t uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_buy_b
#define UNIFFI_FFIDEF_UNIFFI_BREEZ_SDK_LIQUID_BINDINGS_CHECKSUM_METHOD_BINDINGLIQUIDSDK_CHECK_MESSAGE
uint16_t uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_check_message(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_BREEZ_SDK_LIQUID_BINDINGS_CHECKSUM_METHOD_BINDINGLIQUIDSDK_CREATE_BOLT12_INVOICE
#define UNIFFI_FFIDEF_UNIFFI_BREEZ_SDK_LIQUID_BINDINGS_CHECKSUM_METHOD_BINDINGLIQUIDSDK_CREATE_BOLT12_INVOICE
uint16_t uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_create_bolt12_invoice(void
);
#endif
#ifndef UNIFFI_FFIDEF_UNIFFI_BREEZ_SDK_LIQUID_BINDINGS_CHECKSUM_METHOD_BINDINGLIQUIDSDK_DISCONNECT

View File

@@ -9,11 +9,14 @@ struct Constants {
static let MESSAGE_DATA_TYPE = "notification_type"
static let MESSAGE_DATA_PAYLOAD = "notification_payload"
static let MESSAGE_TYPE_INVOICE_REQUEST = "invoice_request"
static let MESSAGE_TYPE_SWAP_UPDATED = "swap_updated"
static let MESSAGE_TYPE_LNURL_PAY_INFO = "lnurlpay_info"
static let MESSAGE_TYPE_LNURL_PAY_INVOICE = "lnurlpay_invoice"
// Resource Identifiers
static let INVOICE_REQUEST_NOTIFICATION_TITLE = "invoice_request_notification_title"
static let INVOICE_REQUEST_NOTIFICATION_FAILURE_TITLE = "invoice_request_notification_failure_title"
static let LNURL_PAY_INFO_NOTIFICATION_TITLE = "lnurl_pay_info_notification_title"
static let LNURL_PAY_INVOICE_NOTIFICATION_TITLE = "lnurl_pay_invoice_notification_title"
static let LNURL_PAY_METADATA_PLAIN_TEXT = "lnurl_pay_metadata_plain_text"
@@ -26,6 +29,8 @@ struct Constants {
static let SWAP_CONFIRMED_NOTIFICATION_FAILURE_TITLE = "swap_confirmed_notification_failure_title"
// Resource Identifier Defaults
static let DEFAULT_INVOICE_REQUEST_NOTIFICATION_TITLE = "Fetching Invoice"
static let DEFAULT_INVOICE_REQUEST_NOTIFICATION_FAILURE_TITLE = "Invoice Request Failed"
static let DEFAULT_LNURL_PAY_INFO_NOTIFICATION_TITLE = "Retrieving Payment Information"
static let DEFAULT_LNURL_PAY_INVOICE_NOTIFICATION_TITLE = "Fetching Invoice"
static let DEFAULT_LNURL_PAY_METADATA_PLAIN_TEXT = "Pay with LNURL"

View File

@@ -72,6 +72,8 @@ open class SDKNotificationService: UNNotificationServiceExtension {
self.logger.log(tag: TAG, line: "\(notificationType) data string: \(payload)", level: "INFO")
switch(notificationType) {
case Constants.MESSAGE_TYPE_INVOICE_REQUEST:
return InvoiceRequestTask(payload: payload, logger: self.logger, contentHandler: contentHandler, bestAttemptContent: bestAttemptContent)
case Constants.MESSAGE_TYPE_SWAP_UPDATED:
return SwapUpdatedTask(payload: payload, logger: self.logger, contentHandler: contentHandler, bestAttemptContent: bestAttemptContent)
case Constants.MESSAGE_TYPE_LNURL_PAY_INFO:

View File

@@ -0,0 +1,45 @@
import UserNotifications
import Foundation
struct InvoiceRequestRequest: Codable {
let offer: String
let invoice_request: String
let reply_url: String
}
struct InvoiceRequestResponse: Decodable, Encodable {
let invoice: String
init(invoice: String) {
self.invoice = invoice
}
}
class InvoiceRequestTask : ReplyableTask {
fileprivate let TAG = "InvoiceRequestTask"
init(payload: String, logger: ServiceLogger, contentHandler: ((UNNotificationContent) -> Void)? = nil, bestAttemptContent: UNMutableNotificationContent? = nil) {
let successNotificationTitle = ResourceHelper.shared.getString(key: Constants.INVOICE_REQUEST_NOTIFICATION_TITLE, fallback: Constants.DEFAULT_INVOICE_REQUEST_NOTIFICATION_TITLE)
let failNotificationTitle = ResourceHelper.shared.getString(key: Constants.INVOICE_REQUEST_NOTIFICATION_FAILURE_TITLE, fallback: Constants.DEFAULT_INVOICE_REQUEST_NOTIFICATION_FAILURE_TITLE)
super.init(payload: payload, logger: logger, contentHandler: contentHandler, bestAttemptContent: bestAttemptContent, successNotificationTitle: successNotificationTitle, failNotificationTitle: failNotificationTitle)
}
override func start(liquidSDK: BindingLiquidSdk) throws {
var request: InvoiceRequestRequest? = nil
do {
request = try JSONDecoder().decode(InvoiceRequestRequest.self, from: self.payload.data(using: .utf8)!)
} catch let e {
self.logger.log(tag: TAG, line: "failed to decode payload: \(e)", level: "ERROR")
self.displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_REPLACEABLE)
throw e
}
do {
let createBolt12InvoiceRes = try liquidSDK.createBolt12Invoice(req: CreateBolt12InvoiceRequest(offer: request!.offer, invoiceRequest: request!.invoice_request))
self.replyServer(encodable: InvoiceRequestResponse(invoice: createBolt12InvoiceRes.invoice), replyURL: request!.reply_url)
} catch let e {
self.logger.log(tag: TAG, line: "failed to process invoice request: \(e)", level: "ERROR")
self.displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_REPLACEABLE)
}
}
}

View File

@@ -11,52 +11,7 @@ struct LnurlErrorResponse: Decodable, Encodable {
}
}
class LnurlPayTask : TaskProtocol {
var payload: String
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
var logger: ServiceLogger
var successNotificationTitle: String
var failNotificationTitle: String
init(payload: String, logger: ServiceLogger, contentHandler: ((UNNotificationContent) -> Void)? = nil, bestAttemptContent: UNMutableNotificationContent? = nil, successNotificationTitle: String, failNotificationTitle: String) {
self.payload = payload
self.contentHandler = contentHandler
self.bestAttemptContent = bestAttemptContent
self.logger = logger
self.successNotificationTitle = successNotificationTitle;
self.failNotificationTitle = failNotificationTitle;
}
func start(liquidSDK: BindingLiquidSdk) throws {}
public func onEvent(e: SdkEvent) {}
func onShutdown() {
displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_REPLACEABLE)
}
func replyServer(encodable: Encodable, replyURL: String) {
guard let serverReplyURL = URL(string: replyURL) else {
self.displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_REPLACEABLE)
return
}
var request = URLRequest(url: serverReplyURL)
request.httpMethod = "POST"
request.httpBody = try! JSONEncoder().encode(encodable)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
let statusCode = (response as! HTTPURLResponse).statusCode
if statusCode == 200 {
self.displayPushNotification(title: self.successNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_REPLACEABLE)
} else {
self.displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_REPLACEABLE)
return
}
}
task.resume()
}
class LnurlPayTask : ReplyableTask {
func fail(withError: String, replyURL: String, failNotificationTitle: String? = nil) {
if let serverReplyURL = URL(string: replyURL) {
var request = URLRequest(url: serverReplyURL)

View File

@@ -59,3 +59,50 @@ extension TaskProtocol {
}
}
}
class ReplyableTask : TaskProtocol {
var payload: String
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
var logger: ServiceLogger
var successNotificationTitle: String
var failNotificationTitle: String
init(payload: String, logger: ServiceLogger, contentHandler: ((UNNotificationContent) -> Void)? = nil, bestAttemptContent: UNMutableNotificationContent? = nil, successNotificationTitle: String, failNotificationTitle: String) {
self.payload = payload
self.contentHandler = contentHandler
self.bestAttemptContent = bestAttemptContent
self.logger = logger
self.successNotificationTitle = successNotificationTitle;
self.failNotificationTitle = failNotificationTitle;
}
func start(liquidSDK: BindingLiquidSdk) throws {}
public func onEvent(e: SdkEvent) {}
func onShutdown() {
displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_REPLACEABLE)
}
func replyServer(encodable: Encodable, replyURL: String) {
guard let serverReplyURL = URL(string: replyURL) else {
self.displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_REPLACEABLE)
return
}
var request = URLRequest(url: serverReplyURL)
request.httpMethod = "POST"
request.httpBody = try! JSONEncoder().encode(encodable)
let task = URLSession.shared.dataTask(with: request) { data, response, error in
let statusCode = (response as! HTTPURLResponse).statusCode
if statusCode == 200 {
self.displayPushNotification(title: self.successNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_REPLACEABLE)
} else {
self.displayPushNotification(title: self.failNotificationTitle, logger: self.logger, threadIdentifier: Constants.NOTIFICATION_THREAD_REPLACEABLE)
return
}
}
task.resume()
}
}

View File

@@ -459,6 +459,8 @@ dictionary SendPaymentResponse {
enum PaymentMethod {
"Lightning",
"Bolt11Invoice",
"Bolt12Offer",
"BitcoinAddress",
"LiquidAddress",
};
@@ -493,6 +495,15 @@ dictionary ReceivePaymentResponse {
string destination;
};
dictionary CreateBolt12InvoiceRequest {
string offer;
string invoice_request;
};
dictionary CreateBolt12InvoiceResponse {
string invoice;
};
dictionary Limits {
u64 min_sat;
u64 max_sat;
@@ -808,6 +819,9 @@ interface BindingLiquidSdk {
[Throws=PaymentError]
ReceivePaymentResponse receive_payment(ReceivePaymentRequest req);
[Throws=PaymentError]
CreateBolt12InvoiceResponse create_bolt12_invoice(CreateBolt12InvoiceRequest req);
[Throws=PaymentError]
LightningPaymentLimitsResponse fetch_lightning_limits();

View File

@@ -136,6 +136,13 @@ impl BindingLiquidSdk {
rt().block_on(self.sdk.receive_payment(&req))
}
pub fn create_bolt12_invoice(
&self,
req: CreateBolt12InvoiceRequest,
) -> Result<CreateBolt12InvoiceResponse, PaymentError> {
rt().block_on(self.sdk.create_bolt12_invoice(&req))
}
pub fn fetch_lightning_limits(&self) -> Result<LightningPaymentLimitsResponse, PaymentError> {
rt().block_on(self.sdk.fetch_lightning_limits())
}

View File

@@ -77,7 +77,7 @@ maybe-sync = { version = "0.1.1", features = ["sync"] }
prost = "^0.11"
tonic = { version = "^0.8", features = ["tls", "tls-webpki-roots"] }
uuid = { version = "1.8.0", features = ["v4"] }
boltz-client = { git = "https://github.com/danielgranhao/boltz-rust", rev = "9c703b20a86cb8a4d9a0d3d6dc2f610a0389fe7a", features = [
boltz-client = { git = "https://github.com/dangeross/boltz-rust", rev = "0ffdc77b3db6a14484b1a948b78d506a6aebbafe", features = [
"electrum",
] }
rusqlite = { git = "https://github.com/Spxg/rusqlite", rev = "e36644127f31fa6e7ea0999b59432deb4a07f220", features = [
@@ -98,7 +98,7 @@ tonic = { version = "0.12", default-features = false, features = [
"codegen",
"prost",
] }
boltz-client = { git = "https://github.com/danielgranhao/boltz-rust", rev = "9c703b20a86cb8a4d9a0d3d6dc2f610a0389fe7a" }
boltz-client = { git = "https://github.com/dangeross/boltz-rust", rev = "0ffdc77b3db6a14484b1a948b78d506a6aebbafe" }
rusqlite = { git = "https://github.com/Spxg/rusqlite", rev = "e36644127f31fa6e7ea0999b59432deb4a07f220", features = [
"backup",
"bundled",

View File

@@ -134,6 +134,13 @@ impl BindingLiquidSdk {
self.sdk.receive_payment(&req).await
}
pub async fn create_bolt12_invoice(
&self,
req: CreateBolt12InvoiceRequest,
) -> Result<CreateBolt12InvoiceResponse, PaymentError> {
self.sdk.create_bolt12_invoice(&req).await
}
pub async fn fetch_lightning_limits(
&self,
) -> Result<LightningPaymentLimitsResponse, PaymentError> {

View File

@@ -469,7 +469,7 @@ impl ChainSwapHandler {
ensure_sdk!(
matches!(swap.direction, Direction::Incoming),
PaymentError::generic(&format!(
PaymentError::generic(format!(
"Only an incoming chain swap can be a zero-amount swap. Swap ID: {}",
&swap.id
))

View File

@@ -1,6 +1,9 @@
use anyhow::Error;
use lwk_wollet::secp256k1;
use sdk_common::prelude::{LnUrlAuthError, LnUrlPayError, LnUrlWithdrawError};
use sdk_common::{
lightning_with_bolt12::offers::parse::Bolt12SemanticError,
prelude::{LnUrlAuthError, LnUrlPayError, LnUrlWithdrawError},
};
use crate::payjoin::error::PayjoinError;
@@ -127,39 +130,47 @@ pub enum PaymentError {
SignerError { err: String },
}
impl PaymentError {
pub(crate) fn asset_error(err: &str) -> Self {
pub(crate) fn asset_error<S: AsRef<str>>(err: S) -> Self {
Self::AssetError {
err: err.to_string(),
err: err.as_ref().to_string(),
}
}
pub(crate) fn generic(err: &str) -> Self {
pub(crate) fn generic<S: AsRef<str>>(err: S) -> Self {
Self::Generic {
err: err.to_string(),
err: err.as_ref().to_string(),
}
}
pub(crate) fn invalid_invoice(err: &str) -> Self {
pub(crate) fn invalid_invoice<S: AsRef<str>>(err: S) -> Self {
Self::InvalidInvoice {
err: err.to_string(),
err: err.as_ref().to_string(),
}
}
pub(crate) fn invalid_network(err: &str) -> Self {
pub(crate) fn invalid_network<S: AsRef<str>>(err: S) -> Self {
Self::InvalidNetwork {
err: err.to_string(),
err: err.as_ref().to_string(),
}
}
pub(crate) fn receive_error(err: &str) -> Self {
pub(crate) fn receive_error<S: AsRef<str>>(err: S) -> Self {
Self::ReceiveError {
err: err.to_string(),
err: err.as_ref().to_string(),
}
}
pub(crate) fn amount_missing(err: &str) -> Self {
pub(crate) fn amount_missing<S: AsRef<str>>(err: S) -> Self {
Self::AmountMissing {
err: err.to_string(),
err: err.as_ref().to_string(),
}
}
}
impl From<Bolt12SemanticError> for PaymentError {
fn from(err: Bolt12SemanticError) -> Self {
PaymentError::Generic {
err: format!("Failed to create BOLT12 invoice: {err:?}"),
}
}
}

View File

@@ -39,7 +39,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
default_rust_auto_opaque = RustAutoOpaqueNom,
);
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.9.0";
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1264782025;
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 464449310;
// Section: executor
@@ -271,6 +271,55 @@ fn wire__crate__bindings__BindingLiquidSdk_check_message_impl(
},
)
}
fn wire__crate__bindings__BindingLiquidSdk_create_bolt12_invoice_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
that: impl CstDecode<
RustOpaqueNom<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<BindingLiquidSdk>>,
>,
req: impl CstDecode<crate::model::CreateBolt12InvoiceRequest>,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::DcoCodec, _, _, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "BindingLiquidSdk_create_bolt12_invoice",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let api_that = that.cst_decode();
let api_req = req.cst_decode();
move |context| async move {
transform_result_dco::<_, _, crate::error::PaymentError>(
(move || async move {
let mut api_that_guard = None;
let decode_indices_ =
flutter_rust_bridge::for_generated::lockable_compute_decode_order(
vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new(
&api_that, 0, false,
)],
);
for i in decode_indices_ {
match i {
0 => {
api_that_guard =
Some(api_that.lockable_decode_async_ref().await)
}
_ => unreachable!(),
}
}
let api_that_guard = api_that_guard.unwrap();
let output_ok = crate::bindings::BindingLiquidSdk::create_bolt12_invoice(
&*api_that_guard,
api_req,
)
.await?;
Ok(output_ok)
})()
.await,
)
}
},
)
}
fn wire__crate__bindings__BindingLiquidSdk_disconnect_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
that: impl CstDecode<
@@ -2185,8 +2234,10 @@ impl CstDecode<crate::model::PaymentMethod> for i32 {
fn cst_decode(self) -> crate::model::PaymentMethod {
match self {
0 => crate::model::PaymentMethod::Lightning,
1 => crate::model::PaymentMethod::BitcoinAddress,
2 => crate::model::PaymentMethod::LiquidAddress,
1 => crate::model::PaymentMethod::Bolt11Invoice,
2 => crate::model::PaymentMethod::Bolt12Offer,
3 => crate::model::PaymentMethod::BitcoinAddress,
4 => crate::model::PaymentMethod::LiquidAddress,
_ => unreachable!("Invalid variant for PaymentMethod: {}", self),
}
}
@@ -2625,6 +2676,28 @@ impl SseDecode for crate::model::ConnectRequest {
}
}
impl SseDecode for crate::model::CreateBolt12InvoiceRequest {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut var_offer = <String>::sse_decode(deserializer);
let mut var_invoiceRequest = <String>::sse_decode(deserializer);
return crate::model::CreateBolt12InvoiceRequest {
offer: var_offer,
invoice_request: var_invoiceRequest,
};
}
}
impl SseDecode for crate::model::CreateBolt12InvoiceResponse {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut var_invoice = <String>::sse_decode(deserializer);
return crate::model::CreateBolt12InvoiceResponse {
invoice: var_invoice,
};
}
}
impl SseDecode for crate::bindings::CurrencyInfo {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
@@ -4122,8 +4195,10 @@ impl SseDecode for crate::model::PaymentMethod {
let mut inner = <i32>::sse_decode(deserializer);
return match inner {
0 => crate::model::PaymentMethod::Lightning,
1 => crate::model::PaymentMethod::BitcoinAddress,
2 => crate::model::PaymentMethod::LiquidAddress,
1 => crate::model::PaymentMethod::Bolt11Invoice,
2 => crate::model::PaymentMethod::Bolt12Offer,
3 => crate::model::PaymentMethod::BitcoinAddress,
4 => crate::model::PaymentMethod::LiquidAddress,
_ => unreachable!("Invalid variant for PaymentMethod: {}", inner),
};
}
@@ -4264,15 +4339,15 @@ impl SseDecode for crate::model::PrepareReceiveResponse {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
let mut var_paymentMethod = <crate::model::PaymentMethod>::sse_decode(deserializer);
let mut var_amount = <Option<crate::model::ReceiveAmount>>::sse_decode(deserializer);
let mut var_feesSat = <u64>::sse_decode(deserializer);
let mut var_amount = <Option<crate::model::ReceiveAmount>>::sse_decode(deserializer);
let mut var_minPayerAmountSat = <Option<u64>>::sse_decode(deserializer);
let mut var_maxPayerAmountSat = <Option<u64>>::sse_decode(deserializer);
let mut var_swapperFeerate = <Option<f64>>::sse_decode(deserializer);
return crate::model::PrepareReceiveResponse {
payment_method: var_paymentMethod,
amount: var_amount,
fees_sat: var_feesSat,
amount: var_amount,
min_payer_amount_sat: var_minPayerAmountSat,
max_payer_amount_sat: var_maxPayerAmountSat,
swapper_feerate: var_swapperFeerate,
@@ -5274,6 +5349,44 @@ impl flutter_rust_bridge::IntoIntoDart<crate::model::ConnectRequest>
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::model::CreateBolt12InvoiceRequest {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[
self.offer.into_into_dart().into_dart(),
self.invoice_request.into_into_dart().into_dart(),
]
.into_dart()
}
}
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
for crate::model::CreateBolt12InvoiceRequest
{
}
impl flutter_rust_bridge::IntoIntoDart<crate::model::CreateBolt12InvoiceRequest>
for crate::model::CreateBolt12InvoiceRequest
{
fn into_into_dart(self) -> crate::model::CreateBolt12InvoiceRequest {
self
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::model::CreateBolt12InvoiceResponse {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[self.invoice.into_into_dart().into_dart()].into_dart()
}
}
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
for crate::model::CreateBolt12InvoiceResponse
{
}
impl flutter_rust_bridge::IntoIntoDart<crate::model::CreateBolt12InvoiceResponse>
for crate::model::CreateBolt12InvoiceResponse
{
fn into_into_dart(self) -> crate::model::CreateBolt12InvoiceResponse {
self
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for FrbWrapper<crate::bindings::CurrencyInfo> {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[
@@ -6479,8 +6592,10 @@ impl flutter_rust_bridge::IntoDart for crate::model::PaymentMethod {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
match self {
Self::Lightning => 0.into_dart(),
Self::BitcoinAddress => 1.into_dart(),
Self::LiquidAddress => 2.into_dart(),
Self::Bolt11Invoice => 1.into_dart(),
Self::Bolt12Offer => 2.into_dart(),
Self::BitcoinAddress => 3.into_dart(),
Self::LiquidAddress => 4.into_dart(),
_ => unreachable!(),
}
}
@@ -6693,8 +6808,8 @@ impl flutter_rust_bridge::IntoDart for crate::model::PrepareReceiveResponse {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[
self.payment_method.into_into_dart().into_dart(),
self.amount.into_into_dart().into_dart(),
self.fees_sat.into_into_dart().into_dart(),
self.amount.into_into_dart().into_dart(),
self.min_payer_amount_sat.into_into_dart().into_dart(),
self.max_payer_amount_sat.into_into_dart().into_dart(),
self.swapper_feerate.into_into_dart().into_dart(),
@@ -7624,6 +7739,21 @@ impl SseEncode for crate::model::ConnectRequest {
}
}
impl SseEncode for crate::model::CreateBolt12InvoiceRequest {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<String>::sse_encode(self.offer, serializer);
<String>::sse_encode(self.invoice_request, serializer);
}
}
impl SseEncode for crate::model::CreateBolt12InvoiceResponse {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<String>::sse_encode(self.invoice, serializer);
}
}
impl SseEncode for crate::bindings::CurrencyInfo {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
@@ -8839,8 +8969,10 @@ impl SseEncode for crate::model::PaymentMethod {
<i32>::sse_encode(
match self {
crate::model::PaymentMethod::Lightning => 0,
crate::model::PaymentMethod::BitcoinAddress => 1,
crate::model::PaymentMethod::LiquidAddress => 2,
crate::model::PaymentMethod::Bolt11Invoice => 1,
crate::model::PaymentMethod::Bolt12Offer => 2,
crate::model::PaymentMethod::BitcoinAddress => 3,
crate::model::PaymentMethod::LiquidAddress => 4,
_ => {
unimplemented!("");
}
@@ -8956,8 +9088,8 @@ impl SseEncode for crate::model::PrepareReceiveResponse {
// Codec=Sse (Serialization based), see doc to use other codecs
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
<crate::model::PaymentMethod>::sse_encode(self.payment_method, serializer);
<Option<crate::model::ReceiveAmount>>::sse_encode(self.amount, serializer);
<u64>::sse_encode(self.fees_sat, serializer);
<Option<crate::model::ReceiveAmount>>::sse_encode(self.amount, serializer);
<Option<u64>>::sse_encode(self.min_payer_amount_sat, serializer);
<Option<u64>>::sse_encode(self.max_payer_amount_sat, serializer);
<Option<f64>>::sse_encode(self.swapper_feerate, serializer);
@@ -9724,6 +9856,15 @@ mod io {
CstDecode::<crate::model::ConnectRequest>::cst_decode(*wrap).into()
}
}
impl CstDecode<crate::model::CreateBolt12InvoiceRequest>
for *mut wire_cst_create_bolt_12_invoice_request
{
// Codec=Cst (C-struct based), see doc to use other codecs
fn cst_decode(self) -> crate::model::CreateBolt12InvoiceRequest {
let wrap = unsafe { flutter_rust_bridge::for_generated::box_from_leak_ptr(self) };
CstDecode::<crate::model::CreateBolt12InvoiceRequest>::cst_decode(*wrap).into()
}
}
impl CstDecode<f64> for *mut f64 {
// Codec=Cst (C-struct based), see doc to use other codecs
fn cst_decode(self) -> f64 {
@@ -10090,6 +10231,27 @@ mod io {
}
}
}
impl CstDecode<crate::model::CreateBolt12InvoiceRequest>
for wire_cst_create_bolt_12_invoice_request
{
// Codec=Cst (C-struct based), see doc to use other codecs
fn cst_decode(self) -> crate::model::CreateBolt12InvoiceRequest {
crate::model::CreateBolt12InvoiceRequest {
offer: self.offer.cst_decode(),
invoice_request: self.invoice_request.cst_decode(),
}
}
}
impl CstDecode<crate::model::CreateBolt12InvoiceResponse>
for wire_cst_create_bolt_12_invoice_response
{
// Codec=Cst (C-struct based), see doc to use other codecs
fn cst_decode(self) -> crate::model::CreateBolt12InvoiceResponse {
crate::model::CreateBolt12InvoiceResponse {
invoice: self.invoice.cst_decode(),
}
}
}
impl CstDecode<crate::bindings::CurrencyInfo> for wire_cst_currency_info {
// Codec=Cst (C-struct based), see doc to use other codecs
fn cst_decode(self) -> crate::bindings::CurrencyInfo {
@@ -11156,8 +11318,8 @@ mod io {
fn cst_decode(self) -> crate::model::PrepareReceiveResponse {
crate::model::PrepareReceiveResponse {
payment_method: self.payment_method.cst_decode(),
amount: self.amount.cst_decode(),
fees_sat: self.fees_sat.cst_decode(),
amount: self.amount.cst_decode(),
min_payer_amount_sat: self.min_payer_amount_sat.cst_decode(),
max_payer_amount_sat: self.max_payer_amount_sat.cst_decode(),
swapper_feerate: self.swapper_feerate.cst_decode(),
@@ -11813,6 +11975,31 @@ mod io {
Self::new_with_null_ptr()
}
}
impl NewWithNullPtr for wire_cst_create_bolt_12_invoice_request {
fn new_with_null_ptr() -> Self {
Self {
offer: core::ptr::null_mut(),
invoice_request: core::ptr::null_mut(),
}
}
}
impl Default for wire_cst_create_bolt_12_invoice_request {
fn default() -> Self {
Self::new_with_null_ptr()
}
}
impl NewWithNullPtr for wire_cst_create_bolt_12_invoice_response {
fn new_with_null_ptr() -> Self {
Self {
invoice: core::ptr::null_mut(),
}
}
}
impl Default for wire_cst_create_bolt_12_invoice_response {
fn default() -> Self {
Self::new_with_null_ptr()
}
}
impl NewWithNullPtr for wire_cst_currency_info {
fn new_with_null_ptr() -> Self {
Self {
@@ -12519,8 +12706,8 @@ mod io {
fn new_with_null_ptr() -> Self {
Self {
payment_method: Default::default(),
amount: core::ptr::null_mut(),
fees_sat: Default::default(),
amount: core::ptr::null_mut(),
min_payer_amount_sat: core::ptr::null_mut(),
max_payer_amount_sat: core::ptr::null_mut(),
swapper_feerate: core::ptr::null_mut(),
@@ -12942,6 +13129,15 @@ mod io {
wire__crate__bindings__BindingLiquidSdk_check_message_impl(that, req)
}
#[unsafe(no_mangle)]
pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_create_bolt12_invoice(
port_: i64,
that: usize,
req: *mut wire_cst_create_bolt_12_invoice_request,
) {
wire__crate__bindings__BindingLiquidSdk_create_bolt12_invoice_impl(port_, that, req)
}
#[unsafe(no_mangle)]
pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_disconnect(
port_: i64,
@@ -13371,6 +13567,14 @@ mod io {
)
}
#[unsafe(no_mangle)]
pub extern "C" fn frbgen_breez_liquid_cst_new_box_autoadd_create_bolt_12_invoice_request(
) -> *mut wire_cst_create_bolt_12_invoice_request {
flutter_rust_bridge::for_generated::new_leak_box_ptr(
wire_cst_create_bolt_12_invoice_request::new_with_null_ptr(),
)
}
#[unsafe(no_mangle)]
pub extern "C" fn frbgen_breez_liquid_cst_new_box_autoadd_f_64(value: f64) -> *mut f64 {
flutter_rust_bridge::for_generated::new_leak_box_ptr(value)
@@ -14083,6 +14287,17 @@ mod io {
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct wire_cst_create_bolt_12_invoice_request {
offer: *mut wire_cst_list_prim_u_8_strict,
invoice_request: *mut wire_cst_list_prim_u_8_strict,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct wire_cst_create_bolt_12_invoice_response {
invoice: *mut wire_cst_list_prim_u_8_strict,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct wire_cst_currency_info {
name: *mut wire_cst_list_prim_u_8_strict,
fraction_size: u32,
@@ -14966,8 +15181,8 @@ mod io {
#[derive(Clone, Copy)]
pub struct wire_cst_prepare_receive_response {
payment_method: i32,
amount: *mut wire_cst_receive_amount,
fees_sat: u64,
amount: *mut wire_cst_receive_amount,
min_payer_amount_sat: *mut u64,
max_payer_amount_sat: *mut u64,
swapper_feerate: *mut f64,

View File

@@ -170,12 +170,14 @@ pub(crate) mod chain;
pub(crate) mod chain_swap;
pub mod error;
pub(crate) mod event;
#[allow(deprecated)]
#[cfg(feature = "frb")]
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
pub(crate) mod frb_generated;
pub(crate) mod lnurl;
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
pub mod logger;
#[allow(deprecated)]
pub mod model;
pub(crate) mod payjoin;
pub mod persist;

View File

@@ -14,9 +14,9 @@ use lwk_wollet::ElementsNetwork;
use maybe_sync::{MaybeSend, MaybeSync};
use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef};
use rusqlite::ToSql;
use sdk_common::bitcoin::hashes::hex::ToHex as _;
use sdk_common::prelude::*;
use sdk_common::utils::Arc;
use sdk_common::{bitcoin::hashes::hex::ToHex, lightning_with_bolt12::offers::offer::Offer};
use serde::{Deserialize, Serialize};
use std::cmp::PartialEq;
use std::path::PathBuf;
@@ -573,13 +573,13 @@ pub(crate) struct ReservedAddress {
}
/// The send/receive methods supported by the SDK
#[derive(Clone, Debug, EnumString, Serialize, Eq, PartialEq)]
#[derive(Clone, Debug, Serialize)]
pub enum PaymentMethod {
#[strum(serialize = "lightning")]
#[deprecated(since = "0.8.1", note = "Use `Bolt11Invoice` instead")]
Lightning,
#[strum(serialize = "bitcoin")]
Bolt11Invoice,
Bolt12Offer,
BitcoinAddress,
#[strum(serialize = "liquid")]
LiquidAddress,
}
@@ -599,7 +599,6 @@ pub enum ReceiveAmount {
#[derive(Debug, Serialize)]
pub struct PrepareReceiveRequest {
pub payment_method: PaymentMethod,
/// The amount to be paid in either Bitcoin or another asset
pub amount: Option<ReceiveAmount>,
}
@@ -608,8 +607,6 @@ pub struct PrepareReceiveRequest {
#[derive(Debug, Serialize, Clone)]
pub struct PrepareReceiveResponse {
pub payment_method: PaymentMethod,
pub amount: Option<ReceiveAmount>,
/// Generally represents the total fees that would be paid to send or receive this payment.
///
/// In case of Zero-Amount Receive Chain swaps, the swapper service fee (`swapper_feerate` times
@@ -619,17 +616,16 @@ pub struct PrepareReceiveResponse {
///
/// In all other types of swaps, the swapper service fee is included in `fees_sat`.
pub fees_sat: u64,
/// The amount to be paid in either Bitcoin or another asset
pub amount: Option<ReceiveAmount>,
/// The minimum amount the payer can send for this swap to succeed.
///
/// When the method is [PaymentMethod::LiquidAddress], this is empty.
pub min_payer_amount_sat: Option<u64>,
/// The maximum amount the payer can send for this swap to succeed.
///
/// When the method is [PaymentMethod::LiquidAddress], this is empty.
pub max_payer_amount_sat: Option<u64>,
/// The percentage of the sent amount that will count towards the service fee.
///
/// When the method is [PaymentMethod::LiquidAddress], this is empty.
@@ -654,6 +650,22 @@ pub struct ReceivePaymentResponse {
pub destination: String,
}
/// An argument when calling [crate::sdk::LiquidSdk::create_bolt12_invoice].
#[derive(Debug, Serialize)]
pub struct CreateBolt12InvoiceRequest {
/// The BOLT12 offer
pub offer: String,
/// The invoice request created from the offer
pub invoice_request: String,
}
/// Returned when calling [crate::sdk::LiquidSdk::create_bolt12_invoice].
#[derive(Debug, Serialize, Clone)]
pub struct CreateBolt12InvoiceResponse {
/// The BOLT12 invoice
pub invoice: String,
}
/// The minimum and maximum in satoshis of a Lightning or onchain payment.
#[derive(Debug, Serialize)]
pub struct Limits {
@@ -1393,6 +1405,8 @@ pub struct ReceiveSwap {
pub(crate) create_response_json: String,
pub(crate) claim_private_key: String,
pub(crate) invoice: String,
/// The bolt12 offer, if used to create the swap
pub(crate) bolt12_offer: Option<String>,
pub(crate) payment_hash: Option<String>,
pub(crate) destination_pubkey: Option<String>,
pub(crate) description: Option<String>,
@@ -1444,7 +1458,7 @@ impl ReceiveSwap {
let res = CreateReverseResponse {
id: self.id.clone(),
invoice: self.invoice.clone(),
invoice: Some(self.invoice.clone()),
swap_tree: internal_create_response.swap_tree.clone().into(),
lockup_address: internal_create_response.lockup_address.clone(),
refund_public_key: crate::utils::json_to_pubkey(
@@ -1479,7 +1493,7 @@ impl ReceiveSwap {
pub(crate) fn from_boltz_struct_to_json(
create_response: &CreateReverseResponse,
expected_swap_id: &str,
expected_invoice: &str,
expected_invoice: Option<&str>,
) -> Result<String, PaymentError> {
let internal_create_response =
crate::persist::receive::InternalCreateReverseResponse::try_convert_from_boltz(
@@ -1510,6 +1524,35 @@ pub struct RefundableSwap {
pub last_refund_tx_id: Option<String>,
}
/// A BOLT12 offer
#[derive(Clone, Debug, Derivative)]
#[derivative(PartialEq)]
pub(crate) struct Bolt12Offer {
/// The BOLT12 offer string
pub(crate) id: String,
/// The description of the offer
pub(crate) description: String,
/// The private key used to create the offer
pub(crate) private_key: String,
/// The optional webhook URL where the offer invoice request will be sent
pub(crate) webhook_url: Option<String>,
/// The creation time of the offer
pub(crate) created_at: u32,
}
impl Bolt12Offer {
pub(crate) fn get_keypair(&self) -> Result<Keypair, SdkError> {
utils::decode_keypair(&self.private_key)
}
}
impl TryFrom<Bolt12Offer> for Offer {
type Error = SdkError;
fn try_from(val: Bolt12Offer) -> Result<Self, Self::Error> {
Offer::from_str(&val.id)
.map_err(|e| SdkError::generic(format!("Failed to parse BOLT12 offer: {e:?}")))
}
}
/// The payment state of an individual payment.
#[derive(Clone, Copy, Debug, Default, EnumString, Eq, PartialEq, Serialize, Hash)]
#[strum(serialize_all = "lowercase")]
@@ -2432,33 +2475,6 @@ impl From<electrum_client::GetBalanceRes> for BtcScriptBalance {
}
}
#[macro_export]
macro_rules! get_invoice_amount {
($invoice:expr) => {
$invoice
.parse::<Bolt11Invoice>()
.expect("Expecting valid invoice")
.amount_milli_satoshis()
.expect("Expecting valid amount")
/ 1000
};
}
#[macro_export]
macro_rules! get_invoice_description {
($invoice:expr) => {
match $invoice
.trim()
.parse::<Bolt11Invoice>()
.expect("Expecting valid invoice")
.description()
{
Bolt11InvoiceDescription::Direct(msg) => Some(msg.to_string()),
Bolt11InvoiceDescription::Hash(_) => None,
}
};
}
#[macro_export]
macro_rules! get_updated_fields {
($($var:ident),* $(,)?) => {{

View File

@@ -0,0 +1,240 @@
use anyhow::Result;
use rusqlite::{params, Connection, Params, Row};
use crate::model::*;
use crate::persist::Persister;
use crate::sync::model::data::Bolt12OfferSyncData;
use crate::sync::model::RecordType;
impl Persister {
pub(crate) fn insert_or_update_bolt12_offer_inner(
con: &Connection,
bolt12_offer: &Bolt12Offer,
) -> Result<()> {
con.execute(
"
INSERT INTO bolt12_offers (
id,
description,
private_key,
webhook_url,
created_at
)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT (id)
DO UPDATE SET webhook_url = excluded.webhook_url
",
(
&bolt12_offer.id,
&bolt12_offer.description,
&bolt12_offer.private_key,
&bolt12_offer.webhook_url,
&bolt12_offer.created_at,
),
)?;
Ok(())
}
pub(crate) fn insert_or_update_bolt12_offer(&self, bolt12_offer: &Bolt12Offer) -> Result<()> {
let maybe_bolt12_offer = self.fetch_bolt12_offer_by_id(&bolt12_offer.id)?;
let updated_fields = Bolt12OfferSyncData::updated_fields(maybe_bolt12_offer, bolt12_offer);
let mut con = self.get_connection()?;
let tx = con.transaction_with_behavior(rusqlite::TransactionBehavior::Immediate)?;
Self::insert_or_update_bolt12_offer_inner(&tx, bolt12_offer)?;
// Trigger a sync if:
// - updated_fields is None (swap is inserted, not updated)
// - updated_fields in a non empty list of updated fields
if updated_fields.as_ref().is_none_or(|u| !u.is_empty()) {
self.commit_outgoing(
&tx,
&bolt12_offer.id,
RecordType::Bolt12Offer,
updated_fields,
)?;
tx.commit()?;
self.trigger_sync();
} else {
tx.commit()?;
}
Ok(())
}
fn list_bolt12_offers_query(where_clauses: Vec<String>) -> String {
let mut where_clause_str = String::new();
if !where_clauses.is_empty() {
where_clause_str = String::from("WHERE ");
where_clause_str.push_str(where_clauses.join(" AND ").as_str());
}
format!(
"
SELECT
id,
description,
private_key,
webhook_url,
created_at
FROM bolt12_offers
{where_clause_str}
ORDER BY created_at
"
)
}
pub(crate) fn fetch_bolt12_offer_by_id(&self, id: &str) -> Result<Option<Bolt12Offer>> {
let con: Connection = self.get_connection()?;
let query = Self::list_bolt12_offers_query(vec!["id = ?".to_string()]);
let res = con.query_row(&query, [id], Self::sql_row_to_bolt12_offer);
Ok(res.ok())
}
pub(crate) fn fetch_bolt12_offer_by_description(
&self,
description: &str,
) -> Result<Option<Bolt12Offer>> {
let con: Connection = self.get_connection()?;
let query = Self::list_bolt12_offers_query(vec!["description = ?".to_string()]);
let res = con.query_row(&query, [description], Self::sql_row_to_bolt12_offer);
Ok(res.ok())
}
fn sql_row_to_bolt12_offer(row: &Row) -> rusqlite::Result<Bolt12Offer> {
Ok(Bolt12Offer {
id: row.get(0)?,
description: row.get(1)?,
private_key: row.get(2)?,
webhook_url: row.get(3)?,
created_at: row.get(4)?,
})
}
pub(crate) fn list_bolt12_offers(&self) -> Result<Vec<Bolt12Offer>> {
let con: Connection = self.get_connection()?;
self.list_bolt12_offers_where(&con, vec![], params![])
}
pub(crate) fn list_bolt12_offers_where<P>(
&self,
con: &Connection,
where_clauses: Vec<String>,
params: P,
) -> Result<Vec<Bolt12Offer>>
where
P: Params,
{
let query = Self::list_bolt12_offers_query(where_clauses);
let offers = con
.prepare(&query)?
.query_map(params, Self::sql_row_to_bolt12_offer)?
.map(|i| i.unwrap())
.collect();
Ok(offers)
}
pub(crate) fn list_bolt12_offers_by_webhook_url(
&self,
webhook_url: &str,
) -> Result<Vec<Bolt12Offer>> {
let con = self.get_connection()?;
let where_clause = vec!["webhook_url = ?".to_string()];
self.list_bolt12_offers_where(&con, where_clause, params![webhook_url])
}
}
#[cfg(test)]
mod tests {
use crate::test_utils::{bolt12_offer::new_bolt12_offer, persist::create_persister};
use anyhow::{anyhow, Result};
use rusqlite::params;
#[cfg(feature = "browser-tests")]
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[sdk_macros::test_all]
fn test_fetch_bolt12_offer() -> Result<()> {
create_persister!(storage);
let bolt12_offer = new_bolt12_offer(None, Some("http://localhost:4004/notify".to_string()));
storage.insert_or_update_bolt12_offer(&bolt12_offer)?;
// Fetch bolt12 offer by id
assert!(storage.fetch_send_swap_by_id(&bolt12_offer.id).is_ok());
// Fetch bolt12 offer by description
assert!(storage
.fetch_bolt12_offer_by_description(&bolt12_offer.description)
.is_ok());
Ok(())
}
#[sdk_macros::test_all]
fn test_list_bolt12_offers() -> Result<()> {
create_persister!(storage);
storage.insert_or_update_bolt12_offer(&new_bolt12_offer(
None,
Some("http://localhost:4004/notify".to_string()),
))?;
storage.insert_or_update_bolt12_offer(&new_bolt12_offer(
Some("another".to_string()),
Some("http://other.local:4004/notify".to_string()),
))?;
storage.insert_or_update_bolt12_offer(&new_bolt12_offer(
Some("another-other".to_string()),
Some("http://localhost:4004/notify".to_string()),
))?;
let con = storage.get_connection()?;
let offers = storage.list_bolt12_offers_where(&con, vec![], params![])?;
assert_eq!(offers.len(), 3);
let offers = storage.list_bolt12_offers_by_webhook_url("http://localhost:4004/notify")?;
assert_eq!(offers.len(), 2);
Ok(())
}
#[sdk_macros::test_all]
fn test_update_bolt12_offer() -> Result<()> {
create_persister!(storage);
let mut bolt12_offer =
new_bolt12_offer(None, Some("http://localhost:4004/notify".to_string()));
storage.insert_or_update_bolt12_offer(&bolt12_offer)?;
let offers = storage.list_bolt12_offers_by_webhook_url("http://localhost:4004/notify")?;
assert_eq!(offers.len(), 1);
bolt12_offer.webhook_url = Some("http://other.local:4004/notify".to_string());
storage.insert_or_update_bolt12_offer(&bolt12_offer)?;
let offers = storage.list_bolt12_offers_by_webhook_url("http://localhost:4004/notify")?;
assert_eq!(offers.len(), 0);
let offers = storage.list_bolt12_offers_by_webhook_url("http://other.local:4004/notify")?;
assert_eq!(offers.len(), 1);
let mut offer = storage
.fetch_bolt12_offer_by_id(&bolt12_offer.id)?
.ok_or(anyhow!("Could not find BOLT12 offer in database"))?;
assert_eq!(offer.id, bolt12_offer.id);
assert_eq!(
offer.webhook_url,
Some("http://other.local:4004/notify".to_string())
);
offer.webhook_url = None;
storage.insert_or_update_bolt12_offer(&offer)?;
let offers = storage.list_bolt12_offers_by_webhook_url("http://other.local:4004/notify")?;
assert_eq!(offers.len(), 0);
Ok(())
}
}

View File

@@ -328,5 +328,15 @@ pub(crate) fn current_migrations(network: LiquidNetwork) -> Vec<&'static str> {
ALTER TABLE send_swaps ADD COLUMN refund_address TEXT;
ALTER TABLE chain_swaps ADD COLUMN refund_address TEXT;
",
"
CREATE TABLE IF NOT EXISTS bolt12_offers(
id TEXT NOT NULL PRIMARY KEY,
description TEXT NOT NULL,
private_key TEXT NOT NULL,
webhook_url TEXT,
created_at INTEGER NOT NULL
) STRICT;
",
"ALTER TABLE receive_swaps ADD COLUMN bolt12_offer TEXT;",
]
}

View File

@@ -1,6 +1,7 @@
mod address;
pub(crate) mod asset_metadata;
mod backup;
pub(crate) mod bolt12_offer;
pub(crate) mod cache;
pub(crate) mod chain;
mod migrations;
@@ -13,10 +14,9 @@ use std::collections::{HashMap, HashSet};
use std::ops::Not;
use std::{path::PathBuf, str::FromStr};
use crate::lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription};
use crate::model::*;
use crate::sync::model::RecordType;
use crate::{get_invoice_description, utils};
use crate::utils;
use anyhow::{anyhow, Result};
use boltz_client::boltz::{ChainPair, ReversePair, SubmarinePair};
use log::{error, warn};
@@ -501,6 +501,7 @@ impl Persister {
rs.created_at,
rs.timeout_block_height,
rs.invoice,
rs.bolt12_offer,
rs.payment_hash,
rs.destination_pubkey,
rs.description,
@@ -612,67 +613,68 @@ impl Persister {
let maybe_receive_swap_created_at: Option<u32> = row.get(9)?;
let maybe_receive_swap_timeout_block_height: Option<u32> = row.get(10)?;
let maybe_receive_swap_invoice: Option<String> = row.get(11)?;
let maybe_receive_swap_payment_hash: Option<String> = row.get(12)?;
let maybe_receive_swap_destination_pubkey: Option<String> = row.get(13)?;
let maybe_receive_swap_description: Option<String> = row.get(14)?;
let maybe_receive_swap_preimage: Option<String> = row.get(15)?;
let maybe_receive_swap_payer_amount_sat: Option<u64> = row.get(16)?;
let maybe_receive_swap_receiver_amount_sat: Option<u64> = row.get(17)?;
let maybe_receive_swap_receiver_state: Option<PaymentState> = row.get(18)?;
let maybe_receive_swap_pair_fees_json: Option<String> = row.get(19)?;
let maybe_receive_swap_bolt12_offer: Option<String> = row.get(12)?;
let maybe_receive_swap_payment_hash: Option<String> = row.get(13)?;
let maybe_receive_swap_destination_pubkey: Option<String> = row.get(14)?;
let maybe_receive_swap_description: Option<String> = row.get(15)?;
let maybe_receive_swap_preimage: Option<String> = row.get(16)?;
let maybe_receive_swap_payer_amount_sat: Option<u64> = row.get(17)?;
let maybe_receive_swap_receiver_amount_sat: Option<u64> = row.get(18)?;
let maybe_receive_swap_receiver_state: Option<PaymentState> = row.get(19)?;
let maybe_receive_swap_pair_fees_json: Option<String> = row.get(20)?;
let maybe_receive_swap_pair_fees: Option<ReversePair> =
maybe_receive_swap_pair_fees_json.and_then(|pair| serde_json::from_str(&pair).ok());
let maybe_receive_swap_claim_tx_id: Option<String> = row.get(20)?;
let maybe_receive_swap_claim_tx_id: Option<String> = row.get(21)?;
let maybe_send_swap_id: Option<String> = row.get(21)?;
let maybe_send_swap_created_at: Option<u32> = row.get(22)?;
let maybe_send_swap_timeout_block_height: Option<u32> = row.get(23)?;
let maybe_send_swap_invoice: Option<String> = row.get(24)?;
let maybe_send_swap_bolt12_offer: Option<String> = row.get(25)?;
let maybe_send_swap_payment_hash: Option<String> = row.get(26)?;
let maybe_send_swap_destination_pubkey: Option<String> = row.get(27)?;
let maybe_send_swap_description: Option<String> = row.get(28)?;
let maybe_send_swap_preimage: Option<String> = row.get(29)?;
let maybe_send_swap_refund_tx_id: Option<String> = row.get(30)?;
let maybe_send_swap_payer_amount_sat: Option<u64> = row.get(31)?;
let maybe_send_swap_receiver_amount_sat: Option<u64> = row.get(32)?;
let maybe_send_swap_state: Option<PaymentState> = row.get(33)?;
let maybe_send_swap_pair_fees_json: Option<String> = row.get(34)?;
let maybe_send_swap_id: Option<String> = row.get(22)?;
let maybe_send_swap_created_at: Option<u32> = row.get(23)?;
let maybe_send_swap_timeout_block_height: Option<u32> = row.get(24)?;
let maybe_send_swap_invoice: Option<String> = row.get(25)?;
let maybe_send_swap_bolt12_offer: Option<String> = row.get(26)?;
let maybe_send_swap_payment_hash: Option<String> = row.get(27)?;
let maybe_send_swap_destination_pubkey: Option<String> = row.get(28)?;
let maybe_send_swap_description: Option<String> = row.get(29)?;
let maybe_send_swap_preimage: Option<String> = row.get(30)?;
let maybe_send_swap_refund_tx_id: Option<String> = row.get(31)?;
let maybe_send_swap_payer_amount_sat: Option<u64> = row.get(32)?;
let maybe_send_swap_receiver_amount_sat: Option<u64> = row.get(33)?;
let maybe_send_swap_state: Option<PaymentState> = row.get(34)?;
let maybe_send_swap_pair_fees_json: Option<String> = row.get(35)?;
let maybe_send_swap_pair_fees: Option<SubmarinePair> =
maybe_send_swap_pair_fees_json.and_then(|pair| serde_json::from_str(&pair).ok());
let maybe_chain_swap_id: Option<String> = row.get(35)?;
let maybe_chain_swap_created_at: Option<u32> = row.get(36)?;
let maybe_chain_swap_timeout_block_height: Option<u32> = row.get(37)?;
let maybe_chain_swap_direction: Option<Direction> = row.get(38)?;
let maybe_chain_swap_preimage: Option<String> = row.get(39)?;
let maybe_chain_swap_description: Option<String> = row.get(40)?;
let maybe_chain_swap_refund_tx_id: Option<String> = row.get(41)?;
let maybe_chain_swap_payer_amount_sat: Option<u64> = row.get(42)?;
let maybe_chain_swap_receiver_amount_sat: Option<u64> = row.get(43)?;
let maybe_chain_swap_claim_address: Option<String> = row.get(44)?;
let maybe_chain_swap_state: Option<PaymentState> = row.get(45)?;
let maybe_chain_swap_pair_fees_json: Option<String> = row.get(46)?;
let maybe_chain_swap_id: Option<String> = row.get(36)?;
let maybe_chain_swap_created_at: Option<u32> = row.get(37)?;
let maybe_chain_swap_timeout_block_height: Option<u32> = row.get(38)?;
let maybe_chain_swap_direction: Option<Direction> = row.get(39)?;
let maybe_chain_swap_preimage: Option<String> = row.get(40)?;
let maybe_chain_swap_description: Option<String> = row.get(41)?;
let maybe_chain_swap_refund_tx_id: Option<String> = row.get(42)?;
let maybe_chain_swap_payer_amount_sat: Option<u64> = row.get(43)?;
let maybe_chain_swap_receiver_amount_sat: Option<u64> = row.get(44)?;
let maybe_chain_swap_claim_address: Option<String> = row.get(45)?;
let maybe_chain_swap_state: Option<PaymentState> = row.get(46)?;
let maybe_chain_swap_pair_fees_json: Option<String> = row.get(47)?;
let maybe_chain_swap_pair_fees: Option<ChainPair> =
maybe_chain_swap_pair_fees_json.and_then(|pair| serde_json::from_str(&pair).ok());
let maybe_chain_swap_actual_payer_amount_sat: Option<u64> = row.get(47)?;
let maybe_chain_swap_accepted_receiver_amount_sat: Option<u64> = row.get(48)?;
let maybe_chain_swap_auto_accepted_fees: Option<bool> = row.get(49)?;
let maybe_chain_swap_claim_tx_id: Option<String> = row.get(50)?;
let maybe_chain_swap_actual_payer_amount_sat: Option<u64> = row.get(48)?;
let maybe_chain_swap_accepted_receiver_amount_sat: Option<u64> = row.get(49)?;
let maybe_chain_swap_auto_accepted_fees: Option<bool> = row.get(50)?;
let maybe_chain_swap_claim_tx_id: Option<String> = row.get(51)?;
let maybe_swap_refund_tx_amount_sat: Option<u64> = row.get(51)?;
let maybe_swap_refund_tx_amount_sat: Option<u64> = row.get(52)?;
let maybe_payment_details_destination: Option<String> = row.get(52)?;
let maybe_payment_details_description: Option<String> = row.get(53)?;
let maybe_payment_details_lnurl_info_json: Option<String> = row.get(54)?;
let maybe_payment_details_destination: Option<String> = row.get(53)?;
let maybe_payment_details_description: Option<String> = row.get(54)?;
let maybe_payment_details_lnurl_info_json: Option<String> = row.get(55)?;
let maybe_payment_details_lnurl_info: Option<LnUrlInfo> =
maybe_payment_details_lnurl_info_json.and_then(|info| serde_json::from_str(&info).ok());
let maybe_payment_details_bip353_address: Option<String> = row.get(55)?;
let maybe_payment_details_asset_fees: Option<u64> = row.get(56)?;
let maybe_payment_details_bip353_address: Option<String> = row.get(56)?;
let maybe_payment_details_asset_fees: Option<u64> = row.get(57)?;
let maybe_asset_metadata_name: Option<String> = row.get(57)?;
let maybe_asset_metadata_ticker: Option<String> = row.get(58)?;
let maybe_asset_metadata_precision: Option<u8> = row.get(59)?;
let maybe_asset_metadata_name: Option<String> = row.get(58)?;
let maybe_asset_metadata_ticker: Option<String> = row.get(59)?;
let maybe_asset_metadata_precision: Option<u8> = row.get(60)?;
let (swap, payment_type) = match maybe_receive_swap_id {
Some(receive_swap_id) => {
@@ -687,12 +689,14 @@ impl Persister {
.unwrap_or(0),
preimage: maybe_receive_swap_preimage,
invoice: maybe_receive_swap_invoice.clone(),
bolt12_offer: None, // Bolt12 not supported for Receive Swaps
bolt12_offer: maybe_receive_swap_bolt12_offer,
payment_hash: maybe_receive_swap_payment_hash,
destination_pubkey: maybe_receive_swap_destination_pubkey,
description: maybe_receive_swap_description.unwrap_or_else(|| {
maybe_receive_swap_invoice
.and_then(|bolt11| get_invoice_description!(bolt11))
.and_then(|invoice| {
utils::get_invoice_description(&invoice).ok().flatten()
})
.unwrap_or("Lightning payment".to_string())
}),
payer_amount_sat,

View File

@@ -63,6 +63,7 @@ impl Persister {
let rows_affected = con.execute(
"UPDATE receive_swaps
SET
bolt12_offer = :bolt12_offer,
description = :description,
claim_address = :claim_address,
claim_tx_id = :claim_tx_id,
@@ -76,6 +77,7 @@ impl Persister {
version = :version",
named_params! {
":id": &receive_swap.id,
":bolt12_offer": &receive_swap.bolt12_offer,
":description": &receive_swap.description,
":claim_address": &receive_swap.claim_address,
":claim_tx_id": &receive_swap.claim_tx_id,
@@ -140,6 +142,7 @@ impl Persister {
rs.create_response_json,
rs.claim_private_key,
rs.invoice,
rs.bolt12_offer,
rs.payment_hash,
rs.destination_pubkey,
rs.timeout_block_height,
@@ -193,25 +196,26 @@ impl Persister {
create_response_json: row.get(2)?,
claim_private_key: row.get(3)?,
invoice: row.get(4)?,
payment_hash: row.get(5)?,
destination_pubkey: row.get(6)?,
timeout_block_height: row.get(7)?,
description: row.get(8)?,
payer_amount_sat: row.get(9)?,
receiver_amount_sat: row.get(10)?,
claim_fees_sat: row.get(11)?,
claim_address: row.get(12)?,
claim_tx_id: row.get(13)?,
lockup_tx_id: row.get(14)?,
mrh_address: row.get(15)?,
mrh_tx_id: row.get(16)?,
created_at: row.get(17)?,
state: row.get(18)?,
pair_fees_json: row.get(19)?,
bolt12_offer: row.get(5)?,
payment_hash: row.get(6)?,
destination_pubkey: row.get(7)?,
timeout_block_height: row.get(8)?,
description: row.get(9)?,
payer_amount_sat: row.get(10)?,
receiver_amount_sat: row.get(11)?,
claim_fees_sat: row.get(12)?,
claim_address: row.get(13)?,
claim_tx_id: row.get(14)?,
lockup_tx_id: row.get(15)?,
mrh_address: row.get(16)?,
mrh_tx_id: row.get(17)?,
created_at: row.get(18)?,
state: row.get(19)?,
pair_fees_json: row.get(20)?,
metadata: SwapMetadata {
version: row.get(20)?,
last_updated_at: row.get(21)?,
is_local: row.get::<usize, Option<bool>>(22)?.unwrap_or(true),
version: row.get(21)?,
last_updated_at: row.get(22)?,
is_local: row.get::<usize, Option<bool>>(23)?.unwrap_or(true),
},
})
}
@@ -385,7 +389,7 @@ impl InternalCreateReverseResponse {
pub(crate) fn try_convert_from_boltz(
boltz_create_response: &CreateReverseResponse,
expected_swap_id: &str,
expected_invoice: &str,
expected_invoice: Option<&str>,
) -> Result<Self, PaymentError> {
// Do not store the CreateResponse fields that are already stored separately
// Before skipping them, ensure they match the separately stored ones
@@ -393,10 +397,15 @@ impl InternalCreateReverseResponse {
boltz_create_response.id == expected_swap_id,
PaymentError::PersistError
);
ensure_sdk!(
boltz_create_response.invoice == expected_invoice,
PaymentError::PersistError
);
match (&boltz_create_response.invoice, expected_invoice) {
(Some(invoice), Some(expected_invoice)) => {
ensure_sdk!(invoice == expected_invoice, PaymentError::PersistError);
}
(None, None) => {}
_ => {
return Err(PaymentError::PersistError);
}
}
let res = InternalCreateReverseResponse {
swap_tree: boltz_create_response.swap_tree.clone().into(),

View File

@@ -9,6 +9,7 @@ use tokio::sync::broadcast;
use super::{cache::KEY_LAST_DERIVATION_INDEX, PaymentTxDetails, Persister, Swap};
use crate::{
model::Bolt12Offer,
persist::where_clauses_to_string,
sync::model::{
data::LAST_DERIVATION_INDEX_DATA_ID, Record, RecordType, SyncOutgoingChanges, SyncSettings,
@@ -496,6 +497,33 @@ impl Persister {
Ok(())
}
pub(crate) fn commit_incoming_bolt12_offer(
&self,
bolt12_offer: Bolt12Offer,
sync_state: &SyncState,
last_commit_time: Option<u32>,
) -> Result<()> {
let mut con = self.get_connection()?;
let tx = con.transaction_with_behavior(TransactionBehavior::Immediate)?;
if let Some(last_commit_time) = last_commit_time {
Self::check_commit_update(&tx, &sync_state.record_id, last_commit_time)?;
}
Self::insert_or_update_bolt12_offer_inner(&tx, &bolt12_offer)?;
Self::set_sync_state_stmt(&tx)?.execute(named_params! {
":data_id": &sync_state.data_id,
":record_id": &sync_state.record_id,
":record_revision": &sync_state.record_revision,
":is_local": &sync_state.is_local,
})?;
tx.commit()?;
Ok(())
}
pub(crate) fn subscribe_sync_trigger(&self) -> Result<broadcast::Receiver<()>> {
match self.sync_trigger {
Some(ref sender) => Ok(sender.subscribe()),

View File

@@ -272,6 +272,7 @@ mod test {
create_response_json: r#"{"swap_tree":{"claim_leaf":{"output":"82012088a91460bac83421a184c3cf912ae231df8e3f0ce6ac5488204c9f9e348b27b1257c51f3ad2a05589ac8f3af72246ff3094950441cdf826b47ac","version":196},"refund_leaf":{"output":"209916729fe59068c8544b8070a32f653ed9cb550e76a5caaeda557aadf9e2cc2fad03e48c31b1","version":196}},"lockup_address":"lq1pqw632yu95t23pa7jr4s746g68nwl0ukkfvncs3q7t66f9gjqj7ccj2nwx8verw57l2zn029vlnwjuvrpm4yxnz3tccfks9e8rdy2r9tu586g8fya887j","refund_public_key":"029916729fe59068c8544b8070a32f653ed9cb550e76a5caaeda557aadf9e2cc2f","timeout_block_height":3247332,"onchain_amount":1071,"blinding_key":"605f50d0c0516c800594e1d44b9ceaeb7fa7a4258d6357043cc2daaa13e48895"}"#.to_string(),
claim_private_key: "2f23dbb3c13e30ac8df594369b62ef1eb34a50197d7acc15db413961d90810e5".to_string(),
invoice: "lnbc11u1pn65lr9sp5xfmwgmaddn2acwc7rr4xhj3k5dy4tyfhma57tpfp0z7eyp90fdjspp5a48w03jc5dtzqnyyqw727naffpcdvhj7s9hen45zh9m3auhfmx2qdpz2djkuepqw3hjqnpdgf2yxgrpv3j8yetnwvxqyp2xqcqz95rzjqfxfl8353vnmzftu28e662s9tzdv3ua0wgjxlucff9gyg8xlsf45wzzxeyqq28qqqqqqqqqqqqqqq9gq2y9qyysgq37h36xnz7khazpus03846hml4q8y8qekrzwh5ql36fy6l7dmgyuq3d9jyvnmm3h8tmxn7ae20wgte2elq4akpu3mqnyj626zy69drmqq95tqch".to_string(),
bolt12_offer: None,
payment_hash: Some("ed4ee7c658a356204c8403bcaf4fa94870d65e5e816f99d682b9771ef2e9d994".to_string()),
destination_pubkey: Some("03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f".to_string()),
description: Some("Test payment".to_string()),

File diff suppressed because it is too large Load Diff

View File

@@ -8,9 +8,10 @@ use crate::{
use anyhow::{anyhow, Result};
use boltz_client::{
boltz::{
self, BoltzApiClientV2, ChainPair, Cooperative, CreateChainRequest, CreateChainResponse,
CreateReverseRequest, CreateReverseResponse, CreateSubmarineRequest,
CreateSubmarineResponse, ReversePair, SubmarineClaimTxResponse, SubmarinePair,
self, BoltzApiClientV2, ChainPair, Cooperative, CreateBolt12OfferRequest,
CreateChainRequest, CreateChainResponse, CreateReverseRequest, CreateReverseResponse,
CreateSubmarineRequest, CreateSubmarineResponse, GetBolt12ParamsResponse, GetNodesResponse,
ReversePair, SubmarineClaimTxResponse, SubmarinePair, UpdateBolt12OfferRequest, WsRequest,
},
elements::secp256k1_zkp::{MusigPartialSignature, MusigPubNonce},
network::Chain,
@@ -43,14 +44,16 @@ pub struct BoltzSwapper<P: ProxyUrlFetcher> {
liquid_client: OnceLock<LiquidClient>,
bitcoin_client: OnceLock<BitcoinClient>,
proxy_url: Arc<P>,
subscription_notifier: broadcast::Sender<String>,
request_notifier: broadcast::Sender<WsRequest>,
update_notifier: broadcast::Sender<boltz::SwapStatus>,
invoice_request_notifier: broadcast::Sender<boltz::InvoiceRequest>,
}
impl<P: ProxyUrlFetcher> BoltzSwapper<P> {
pub fn new(config: Config, proxy_url: Arc<P>) -> Result<Self, SdkError> {
let (subscription_notifier, _) = broadcast::channel::<String>(30);
let (request_notifier, _) = broadcast::channel::<WsRequest>(30);
let (update_notifier, _) = broadcast::channel::<boltz::SwapStatus>(30);
let (invoice_request_notifier, _) = broadcast::channel::<boltz::InvoiceRequest>(30);
Ok(Self {
proxy_url,
@@ -58,8 +61,9 @@ impl<P: ProxyUrlFetcher> BoltzSwapper<P> {
boltz_client: OnceLock::new(),
liquid_client: OnceLock::new(),
bitcoin_client: OnceLock::new(),
subscription_notifier,
request_notifier,
update_notifier,
invoice_request_notifier,
})
}
@@ -442,7 +446,7 @@ impl<P: ProxyUrlFetcher> Swapper for BoltzSwapper<P> {
Swap::Chain(chain_swap) => match chain_swap.direction {
Direction::Incoming => {
let Some(broadcast_fee_rate_sat_per_vb) = broadcast_fee_rate_sat_per_vb else {
return Err(PaymentError::generic(&format!("No broadcast fee rate provided when refunding incoming Chain Swap {swap_id}")));
return Err(PaymentError::generic(format!("No broadcast fee rate provided when refunding incoming Chain Swap {swap_id}")));
};
Transaction::Bitcoin(
@@ -520,4 +524,46 @@ impl<P: ProxyUrlFetcher> Swapper for BoltzSwapper<P> {
info!("Received BOLT12 invoice response: {invoice_res:?}");
Ok(invoice_res.invoice)
}
async fn create_bolt12_offer(&self, req: CreateBolt12OfferRequest) -> Result<(), SdkError> {
self.get_boltz_client()
.await?
.inner
.post_bolt12_offer(req)
.await?;
Ok(())
}
async fn update_bolt12_offer(&self, req: UpdateBolt12OfferRequest) -> Result<(), SdkError> {
self.get_boltz_client()
.await?
.inner
.patch_bolt12_offer(req)
.await?;
Ok(())
}
async fn delete_bolt12_offer(&self, offer: &str, signature: &str) -> Result<(), SdkError> {
self.get_boltz_client()
.await?
.inner
.delete_bolt12_offer(offer, signature)
.await?;
Ok(())
}
async fn get_bolt12_params(&self) -> Result<GetBolt12ParamsResponse, SdkError> {
let res = self
.get_boltz_client()
.await?
.inner
.get_bolt12_params()
.await?;
Ok(res)
}
async fn get_nodes(&self) -> Result<GetNodesResponse, SdkError> {
let res = self.get_boltz_client().await?.inner.get_nodes().await?;
Ok(res)
}
}

View File

@@ -8,7 +8,7 @@ use anyhow::Result;
use boltz_client::boltz::{
self,
tokio_tungstenite_wasm::{Message, WebSocketStream},
WsRequest, WsResponse,
InvoiceRequestParams, WsRequest, WsResponse,
};
use futures_util::{stream::SplitSink, SinkExt, StreamExt};
use log::{debug, error, info, warn};
@@ -17,20 +17,17 @@ use tokio::sync::{broadcast, watch};
use tokio_with_wasm::alias as tokio;
impl<P: ProxyUrlFetcher> BoltzSwapper<P> {
async fn send_subscription(
async fn send_request(
&self,
swap_id: String,
ws_request: WsRequest,
sender: &mut SplitSink<WebSocketStream, Message>,
) {
info!("Subscribing to status updates for swap ID {swap_id}");
let subscription = WsRequest::subscribe_swap_request(&swap_id);
match serde_json::to_string(&subscription) {
Ok(subscribe_json) => match sender.send(Message::Text(subscribe_json.into())).await {
Ok(_) => info!("Subscribed"),
Err(e) => error!("Failed to subscribe to {swap_id}: {e:?}"),
match serde_json::to_string(&ws_request) {
Ok(ref req_json) => match sender.send(Message::Text(req_json.into())).await {
Ok(_) => debug!("Sent request: {req_json}"),
Err(e) => error!("Failed to send {req_json}: {e:?}"),
},
Err(e) => error!("Invalid subscription msg: {e:?}"),
Err(e) => error!("Error encoding request: {e:?}"),
}
}
}
@@ -60,10 +57,10 @@ impl<P: ProxyUrlFetcher> SwapperStatusStream for BoltzSwapper<P> {
Ok(ws_stream) => {
let (mut sender, mut receiver) = ws_stream.split();
let mut tracked_swap_ids: HashSet<String> = HashSet::new();
let mut subscription_stream = self.subscription_notifier.subscribe();
let mut tracked_ids: HashSet<String> = HashSet::new();
let mut request_stream = self.request_notifier.subscribe();
callback.subscribe_swaps().await;
callback.track_subscriptions().await;
loop {
tokio::select! {
@@ -84,15 +81,21 @@ impl<P: ProxyUrlFetcher> SwapperStatusStream for BoltzSwapper<P> {
}
},
swap_res = subscription_stream.recv() => match swap_res {
Ok(swap_id) => {
if !tracked_swap_ids.contains(&swap_id) {
self.send_subscription(swap_id.clone(), &mut sender).await;
tracked_swap_ids.insert(swap_id.clone());
}
ws_request_res = request_stream.recv() => match ws_request_res {
Ok(WsRequest::Subscribe(subscribe)) => {
let id = match subscribe.clone() {
boltz::SubscribeRequest::SwapUpdate { args } => args.first().cloned(),
boltz::SubscribeRequest::InvoiceRequest { args } => args.first().map(|p| p.offer.clone()),
};
if let Some(id) = id {
if !tracked_ids.contains(&id) {
self.send_request(WsRequest::Subscribe(subscribe), &mut sender).await;
tracked_ids.insert(id);
}
}
},
Err(e) => error!("Received error on subscription stream: {e:?}"),
Ok(ws_request) => self.send_request(ws_request, &mut sender).await,
Err(e) => error!("Received error on request stream: {e:?}"),
},
maybe_next = receiver.next() => match maybe_next {
@@ -104,18 +107,30 @@ impl<P: ProxyUrlFetcher> SwapperStatusStream for BoltzSwapper<P> {
},
Ok(Message::Text(payload)) => {
let payload = payload.as_str();
info!("Received text msg: {payload:?}");
debug!("Received text msg: {payload:?}");
match serde_json::from_str::<WsResponse>(payload) {
// Subscribing/unsubscribing confirmation
Ok(WsResponse::Subscribe { .. }) | Ok(WsResponse::Unsubscribe { .. }) => {}
// Status update(s)
// Swap status update(s)
Ok(WsResponse::Update(update)) => {
for update in update.args {
let _ = self.update_notifier.send(update);
}
}
// Invoice requests(s)
Ok(WsResponse::InvoiceRequest(invoice_request)) => {
for invoice_request in invoice_request.args {
let _ = self.invoice_request_notifier.send(invoice_request);
}
}
// Error response
Ok(WsResponse::Error(error)) => {
error!("Received error msg: {error:?}");
}
// A response to one of our pings
Ok(WsResponse::Pong) => debug!("Received pong"),
@@ -150,11 +165,41 @@ impl<P: ProxyUrlFetcher> SwapperStatusStream for BoltzSwapper<P> {
}
fn track_swap_id(&self, swap_id: &str) -> Result<()> {
let _ = self.subscription_notifier.send(swap_id.to_string());
let _ =
self.request_notifier
.send(WsRequest::Subscribe(boltz::SubscribeRequest::SwapUpdate {
args: vec![swap_id.to_string()],
}));
Ok(())
}
fn track_offer(&self, offer: &str, signature: &str) -> Result<()> {
let _ = self.request_notifier.send(WsRequest::Subscribe(
boltz::SubscribeRequest::InvoiceRequest {
args: vec![InvoiceRequestParams {
offer: offer.to_string(),
signature: signature.to_string(),
}],
},
));
Ok(())
}
fn send_invoice_created(&self, id: &str, invoice: &str) -> Result<()> {
let _ = self
.request_notifier
.send(WsRequest::Invoice(boltz::InvoiceCreated {
id: id.to_string(),
invoice: invoice.to_string(),
}));
Ok(())
}
fn subscribe_swap_updates(&self) -> broadcast::Receiver<boltz::SwapStatus> {
self.update_notifier.subscribe()
}
fn subscribe_invoice_requests(&self) -> broadcast::Receiver<boltz::InvoiceRequest> {
self.invoice_request_notifier.subscribe()
}
}

View File

@@ -1,9 +1,10 @@
use anyhow::Result;
use boltz_client::{
boltz::{
ChainPair, CreateChainRequest, CreateChainResponse, CreateReverseRequest,
CreateReverseResponse, CreateSubmarineRequest, CreateSubmarineResponse, ReversePair,
SubmarineClaimTxResponse, SubmarinePair,
ChainPair, CreateBolt12OfferRequest, CreateChainRequest, CreateChainResponse,
CreateReverseRequest, CreateReverseResponse, CreateSubmarineRequest,
CreateSubmarineResponse, GetBolt12ParamsResponse, GetNodesResponse, ReversePair,
SubmarineClaimTxResponse, SubmarinePair, UpdateBolt12OfferRequest,
},
network::Chain,
Amount,
@@ -129,6 +130,16 @@ pub trait Swapper: MaybeSend + MaybeSync {
offer: &str,
amount_sat: u64,
) -> Result<String, PaymentError>;
async fn create_bolt12_offer(&self, req: CreateBolt12OfferRequest) -> Result<(), SdkError>;
async fn update_bolt12_offer(&self, req: UpdateBolt12OfferRequest) -> Result<(), SdkError>;
async fn delete_bolt12_offer(&self, offer: &str, signature: &str) -> Result<(), SdkError>;
async fn get_bolt12_params(&self) -> Result<GetBolt12ParamsResponse, SdkError>;
async fn get_nodes(&self) -> Result<GetNodesResponse, SdkError>;
}
pub trait SwapperStatusStream: MaybeSend + MaybeSync {
@@ -137,8 +148,16 @@ pub trait SwapperStatusStream: MaybeSend + MaybeSync {
callback: Box<dyn SubscriptionHandler>,
shutdown: watch::Receiver<()>,
);
fn track_swap_id(&self, swap_id: &str) -> Result<()>;
fn track_offer(&self, offer: &str, signature: &str) -> Result<()>;
fn send_invoice_created(&self, id: &str, invoice: &str) -> Result<()>;
fn subscribe_swap_updates(&self) -> broadcast::Receiver<boltz_client::boltz::SwapStatus>;
fn subscribe_invoice_requests(
&self,
) -> broadcast::Receiver<boltz_client::boltz::InvoiceRequest>;
}
#[sdk_macros::async_trait]

View File

@@ -1,15 +1,15 @@
use log::{error, info};
use maybe_sync::{MaybeSend, MaybeSync};
use crate::persist::Persister;
use sdk_common::bitcoin::hashes::hex::ToHex;
use sdk_common::utils::Arc;
use crate::{persist::Persister, utils};
use super::SwapperStatusStream;
#[sdk_macros::async_trait]
pub trait SubscriptionHandler: MaybeSend + MaybeSync {
async fn subscribe_swaps(&self);
async fn track_subscriptions(&self);
}
#[derive(Clone)]
@@ -32,7 +32,7 @@ impl SwapperSubscriptionHandler {
#[sdk_macros::async_trait]
impl SubscriptionHandler for SwapperSubscriptionHandler {
async fn subscribe_swaps(&self) {
async fn track_subscriptions(&self) {
match self.persister.list_ongoing_swaps() {
Ok(initial_ongoing_swaps) => {
info!(
@@ -48,5 +48,35 @@ impl SubscriptionHandler for SwapperSubscriptionHandler {
}
Err(e) => error!("Failed to list initial ongoing swaps: {e:?}"),
}
match self.persister.list_bolt12_offers() {
Ok(initial_bolt12_offers) => {
info!(
"On stream reconnection, got {} initial BOLT12 offers",
initial_bolt12_offers.len()
);
for bolt12_offer in initial_bolt12_offers {
let offer = &bolt12_offer.id;
let Ok(keypair) = bolt12_offer.get_keypair() else {
error!("Failed to get keypair for BOLT12 offer: {offer}");
continue;
};
let Ok(subscribe_hash_sig) = utils::sign_message_hash("SUBSCRIBE", &keypair)
else {
error!("Failed to sign hash for BOLT12 offer: {offer}");
continue;
};
match self
.status_stream
.track_offer(offer, &subscribe_hash_sig.to_hex())
{
Ok(_) => info!("Tracking bolt12 offer: {offer}"),
Err(e) => error!("Failed to track bolt12 offer: {e:?}"),
}
}
}
Err(e) => error!("Failed to list initial bolt12 offers: {e:?}"),
}
}
}

View File

@@ -17,7 +17,7 @@ use self::model::{ListenChangesRequest, Notification, SyncOutgoingChanges};
use crate::prelude::Swap;
use crate::recover::recoverer::Recoverer;
use crate::sync::model::data::{
ChainSyncData, PaymentDetailsSyncData, ReceiveSyncData, SendSyncData,
Bolt12OfferSyncData, ChainSyncData, PaymentDetailsSyncData, ReceiveSyncData, SendSyncData,
};
use crate::sync::model::{DecryptionInfo, Record, SetRecordRequest, SetRecordStatus};
use crate::utils;
@@ -243,6 +243,13 @@ impl SyncService {
*last_commit_time,
)
}
SyncData::Bolt12Offer(bolt12_offer_data) => {
self.persister.commit_incoming_bolt12_offer(
bolt12_offer_data.into(),
new_sync_state,
*last_commit_time,
)
}
}
}
@@ -286,6 +293,14 @@ impl SyncService {
.into();
SyncData::PaymentDetails(payment_details_data)
}
RecordType::Bolt12Offer => {
let bolt12_offer_data: Bolt12OfferSyncData = self
.persister
.fetch_bolt12_offer_by_id(data_id)?
.ok_or(anyhow!("Could not find Bolt12 Offer {data_id}"))?
.into();
SyncData::Bolt12Offer(bolt12_offer_data)
}
};
Ok(data)
}

View File

@@ -2,6 +2,7 @@ use anyhow::bail;
use serde::{Deserialize, Serialize};
use crate::{
model::Bolt12Offer,
persist::model::PaymentTxDetails,
prelude::{ChainSwap, Direction, LnUrlInfo, PaymentState, ReceiveSwap, SendSwap, Swap},
};
@@ -231,6 +232,7 @@ pub(crate) struct ReceiveSyncData {
#[serde(default)]
pub(crate) timeout_block_height: u32,
pub(crate) created_at: u32,
pub(crate) bolt12_offer: Option<String>,
pub(crate) payment_hash: Option<String>,
pub(crate) description: Option<String>,
pub(crate) destination_pubkey: Option<String>,
@@ -255,6 +257,7 @@ impl From<ReceiveSwap> for ReceiveSyncData {
fn from(value: ReceiveSwap) -> Self {
Self {
swap_id: value.id,
bolt12_offer: value.bolt12_offer,
payment_hash: value.payment_hash,
invoice: value.invoice,
preimage: value.preimage,
@@ -282,6 +285,7 @@ impl From<ReceiveSyncData> for ReceiveSwap {
pair_fees_json: val.pair_fees_json,
claim_private_key: val.claim_private_key,
invoice: val.invoice,
bolt12_offer: val.bolt12_offer,
payment_hash: val.payment_hash,
destination_pubkey: val.destination_pubkey,
description: val.description,
@@ -352,6 +356,66 @@ impl From<PaymentDetailsSyncData> for PaymentTxDetails {
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub(crate) struct Bolt12OfferSyncData {
pub(crate) id: String,
pub(crate) description: String,
pub(crate) private_key: String,
pub(crate) webhook_url: Option<String>,
pub(crate) created_at: u32,
}
impl Bolt12OfferSyncData {
pub(crate) fn merge(&mut self, other: &Self, updated_fields: &[String]) {
for field in updated_fields {
match field.as_str() {
"webhook_url" => self.webhook_url.clone_from(&other.webhook_url),
_ => continue,
}
}
}
pub(crate) fn updated_fields(
bolt12_offer: Option<Bolt12Offer>,
update: &Bolt12Offer,
) -> Option<Vec<String>> {
match bolt12_offer {
Some(bolt12_offer) => {
let mut updated_fields = vec![];
if update.webhook_url != bolt12_offer.webhook_url {
updated_fields.push("webhook_url".to_string());
}
Some(updated_fields)
}
None => None,
}
}
}
impl From<Bolt12Offer> for Bolt12OfferSyncData {
fn from(value: Bolt12Offer) -> Self {
Self {
id: value.id,
description: value.description,
private_key: value.private_key,
webhook_url: value.webhook_url,
created_at: value.created_at,
}
}
}
impl From<Bolt12OfferSyncData> for Bolt12Offer {
fn from(val: Bolt12OfferSyncData) -> Self {
Bolt12Offer {
id: val.id,
description: val.description,
private_key: val.private_key,
webhook_url: val.webhook_url,
created_at: val.created_at,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(tag = "data_type", content = "data")]
pub(crate) enum SyncData {
@@ -360,6 +424,7 @@ pub(crate) enum SyncData {
Receive(ReceiveSyncData),
LastDerivationIndex(u32),
PaymentDetails(PaymentDetailsSyncData),
Bolt12Offer(Bolt12OfferSyncData),
}
impl SyncData {
@@ -370,6 +435,7 @@ impl SyncData {
SyncData::Receive(receive_data) => &receive_data.swap_id,
SyncData::LastDerivationIndex(_) => LAST_DERIVATION_INDEX_DATA_ID,
SyncData::PaymentDetails(payment_details) => &payment_details.tx_id,
SyncData::Bolt12Offer(bolt12_offer_data) => &bolt12_offer_data.id,
}
}
@@ -380,7 +446,9 @@ impl SyncData {
/// Whether the data is a swap
pub(crate) fn is_swap(&self) -> bool {
match self {
SyncData::LastDerivationIndex(_) | SyncData::PaymentDetails(_) => false,
SyncData::Bolt12Offer(_)
| SyncData::LastDerivationIndex(_)
| SyncData::PaymentDetails(_) => false,
SyncData::Chain(_) | SyncData::Send(_) | SyncData::Receive(_) => true,
}
}
@@ -405,6 +473,9 @@ impl SyncData {
(SyncData::PaymentDetails(ref mut base), SyncData::PaymentDetails(other)) => {
base.merge(other, updated_fields)
}
(SyncData::Bolt12Offer(ref mut base), SyncData::Bolt12Offer(other)) => {
base.merge(other, updated_fields)
}
_ => return Err(anyhow::anyhow!("Cannot merge data from two separate types")),
};
Ok(())

View File

@@ -19,7 +19,7 @@ pub(crate) mod data;
const MESSAGE_PREFIX: &[u8; 13] = b"realtimesync:";
lazy_static! {
static ref CURRENT_SCHEMA_VERSION: Version = Version::parse("0.5.0").unwrap();
static ref CURRENT_SCHEMA_VERSION: Version = Version::parse("0.6.0").unwrap();
}
#[derive(Copy, Clone)]
@@ -29,6 +29,7 @@ pub(crate) enum RecordType {
Chain = 2,
LastDerivationIndex = 3,
PaymentDetails = 4,
Bolt12Offer = 5,
}
impl ToSql for RecordType {
@@ -46,6 +47,7 @@ impl FromSql for RecordType {
2 => Ok(Self::Chain),
3 => Ok(Self::LastDerivationIndex),
4 => Ok(Self::PaymentDetails),
5 => Ok(Self::Bolt12Offer),
_ => Err(FromSqlError::OutOfRange(i)),
},
_ => Err(FromSqlError::InvalidType),
@@ -261,6 +263,7 @@ impl Record {
SyncData::Receive(_) => "receive-swap",
SyncData::LastDerivationIndex(_) => "derivation-index",
SyncData::PaymentDetails(_) => "payment-details",
SyncData::Bolt12Offer(_) => "bolt12-offer",
}
.to_string();
Self::id(prefix, data.id())
@@ -273,6 +276,7 @@ impl Record {
RecordType::Receive => "receive-swap",
RecordType::LastDerivationIndex => "derivation-index",
RecordType::PaymentDetails => "payment-details",
RecordType::Bolt12Offer => "bolt12-offer",
}
.to_string();
Self::id(prefix, data_id)

View File

@@ -0,0 +1,11 @@
use crate::{model::Bolt12Offer, test_utils::generate_random_string, utils};
pub fn new_bolt12_offer(description: Option<String>, webhook_url: Option<String>) -> Bolt12Offer {
Bolt12Offer {
id: generate_random_string(32),
description: description.unwrap_or("default".to_string()),
private_key: "945affeef55f12227f1d4a3f80a17062a05b229ddc5a01591eb5ddf882df92e3".to_string(),
webhook_url,
created_at: utils::now(),
}
}

View File

@@ -2,6 +2,7 @@
use bip39::rand::{self, distributions::Alphanumeric, Rng};
pub(crate) mod bolt12_offer;
pub(crate) mod chain;
pub(crate) mod chain_swap;
pub mod persist;

View File

@@ -106,6 +106,7 @@ pub fn new_receive_swap(
create_response_json: r#"{"swap_tree":{"claim_leaf":{"output":"82012088a91476089e96a323d103b4d9546ab0b64505672197f58820d5272b21c51e7fe6a2e0d6b3ddafde514ff2b31ca70399a3a0960f19f3b1853dac","version":196},"refund_leaf":{"output":"20859b5e5e3b66c76e0920a21a41f4a64246caf2cf0084307c447ba94a2e3a483dad03842231b1","version":196}},"lockup_address":"lq1pq0ka6jmyx62herardll0ccu3zze4qvmh04vnzdw4c5338rp3yquggh47wr29jh6akr6mtw2zzrgn6nuv68setq76d2uk9fqs0l84z7t2jhw58m0crqu4","refund_public_key":"03859b5e5e3b66c76e0920a21a41f4a64246caf2cf0084307c447ba94a2e3a483d","timeout_block_height":3220100,"onchain_amount":971,"blinding_key":"ef121ccd2906a4cc80f8a9b33b18fa2ba4de7e9032b7b143bfc816494d46dc66"}"#.to_string(),
claim_private_key: "08e4555d4388552fe6a72a89953b3d333ddbb66b7ae2167f5f66327ec66cede1".to_string(),
invoice: "lnbc10u1pnez5ulsp5szkn8zq25p99m3kkhcyv5xfaszvya80gca2efduhp9v0g3qy9spqpp52m3vvah5xj8mzu6knwl4gtcymzg9w7lmm90yctwr39kae36sjpsqdpz2djkuepqw3hjqnpdgf2yxgrpv3j8yetnwvxqyp2xqcqz95rzjqt2jw2epc508le4zurtt8hd0meg5lu4nrjns8xdr5ztq7x0nkxzn6zzxeyqq28qqqqqqqqqqqqqqq9gq2y9qyysgq4kue7c5mrla8cxgzlpddvl62a3quzpkhlza84tkrxea3hmvq4zcnn2rcve7l9cu5xdxglflerp5rcyeyc88j33mht4fea60jj9e7cqspe058nk".to_string(),
bolt12_offer: None,
payment_hash: Some("56e2c676f4348fb173569bbf542f04d890577bfbd95e4c2dc3896ddcc7509060".to_string()),
destination_pubkey: Some("02d96eadea3d780104449aca5c93461ce67c1564e2e1d73225fa67dd3b997a6018".to_string()),
payer_amount_sat: 1000,

View File

@@ -9,13 +9,18 @@ use crate::swapper::{SubscriptionHandler, SwapperStatusStream};
pub(crate) struct MockStatusStream {
pub update_notifier: broadcast::Sender<boltz::SwapStatus>,
pub invoice_request_notifier: broadcast::Sender<boltz::InvoiceRequest>,
}
impl MockStatusStream {
pub(crate) fn new() -> Self {
let (update_notifier, _) = broadcast::channel::<boltz::SwapStatus>(30);
let (invoice_request_notifier, _) = broadcast::channel::<boltz::InvoiceRequest>(30);
Self { update_notifier }
Self {
update_notifier,
invoice_request_notifier,
}
}
pub(crate) async fn send_mock_update(self: Arc<Self>, update: boltz::SwapStatus) -> Result<()> {
@@ -39,7 +44,19 @@ impl SwapperStatusStream for MockStatusStream {
Ok(())
}
fn track_offer(&self, _offer: &str, _signature: &str) -> Result<()> {
Ok(())
}
fn send_invoice_created(&self, _id: &str, _invoice: &str) -> Result<()> {
Ok(())
}
fn subscribe_swap_updates(&self) -> broadcast::Receiver<boltz::SwapStatus> {
self.update_notifier.subscribe()
}
fn subscribe_invoice_requests(&self) -> broadcast::Receiver<boltz::InvoiceRequest> {
self.invoice_request_notifier.subscribe()
}
}

View File

@@ -1,16 +1,18 @@
use anyhow::Result;
use boltz_client::{
boltz::{
ChainFees, ChainMinerFees, ChainPair, ChainSwapDetails, CreateChainResponse,
CreateReverseResponse, CreateSubmarineResponse, Leaf, PairLimits, PairMinerFees,
ReverseFees, ReverseLimits, ReversePair, SubmarineClaimTxResponse, SubmarineFees,
SubmarinePair, SubmarinePairLimits, SwapTree,
ChainFees, ChainMinerFees, ChainPair, ChainSwapDetails, CreateBolt12OfferRequest,
CreateChainResponse, CreateReverseResponse, CreateSubmarineResponse,
GetBolt12ParamsResponse, GetNodesResponse, Leaf, MagicRoutingHint, Node, PairLimits,
PairMinerFees, ReverseFees, ReverseLimits, ReversePair, SubmarineClaimTxResponse,
SubmarineFees, SubmarinePair, SubmarinePairLimits, SwapTree, UpdateBolt12OfferRequest,
},
util::secrets::Preimage,
Amount, PublicKey,
};
use lwk_wollet::secp256k1;
use sdk_common::invoice::parse_invoice;
use std::sync::Mutex;
use std::{collections::HashMap, str::FromStr, sync::Mutex};
use crate::{
ensure_sdk,
@@ -126,7 +128,7 @@ impl Swapper for MockSwapper {
req: boltz_client::swaps::boltz::CreateSubmarineRequest,
) -> Result<CreateSubmarineResponse, PaymentError> {
let invoice = parse_invoice(&req.invoice)
.map_err(|err| PaymentError::invalid_invoice(&err.to_string()))?;
.map_err(|err| PaymentError::invalid_invoice(err.to_string()))?;
let Some(amount_msat) = invoice.amount_msat else {
return Err(PaymentError::invalid_invoice(
"Invoice does not contain an amount",
@@ -294,11 +296,11 @@ impl Swapper for MockSwapper {
async fn create_receive_swap(
&self,
_req: boltz_client::swaps::boltz::CreateReverseRequest,
req: boltz_client::swaps::boltz::CreateReverseRequest,
) -> Result<CreateReverseResponse, PaymentError> {
Ok(CreateReverseResponse {
id: generate_random_string(4),
invoice: "".to_string(),
invoice: req.invoice.map_or(Some("".to_string()), |_| None),
swap_tree: Self::mock_swap_tree(),
lockup_address: "".to_string(),
refund_public_key: Self::mock_public_key(),
@@ -351,6 +353,44 @@ impl Swapper for MockSwapper {
unimplemented!()
}
async fn create_bolt12_offer(&self, _req: CreateBolt12OfferRequest) -> Result<(), SdkError> {
Ok(())
}
async fn update_bolt12_offer(&self, _req: UpdateBolt12OfferRequest) -> Result<(), SdkError> {
Ok(())
}
async fn delete_bolt12_offer(&self, _offer: &str, _signature: &str) -> Result<(), SdkError> {
Ok(())
}
async fn get_bolt12_params(&self) -> Result<GetBolt12ParamsResponse, SdkError> {
Ok(GetBolt12ParamsResponse {
min_cltv: 180,
magic_routing_hint: MagicRoutingHint {
channel_id: "596385002596073472".to_string(),
},
})
}
async fn get_nodes(&self) -> Result<GetNodesResponse, SdkError> {
Ok(GetNodesResponse {
btc: HashMap::from([(
"CLN".to_string(),
Node {
public_key: secp256k1::PublicKey::from_str(
"02d96eadea3d780104449aca5c93461ce67c1564e2e1d73225fa67dd3b997a6018",
)
.unwrap(),
uris: vec![
"02d96eadea3d780104449aca5c93461ce67c1564e2e1d73225fa67dd3b997a6018@143.202.162.204:9736".to_string(),
"02d96eadea3d780104449aca5c93461ce67c1564e2e1d73225fa67dd3b997a6018@2803:6900:581::1:c175:f0ad:9736".to_string()],
},
)]),
})
}
async fn get_zero_amount_chain_swap_quote(&self, _swap_id: &str) -> Result<Amount, SdkError> {
let server_lockup_amount_sat = self.get_zero_amount_swap_server_lockup_sat().await;
Ok(Amount::from_sat(server_lockup_amount_sat))

View File

@@ -125,6 +125,7 @@ pub(crate) fn new_receive_sync_data() -> ReceiveSyncData {
claim_private_key: "".to_string(),
mrh_address: "".to_string(),
preimage: "".to_string(),
bolt12_offer: None,
payment_hash: None,
description: None,
destination_pubkey: None,

View File

@@ -0,0 +1,41 @@
use anyhow::{anyhow, ensure, Result};
use sdk_common::bitcoin::bech32::ToBase32;
use sdk_common::bitcoin::bech32::{self, FromBase32};
use sdk_common::lightning_with_bolt12::offers::invoice::Bolt12Invoice;
use sdk_common::lightning_with_bolt12::offers::invoice_request::InvoiceRequest;
use sdk_common::lightning_with_bolt12::offers::offer::Offer;
use sdk_common::lightning_with_bolt12::util::ser::Writeable;
pub fn encode_invoice(invoice: &Bolt12Invoice) -> Result<String> {
let mut writer = Vec::new();
invoice.write(&mut writer)?;
Ok(bech32::encode_without_checksum("lni", writer.to_base32())?)
}
pub fn encode_offer(offer: &Offer) -> Result<String> {
let mut writer = Vec::new();
offer.write(&mut writer)?;
Ok(bech32::encode_without_checksum("lno", writer.to_base32())?)
}
/// Parsing logic that decodes a string into a [Bolt12Invoice].
///
/// It matches the encoding logic on Boltz side.
pub(crate) fn decode_invoice(invoice: &str) -> Result<Bolt12Invoice> {
let (hrp, data) = bech32::decode_without_checksum(invoice)?;
ensure!(hrp.as_str() == "lni", "Invalid HRP");
let data = Vec::<u8>::from_base32(&data)?;
sdk_common::lightning_with_bolt12::offers::invoice::Bolt12Invoice::try_from(data)
.map_err(|e| anyhow!("Failed to parse BOLT12: {e:?}"))
}
pub(crate) fn decode_invoice_request(invoice_request: &str) -> Result<InvoiceRequest> {
InvoiceRequest::try_from(
hex::decode(invoice_request).map_err(|e| anyhow!("Cannot decode invoice request: {e}"))?,
)
.map_err(|e| anyhow!("Cannot parse invoice request: {e:?}"))
}

View File

@@ -1,14 +1,18 @@
pub(crate) mod bolt12;
use std::str::FromStr;
use std::time::Duration;
use crate::ensure_sdk;
use crate::error::{PaymentError, SdkResult};
use crate::prelude::LiquidNetwork;
use anyhow::{anyhow, ensure, Result};
use anyhow::{anyhow, Result};
use bip39::rand::{self, RngCore};
use boltz_client::boltz::SubmarinePair;
use boltz_client::util::secrets::Preimage;
use boltz_client::ToHex;
use boltz_client::{Keypair, ToHex};
use lazy_static::lazy_static;
use lwk_wollet::bitcoin::secp256k1::Message;
use lwk_wollet::elements::encode::deserialize;
use lwk_wollet::elements::hex::FromHex;
use lwk_wollet::elements::AssetId;
@@ -16,10 +20,9 @@ use lwk_wollet::elements::{
LockTime::{self, *},
Transaction,
};
use sdk_common::bitcoin::bech32;
use sdk_common::bitcoin::bech32::FromBase32;
use sdk_common::lightning_invoice::Bolt11Invoice;
use sdk_common::lightning_with_bolt12::offers::invoice::Bolt12Invoice;
use lwk_wollet::hashes::{sha256, Hash};
use lwk_wollet::secp256k1::schnorr::Signature;
use sdk_common::lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription};
use web_time::{SystemTime, UNIX_EPOCH};
lazy_static! {
@@ -51,6 +54,13 @@ pub(crate) fn generate_keypair() -> boltz_client::Keypair {
boltz_client::Keypair::from_secret_key(&secp, &secret_key)
}
pub(crate) fn generate_entropy() -> [u8; 32] {
let mut entropy_bytes = [0u8; 32];
let mut rng = rand::thread_rng();
rng.fill_bytes(&mut entropy_bytes);
entropy_bytes
}
pub(crate) fn decode_keypair(secret_key: &str) -> SdkResult<boltz_client::Keypair> {
let secp = boltz_client::Secp256k1::new();
let secret_key = lwk_wollet::secp256k1::SecretKey::from_str(secret_key)?;
@@ -71,24 +81,16 @@ pub(crate) fn deserialize_tx_hex(tx_hex: &str) -> Result<Transaction> {
)?)?)
}
/// Parsing logic that decodes a string into a [Bolt12Invoice].
///
/// It matches the encoding logic on Boltz side.
pub(crate) fn parse_bolt12_invoice(invoice: &str) -> Result<Bolt12Invoice> {
let (hrp, data) = bech32::decode_without_checksum(invoice)?;
ensure!(hrp.as_str() == "lni", "Invalid HRP");
let data = Vec::<u8>::from_base32(&data)?;
sdk_common::lightning_with_bolt12::offers::invoice::Bolt12Invoice::try_from(data)
.map_err(|e| anyhow!("Failed to parse BOLT12: {e:?}"))
pub(crate) fn sign_message_hash<S: AsRef<str>>(msg: S, keypair: &Keypair) -> Result<Signature> {
let msg_hash = sha256::Hash::hash(msg.as_ref().as_bytes());
Ok(keypair.sign_schnorr(Message::from_digest_slice(msg_hash.as_byte_array())?))
}
/// Parse and extract the destination pubkey from the invoice.
/// The payee pubkey for Bolt11 and signing pubkey for Bolt12.
pub(crate) fn get_invoice_destination_pubkey(invoice: &str, is_bolt12: bool) -> Result<String> {
if is_bolt12 {
parse_bolt12_invoice(invoice).map(|i| i.signing_pubkey().to_hex())
bolt12::decode_invoice(invoice).map(|i| i.signing_pubkey().to_hex())
} else {
invoice
.trim()
@@ -98,6 +100,23 @@ pub(crate) fn get_invoice_destination_pubkey(invoice: &str, is_bolt12: bool) ->
}
}
/// Parse and extract the description from the BOLT11/12 invoice
pub(crate) fn get_invoice_description(invoice: &str) -> Result<Option<String>, PaymentError> {
let invoice = invoice.trim();
match Bolt11Invoice::from_str(invoice) {
Ok(invoice) => match invoice.description() {
Bolt11InvoiceDescription::Direct(d) => Ok(Some(d.to_string())),
Bolt11InvoiceDescription::Hash(_) => Ok(None),
},
Err(_) => match bolt12::decode_invoice(invoice) {
Ok(invoice) => Ok(invoice.description().map(|d| d.to_string())),
Err(e) => Err(PaymentError::InvalidInvoice {
err: format!("Could not parse invoice: {e:?}"),
}),
},
}
}
/// Verifies a BOLT11/12 invoice against a preimage
pub(crate) fn verify_payment_hash(
preimage: &str,
@@ -108,7 +127,7 @@ pub(crate) fn verify_payment_hash(
let invoice_payment_hash = match Bolt11Invoice::from_str(invoice) {
Ok(invoice) => Ok(invoice.payment_hash().to_string()),
Err(_) => match parse_bolt12_invoice(invoice) {
Err(_) => match bolt12::decode_invoice(invoice) {
Ok(invoice) => Ok(invoice.payment_hash().to_string()),
Err(e) => Err(PaymentError::InvalidInvoice {
err: format!("Could not parse invoice: {e:?}"),

View File

@@ -0,0 +1,160 @@
use std::time::Duration;
use breez_sdk_liquid::model::{
PayAmount, PaymentDetails, PaymentMethod, PaymentState, PaymentType, PrepareReceiveRequest,
PrepareSendRequest, SdkEvent,
};
use serial_test::serial;
use tokio_with_wasm::alias as tokio;
use crate::regtest::{
utils::{self, mine_blocks},
ChainBackend, SdkNodeHandle, TIMEOUT,
};
#[cfg(feature = "browser-tests")]
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
#[sdk_macros::async_test_not_wasm]
#[serial]
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
async fn bolt12_electrum() {
let handle_alice = SdkNodeHandle::init_node(ChainBackend::Electrum)
.await
.unwrap();
let handle_bob = SdkNodeHandle::init_node(ChainBackend::Electrum)
.await
.unwrap();
bolt12(handle_alice, handle_bob).await;
}
#[sdk_macros::async_test_all]
#[serial]
async fn bolt12_esplora() {
let handle_alice = SdkNodeHandle::init_node(ChainBackend::Esplora)
.await
.unwrap();
let handle_bob = SdkNodeHandle::init_node(ChainBackend::Esplora)
.await
.unwrap();
bolt12(handle_alice, handle_bob).await;
}
async fn bolt12(mut handle_alice: SdkNodeHandle, mut handle_bob: SdkNodeHandle) {
handle_alice
.wait_for_event(|e| matches!(e, SdkEvent::Synced { .. }), TIMEOUT)
.await
.unwrap();
handle_bob
.wait_for_event(|e| matches!(e, SdkEvent::Synced { .. }), TIMEOUT)
.await
.unwrap();
// -------------------SETUP-------------------
// Setup Alice with some funds
let (_, receive_response) = handle_alice
.receive_payment(&PrepareReceiveRequest {
payment_method: PaymentMethod::LiquidAddress,
amount: None,
})
.await
.unwrap();
let address = receive_response.destination;
let amount_sat = 200_000;
utils::send_to_address_elementsd(&address, amount_sat)
.await
.unwrap();
handle_alice
.wait_for_event(
|e| matches!(e, SdkEvent::PaymentWaitingConfirmation { .. }),
TIMEOUT,
)
.await
.unwrap();
utils::mine_blocks(1).await.unwrap();
handle_alice
.wait_for_event(|e| matches!(e, SdkEvent::PaymentSucceeded { .. }), TIMEOUT)
.await
.unwrap();
// -------------------CREATE BOLT12 OFFER-------------------
let (_, receive_response) = handle_bob
.receive_payment(&PrepareReceiveRequest {
payment_method: PaymentMethod::Bolt12Offer,
amount: None,
})
.await
.unwrap();
let offer = receive_response.destination;
// -------------------SEND SWAP-------------------
// TODO: Pay an offer using the CLN node
// -------------------MRH-------------------
let receiver_amount_sat = 50_000;
let (_, _) = handle_alice
.send_payment(&PrepareSendRequest {
destination: offer,
amount: Some(PayAmount::Bitcoin {
receiver_amount_sat,
}),
})
.await
.unwrap();
mine_blocks(1).await.unwrap();
handle_bob
.wait_for_event(|e| matches!(e, SdkEvent::PaymentSucceeded { .. }), TIMEOUT)
.await
.unwrap();
// TODO: this shouldn't be needed, but without it, sometimes get_balance_sat isn't updated in time
// https://github.com/breez/breez-sdk-liquid/issues/828
tokio::time::sleep(Duration::from_secs(1)).await;
handle_alice.sdk.sync(false).await.unwrap();
assert_eq!(handle_bob.get_pending_receive_sat().await.unwrap(), 0);
assert_eq!(handle_bob.get_pending_send_sat().await.unwrap(), 0);
assert_eq!(
handle_bob.get_balance_sat().await.unwrap(),
receiver_amount_sat
);
let alice_payments = handle_alice.get_payments().await.unwrap();
assert_eq!(alice_payments.len(), 2);
let alice_payment = &alice_payments[0];
assert_eq!(alice_payment.amount_sat, receiver_amount_sat);
// The prepare response gives the fees for a swap, so instead we test the Liquid fee
assert_eq!(alice_payment.fees_sat, 26);
assert_eq!(alice_payment.payment_type, PaymentType::Send);
assert_eq!(alice_payment.status, PaymentState::Complete);
assert!(matches!(
alice_payment.details,
PaymentDetails::Liquid { .. }
));
let bob_payments = handle_bob.get_payments().await.unwrap();
assert_eq!(bob_payments.len(), 1);
let bob_payment = &bob_payments[0];
assert_eq!(bob_payment.amount_sat, receiver_amount_sat);
assert_eq!(bob_payment.fees_sat, 0);
assert_eq!(bob_payment.payment_type, PaymentType::Receive);
assert_eq!(bob_payment.status, PaymentState::Complete);
// TODO: figure out why occasionally this fails (details = Liquid)
// https://github.com/breez/breez-sdk-liquid/issues/829
/*assert!(matches!(
bob_payment.details,
PaymentDetails::Lightning { .. }
));*/
// On node.js, without disconnecting the sdk, the wasm-pack test process fails after the test succeeds
handle_alice.sdk.disconnect().await.unwrap();
handle_bob.sdk.disconnect().await.unwrap();
}

View File

@@ -2,6 +2,7 @@
mod bitcoin;
mod bolt11;
mod bolt12;
mod liquid;
mod utils;

View File

@@ -11,13 +11,13 @@ init:
clippy: clippy-default clippy-browser clippy-node
clippy-default:
$(CLANG_PREFIX) cargo clippy --all-targets --target=wasm32-unknown-unknown -- -D warnings
$(CLANG_PREFIX) cargo clippy --all-targets --target=wasm32-unknown-unknown -- -A deprecated -D warnings
clippy-browser:
$(CLANG_PREFIX) cargo clippy --all-targets --target=wasm32-unknown-unknown --features browser -- -D warnings
$(CLANG_PREFIX) cargo clippy --all-targets --target=wasm32-unknown-unknown --features browser -- -A deprecated -D warnings
clippy-node:
$(CLANG_PREFIX) cargo clippy --all-targets --target=wasm32-unknown-unknown --features node-js -- -D warnings
$(CLANG_PREFIX) cargo clippy --all-targets --target=wasm32-unknown-unknown --features node-js -- -A deprecated -D warnings
build: build-bundle build-deno build-node build-web

View File

@@ -202,6 +202,14 @@ impl BindingLiquidSdk {
Ok(self.sdk.receive_payment(&req.into()).await?.into())
}
#[wasm_bindgen(js_name = "createBolt12Invoice")]
pub async fn create_bolt12_invoice(
&self,
req: CreateBolt12InvoiceRequest,
) -> WasmResult<CreateBolt12InvoiceResponse> {
Ok(self.sdk.create_bolt12_invoice(&req.into()).await?.into())
}
#[wasm_bindgen(js_name = "fetchLightningLimits")]
pub async fn fetch_lightning_limits(&self) -> WasmResult<LightningPaymentLimitsResponse> {
Ok(self.sdk.fetch_lightning_limits().await?.into())

View File

@@ -356,6 +356,8 @@ pub struct ConnectWithSignerRequest {
#[sdk_macros::extern_wasm_bindgen(breez_sdk_liquid::prelude::PaymentMethod)]
pub enum PaymentMethod {
Lightning,
Bolt11Invoice,
Bolt12Offer,
BitcoinAddress,
LiquidAddress,
}
@@ -380,8 +382,8 @@ pub struct PrepareReceiveRequest {
#[sdk_macros::extern_wasm_bindgen(breez_sdk_liquid::prelude::PrepareReceiveResponse)]
pub struct PrepareReceiveResponse {
pub payment_method: PaymentMethod,
pub amount: Option<ReceiveAmount>,
pub fees_sat: u64,
pub amount: Option<ReceiveAmount>,
pub min_payer_amount_sat: Option<u64>,
pub max_payer_amount_sat: Option<u64>,
pub swapper_feerate: Option<f64>,
@@ -399,6 +401,17 @@ pub struct ReceivePaymentResponse {
pub destination: String,
}
#[sdk_macros::extern_wasm_bindgen(breez_sdk_liquid::prelude::CreateBolt12InvoiceRequest)]
pub struct CreateBolt12InvoiceRequest {
pub offer: String,
pub invoice_request: String,
}
#[sdk_macros::extern_wasm_bindgen(breez_sdk_liquid::prelude::CreateBolt12InvoiceResponse)]
pub struct CreateBolt12InvoiceResponse {
pub invoice: String,
}
#[sdk_macros::extern_wasm_bindgen(breez_sdk_liquid::prelude::Limits)]
pub struct Limits {
pub min_sat: u64,

View File

@@ -39,6 +39,8 @@ abstract class BindingLiquidSdk implements RustOpaqueInterface {
CheckMessageResponse checkMessage({required CheckMessageRequest req});
Future<CreateBolt12InvoiceResponse> createBolt12Invoice({required CreateBolt12InvoiceRequest req});
Future<void> disconnect();
void emptyWalletCache();

View File

@@ -53,7 +53,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
String get codegenVersion => '2.9.0';
@override
int get rustContentHash => 1264782025;
int get rustContentHash => 464449310;
static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig(
stem: 'breez_sdk_liquid',
@@ -82,6 +82,11 @@ abstract class RustLibApi extends BaseApi {
required CheckMessageRequest req,
});
Future<CreateBolt12InvoiceResponse> crateBindingsBindingLiquidSdkCreateBolt12Invoice({
required BindingLiquidSdk that,
required CreateBolt12InvoiceRequest req,
});
Future<void> crateBindingsBindingLiquidSdkDisconnect({required BindingLiquidSdk that});
void crateBindingsBindingLiquidSdkEmptyWalletCache({required BindingLiquidSdk that});
@@ -370,6 +375,35 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
TaskConstMeta get kCrateBindingsBindingLiquidSdkCheckMessageConstMeta =>
const TaskConstMeta(debugName: "BindingLiquidSdk_check_message", argNames: ["that", "req"]);
@override
Future<CreateBolt12InvoiceResponse> crateBindingsBindingLiquidSdkCreateBolt12Invoice({
required BindingLiquidSdk that,
required CreateBolt12InvoiceRequest req,
}) {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
var arg0 =
cst_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerBindingLiquidSdk(
that,
);
var arg1 = cst_encode_box_autoadd_create_bolt_12_invoice_request(req);
return wire.wire__crate__bindings__BindingLiquidSdk_create_bolt12_invoice(port_, arg0, arg1);
},
codec: DcoCodec(
decodeSuccessData: dco_decode_create_bolt_12_invoice_response,
decodeErrorData: dco_decode_payment_error,
),
constMeta: kCrateBindingsBindingLiquidSdkCreateBolt12InvoiceConstMeta,
argValues: [that, req],
apiImpl: this,
),
);
}
TaskConstMeta get kCrateBindingsBindingLiquidSdkCreateBolt12InvoiceConstMeta =>
const TaskConstMeta(debugName: "BindingLiquidSdk_create_bolt12_invoice", argNames: ["that", "req"]);
@override
Future<void> crateBindingsBindingLiquidSdkDisconnect({required BindingLiquidSdk that}) {
return handler.executeNormal(
@@ -1637,6 +1671,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return dco_decode_connect_request(raw);
}
@protected
CreateBolt12InvoiceRequest dco_decode_box_autoadd_create_bolt_12_invoice_request(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return dco_decode_create_bolt_12_invoice_request(raw);
}
@protected
double dco_decode_box_autoadd_f_64(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@@ -1962,6 +2002,25 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
);
}
@protected
CreateBolt12InvoiceRequest dco_decode_create_bolt_12_invoice_request(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
final arr = raw as List<dynamic>;
if (arr.length != 2) throw Exception('unexpected arr length: expect 2 but see ${arr.length}');
return CreateBolt12InvoiceRequest(
offer: dco_decode_String(arr[0]),
invoiceRequest: dco_decode_String(arr[1]),
);
}
@protected
CreateBolt12InvoiceResponse dco_decode_create_bolt_12_invoice_response(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
final arr = raw as List<dynamic>;
if (arr.length != 1) throw Exception('unexpected arr length: expect 1 but see ${arr.length}');
return CreateBolt12InvoiceResponse(invoice: dco_decode_String(arr[0]));
}
@protected
CurrencyInfo dco_decode_currency_info(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
@@ -2975,8 +3034,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
if (arr.length != 6) throw Exception('unexpected arr length: expect 6 but see ${arr.length}');
return PrepareReceiveResponse(
paymentMethod: dco_decode_payment_method(arr[0]),
amount: dco_decode_opt_box_autoadd_receive_amount(arr[1]),
feesSat: dco_decode_u_64(arr[2]),
feesSat: dco_decode_u_64(arr[1]),
amount: dco_decode_opt_box_autoadd_receive_amount(arr[2]),
minPayerAmountSat: dco_decode_opt_box_autoadd_u_64(arr[3]),
maxPayerAmountSat: dco_decode_opt_box_autoadd_u_64(arr[4]),
swapperFeerate: dco_decode_opt_box_autoadd_f_64(arr[5]),
@@ -3682,6 +3741,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return (sse_decode_connect_request(deserializer));
}
@protected
CreateBolt12InvoiceRequest sse_decode_box_autoadd_create_bolt_12_invoice_request(
SseDeserializer deserializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
return (sse_decode_create_bolt_12_invoice_request(deserializer));
}
@protected
double sse_decode_box_autoadd_f_64(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -4017,6 +4084,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
);
}
@protected
CreateBolt12InvoiceRequest sse_decode_create_bolt_12_invoice_request(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
var var_offer = sse_decode_String(deserializer);
var var_invoiceRequest = sse_decode_String(deserializer);
return CreateBolt12InvoiceRequest(offer: var_offer, invoiceRequest: var_invoiceRequest);
}
@protected
CreateBolt12InvoiceResponse sse_decode_create_bolt_12_invoice_response(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
var var_invoice = sse_decode_String(deserializer);
return CreateBolt12InvoiceResponse(invoice: var_invoice);
}
@protected
CurrencyInfo sse_decode_currency_info(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -5356,15 +5438,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
PrepareReceiveResponse sse_decode_prepare_receive_response(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
var var_paymentMethod = sse_decode_payment_method(deserializer);
var var_amount = sse_decode_opt_box_autoadd_receive_amount(deserializer);
var var_feesSat = sse_decode_u_64(deserializer);
var var_amount = sse_decode_opt_box_autoadd_receive_amount(deserializer);
var var_minPayerAmountSat = sse_decode_opt_box_autoadd_u_64(deserializer);
var var_maxPayerAmountSat = sse_decode_opt_box_autoadd_u_64(deserializer);
var var_swapperFeerate = sse_decode_opt_box_autoadd_f_64(deserializer);
return PrepareReceiveResponse(
paymentMethod: var_paymentMethod,
amount: var_amount,
feesSat: var_feesSat,
amount: var_amount,
minPayerAmountSat: var_minPayerAmountSat,
maxPayerAmountSat: var_maxPayerAmountSat,
swapperFeerate: var_swapperFeerate,
@@ -6171,6 +6253,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_connect_request(self, serializer);
}
@protected
void sse_encode_box_autoadd_create_bolt_12_invoice_request(
CreateBolt12InvoiceRequest self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_create_bolt_12_invoice_request(self, serializer);
}
@protected
void sse_encode_box_autoadd_f_64(double self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -6502,6 +6593,22 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_opt_list_prim_u_8_strict(self.seed, serializer);
}
@protected
void sse_encode_create_bolt_12_invoice_request(CreateBolt12InvoiceRequest self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_String(self.offer, serializer);
sse_encode_String(self.invoiceRequest, serializer);
}
@protected
void sse_encode_create_bolt_12_invoice_response(
CreateBolt12InvoiceResponse self,
SseSerializer serializer,
) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_String(self.invoice, serializer);
}
@protected
void sse_encode_currency_info(CurrencyInfo self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
@@ -7589,8 +7696,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
void sse_encode_prepare_receive_response(PrepareReceiveResponse self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_payment_method(self.paymentMethod, serializer);
sse_encode_opt_box_autoadd_receive_amount(self.amount, serializer);
sse_encode_u_64(self.feesSat, serializer);
sse_encode_opt_box_autoadd_receive_amount(self.amount, serializer);
sse_encode_opt_box_autoadd_u_64(self.minPayerAmountSat, serializer);
sse_encode_opt_box_autoadd_u_64(self.maxPayerAmountSat, serializer);
sse_encode_opt_box_autoadd_f_64(self.swapperFeerate, serializer);
@@ -7948,6 +8055,9 @@ class BindingLiquidSdkImpl extends RustOpaque implements BindingLiquidSdk {
CheckMessageResponse checkMessage({required CheckMessageRequest req}) =>
RustLib.instance.api.crateBindingsBindingLiquidSdkCheckMessage(that: this, req: req);
Future<CreateBolt12InvoiceResponse> createBolt12Invoice({required CreateBolt12InvoiceRequest req}) =>
RustLib.instance.api.crateBindingsBindingLiquidSdkCreateBolt12Invoice(that: this, req: req);
Future<void> disconnect() => RustLib.instance.api.crateBindingsBindingLiquidSdkDisconnect(that: this);
void emptyWalletCache() => RustLib.instance.api.crateBindingsBindingLiquidSdkEmptyWalletCache(that: this);

View File

@@ -134,6 +134,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
ConnectRequest dco_decode_box_autoadd_connect_request(dynamic raw);
@protected
CreateBolt12InvoiceRequest dco_decode_box_autoadd_create_bolt_12_invoice_request(dynamic raw);
@protected
double dco_decode_box_autoadd_f_64(dynamic raw);
@@ -278,6 +281,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
ConnectRequest dco_decode_connect_request(dynamic raw);
@protected
CreateBolt12InvoiceRequest dco_decode_create_bolt_12_invoice_request(dynamic raw);
@protected
CreateBolt12InvoiceResponse dco_decode_create_bolt_12_invoice_response(dynamic raw);
@protected
CurrencyInfo dco_decode_currency_info(dynamic raw);
@@ -780,6 +789,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
ConnectRequest sse_decode_box_autoadd_connect_request(SseDeserializer deserializer);
@protected
CreateBolt12InvoiceRequest sse_decode_box_autoadd_create_bolt_12_invoice_request(
SseDeserializer deserializer,
);
@protected
double sse_decode_box_autoadd_f_64(SseDeserializer deserializer);
@@ -926,6 +940,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
ConnectRequest sse_decode_connect_request(SseDeserializer deserializer);
@protected
CreateBolt12InvoiceRequest sse_decode_create_bolt_12_invoice_request(SseDeserializer deserializer);
@protected
CreateBolt12InvoiceResponse sse_decode_create_bolt_12_invoice_response(SseDeserializer deserializer);
@protected
CurrencyInfo sse_decode_currency_info(SseDeserializer deserializer);
@@ -1466,6 +1486,16 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
return ptr;
}
@protected
ffi.Pointer<wire_cst_create_bolt_12_invoice_request> cst_encode_box_autoadd_create_bolt_12_invoice_request(
CreateBolt12InvoiceRequest raw,
) {
// Codec=Cst (C-struct based), see doc to use other codecs
final ptr = wire.cst_new_box_autoadd_create_bolt_12_invoice_request();
cst_api_fill_to_wire_create_bolt_12_invoice_request(raw, ptr.ref);
return ptr;
}
@protected
ffi.Pointer<ffi.Double> cst_encode_box_autoadd_f_64(double raw) {
// Codec=Cst (C-struct based), see doc to use other codecs
@@ -2390,6 +2420,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
cst_api_fill_to_wire_connect_request(apiObj, wireObj.ref);
}
@protected
void cst_api_fill_to_wire_box_autoadd_create_bolt_12_invoice_request(
CreateBolt12InvoiceRequest apiObj,
ffi.Pointer<wire_cst_create_bolt_12_invoice_request> wireObj,
) {
cst_api_fill_to_wire_create_bolt_12_invoice_request(apiObj, wireObj.ref);
}
@protected
void cst_api_fill_to_wire_box_autoadd_fetch_payment_proposed_fees_request(
FetchPaymentProposedFeesRequest apiObj,
@@ -2737,6 +2775,23 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
wireObj.seed = cst_encode_opt_list_prim_u_8_strict(apiObj.seed);
}
@protected
void cst_api_fill_to_wire_create_bolt_12_invoice_request(
CreateBolt12InvoiceRequest apiObj,
wire_cst_create_bolt_12_invoice_request wireObj,
) {
wireObj.offer = cst_encode_String(apiObj.offer);
wireObj.invoice_request = cst_encode_String(apiObj.invoiceRequest);
}
@protected
void cst_api_fill_to_wire_create_bolt_12_invoice_response(
CreateBolt12InvoiceResponse apiObj,
wire_cst_create_bolt_12_invoice_response wireObj,
) {
wireObj.invoice = cst_encode_String(apiObj.invoice);
}
@protected
void cst_api_fill_to_wire_currency_info(CurrencyInfo apiObj, wire_cst_currency_info wireObj) {
wireObj.name = cst_encode_String(apiObj.name);
@@ -3625,8 +3680,8 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
wire_cst_prepare_receive_response wireObj,
) {
wireObj.payment_method = cst_encode_payment_method(apiObj.paymentMethod);
wireObj.amount = cst_encode_opt_box_autoadd_receive_amount(apiObj.amount);
wireObj.fees_sat = cst_encode_u_64(apiObj.feesSat);
wireObj.amount = cst_encode_opt_box_autoadd_receive_amount(apiObj.amount);
wireObj.min_payer_amount_sat = cst_encode_opt_box_autoadd_u_64(apiObj.minPayerAmountSat);
wireObj.max_payer_amount_sat = cst_encode_opt_box_autoadd_u_64(apiObj.maxPayerAmountSat);
wireObj.swapper_feerate = cst_encode_opt_box_autoadd_f_64(apiObj.swapperFeerate);
@@ -4167,6 +4222,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_box_autoadd_connect_request(ConnectRequest self, SseSerializer serializer);
@protected
void sse_encode_box_autoadd_create_bolt_12_invoice_request(
CreateBolt12InvoiceRequest self,
SseSerializer serializer,
);
@protected
void sse_encode_box_autoadd_f_64(double self, SseSerializer serializer);
@@ -4332,6 +4393,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_connect_request(ConnectRequest self, SseSerializer serializer);
@protected
void sse_encode_create_bolt_12_invoice_request(CreateBolt12InvoiceRequest self, SseSerializer serializer);
@protected
void sse_encode_create_bolt_12_invoice_response(CreateBolt12InvoiceResponse self, SseSerializer serializer);
@protected
void sse_encode_currency_info(CurrencyInfo self, SseSerializer serializer);
@@ -4840,6 +4907,23 @@ class RustLibWire implements BaseWire {
_wire__crate__bindings__BindingLiquidSdk_check_messagePtr
.asFunction<WireSyncRust2DartDco Function(int, ffi.Pointer<wire_cst_check_message_request>)>();
void wire__crate__bindings__BindingLiquidSdk_create_bolt12_invoice(
int port_,
int that,
ffi.Pointer<wire_cst_create_bolt_12_invoice_request> req,
) {
return _wire__crate__bindings__BindingLiquidSdk_create_bolt12_invoice(port_, that, req);
}
late final _wire__crate__bindings__BindingLiquidSdk_create_bolt12_invoicePtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(ffi.Int64, ffi.UintPtr, ffi.Pointer<wire_cst_create_bolt_12_invoice_request>)
>
>('frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_create_bolt12_invoice');
late final _wire__crate__bindings__BindingLiquidSdk_create_bolt12_invoice =
_wire__crate__bindings__BindingLiquidSdk_create_bolt12_invoicePtr
.asFunction<void Function(int, int, ffi.Pointer<wire_cst_create_bolt_12_invoice_request>)>();
void wire__crate__bindings__BindingLiquidSdk_disconnect(int port_, int that) {
return _wire__crate__bindings__BindingLiquidSdk_disconnect(port_, that);
}
@@ -5552,6 +5636,18 @@ class RustLibWire implements BaseWire {
late final _cst_new_box_autoadd_connect_request =
_cst_new_box_autoadd_connect_requestPtr.asFunction<ffi.Pointer<wire_cst_connect_request> Function()>();
ffi.Pointer<wire_cst_create_bolt_12_invoice_request> cst_new_box_autoadd_create_bolt_12_invoice_request() {
return _cst_new_box_autoadd_create_bolt_12_invoice_request();
}
late final _cst_new_box_autoadd_create_bolt_12_invoice_requestPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<wire_cst_create_bolt_12_invoice_request> Function()>>(
'frbgen_breez_liquid_cst_new_box_autoadd_create_bolt_12_invoice_request',
);
late final _cst_new_box_autoadd_create_bolt_12_invoice_request =
_cst_new_box_autoadd_create_bolt_12_invoice_requestPtr
.asFunction<ffi.Pointer<wire_cst_create_bolt_12_invoice_request> Function()>();
ffi.Pointer<ffi.Double> cst_new_box_autoadd_f_64(double value) {
return _cst_new_box_autoadd_f_64(value);
}
@@ -6292,6 +6388,12 @@ final class wire_cst_check_message_request extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> signature;
}
final class wire_cst_create_bolt_12_invoice_request extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> offer;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> invoice_request;
}
final class wire_cst_fetch_payment_proposed_fees_request extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> swap_id;
}
@@ -6795,11 +6897,11 @@ final class wire_cst_prepare_receive_response extends ffi.Struct {
@ffi.Int32()
external int payment_method;
external ffi.Pointer<wire_cst_receive_amount> amount;
@ffi.Uint64()
external int fees_sat;
external ffi.Pointer<wire_cst_receive_amount> amount;
external ffi.Pointer<ffi.Uint64> min_payer_amount_sat;
external ffi.Pointer<ffi.Uint64> max_payer_amount_sat;
@@ -7375,6 +7477,10 @@ final class wire_cst_check_message_response extends ffi.Struct {
external bool is_valid;
}
final class wire_cst_create_bolt_12_invoice_response extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> invoice;
}
final class wire_cst_wallet_info extends ffi.Struct {
@ffi.Uint64()
external int balance_sat;

View File

@@ -391,6 +391,44 @@ class ConnectRequest {
seed == other.seed;
}
/// An argument when calling [crate::sdk::LiquidSdk::create_bolt12_invoice].
class CreateBolt12InvoiceRequest {
/// The BOLT12 offer
final String offer;
/// The invoice request created from the offer
final String invoiceRequest;
const CreateBolt12InvoiceRequest({required this.offer, required this.invoiceRequest});
@override
int get hashCode => offer.hashCode ^ invoiceRequest.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CreateBolt12InvoiceRequest &&
runtimeType == other.runtimeType &&
offer == other.offer &&
invoiceRequest == other.invoiceRequest;
}
/// Returned when calling [crate::sdk::LiquidSdk::create_bolt12_invoice].
class CreateBolt12InvoiceResponse {
/// The BOLT12 invoice
final String invoice;
const CreateBolt12InvoiceResponse({required this.invoice});
@override
int get hashCode => invoice.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CreateBolt12InvoiceResponse && runtimeType == other.runtimeType && invoice == other.invoice;
}
/// An argument when calling [crate::sdk::LiquidSdk::fetch_payment_proposed_fees].
class FetchPaymentProposedFeesRequest {
final String swapId;
@@ -964,7 +1002,7 @@ sealed class PaymentDetails with _$PaymentDetails {
}
/// The send/receive methods supported by the SDK
enum PaymentMethod { lightning, bitcoinAddress, liquidAddress }
enum PaymentMethod { lightning, bolt11Invoice, bolt12Offer, bitcoinAddress, liquidAddress }
/// The payment state of an individual payment.
enum PaymentState {
@@ -1248,7 +1286,6 @@ class PrepareReceiveRequest {
/// Returned when calling [crate::sdk::LiquidSdk::prepare_receive_payment].
class PrepareReceiveResponse {
final PaymentMethod paymentMethod;
final ReceiveAmount? amount;
/// Generally represents the total fees that would be paid to send or receive this payment.
///
@@ -1260,6 +1297,9 @@ class PrepareReceiveResponse {
/// In all other types of swaps, the swapper service fee is included in `fees_sat`.
final BigInt feesSat;
/// The amount to be paid in either Bitcoin or another asset
final ReceiveAmount? amount;
/// The minimum amount the payer can send for this swap to succeed.
///
/// When the method is [PaymentMethod::LiquidAddress], this is empty.
@@ -1277,8 +1317,8 @@ class PrepareReceiveResponse {
const PrepareReceiveResponse({
required this.paymentMethod,
this.amount,
required this.feesSat,
this.amount,
this.minPayerAmountSat,
this.maxPayerAmountSat,
this.swapperFeerate,
@@ -1287,8 +1327,8 @@ class PrepareReceiveResponse {
@override
int get hashCode =>
paymentMethod.hashCode ^
amount.hashCode ^
feesSat.hashCode ^
amount.hashCode ^
minPayerAmountSat.hashCode ^
maxPayerAmountSat.hashCode ^
swapperFeerate.hashCode;
@@ -1299,8 +1339,8 @@ class PrepareReceiveResponse {
other is PrepareReceiveResponse &&
runtimeType == other.runtimeType &&
paymentMethod == other.paymentMethod &&
amount == other.amount &&
feesSat == other.feesSat &&
amount == other.amount &&
minPayerAmountSat == other.minPayerAmountSat &&
maxPayerAmountSat == other.maxPayerAmountSat &&
swapperFeerate == other.swapperFeerate;

View File

@@ -135,6 +135,27 @@ class FlutterBreezLiquidBindings {
_frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_check_messagePtr
.asFunction<WireSyncRust2DartDco Function(int, ffi.Pointer<wire_cst_check_message_request>)>();
void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_create_bolt12_invoice(
int port_,
int that,
ffi.Pointer<wire_cst_create_bolt_12_invoice_request> req,
) {
return _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_create_bolt12_invoice(
port_,
that,
req,
);
}
late final _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_create_bolt12_invoicePtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.Int64, ffi.UintPtr, ffi.Pointer<wire_cst_create_bolt_12_invoice_request>)>>(
'frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_create_bolt12_invoice');
late final _frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_create_bolt12_invoice =
_frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_create_bolt12_invoicePtr
.asFunction<void Function(int, int, ffi.Pointer<wire_cst_create_bolt_12_invoice_request>)>();
void frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_disconnect(
int port_,
int that,
@@ -1009,6 +1030,18 @@ class FlutterBreezLiquidBindings {
_frbgen_breez_liquid_cst_new_box_autoadd_connect_requestPtr
.asFunction<ffi.Pointer<wire_cst_connect_request> Function()>();
ffi.Pointer<wire_cst_create_bolt_12_invoice_request>
frbgen_breez_liquid_cst_new_box_autoadd_create_bolt_12_invoice_request() {
return _frbgen_breez_liquid_cst_new_box_autoadd_create_bolt_12_invoice_request();
}
late final _frbgen_breez_liquid_cst_new_box_autoadd_create_bolt_12_invoice_requestPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<wire_cst_create_bolt_12_invoice_request> Function()>>(
'frbgen_breez_liquid_cst_new_box_autoadd_create_bolt_12_invoice_request');
late final _frbgen_breez_liquid_cst_new_box_autoadd_create_bolt_12_invoice_request =
_frbgen_breez_liquid_cst_new_box_autoadd_create_bolt_12_invoice_requestPtr
.asFunction<ffi.Pointer<wire_cst_create_bolt_12_invoice_request> Function()>();
ffi.Pointer<ffi.Double> frbgen_breez_liquid_cst_new_box_autoadd_f_64(
double value,
) {
@@ -1882,6 +1915,26 @@ class FlutterBreezLiquidBindings {
_uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_check_messagePtr
.asFunction<RustBuffer Function(ffi.Pointer<ffi.Void>, RustBuffer, ffi.Pointer<RustCallStatus>)>();
RustBuffer uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_create_bolt12_invoice(
ffi.Pointer<ffi.Void> ptr,
RustBuffer req,
ffi.Pointer<RustCallStatus> out_status,
) {
return _uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_create_bolt12_invoice(
ptr,
req,
out_status,
);
}
late final _uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_create_bolt12_invoicePtr = _lookup<
ffi.NativeFunction<
RustBuffer Function(ffi.Pointer<ffi.Void>, RustBuffer, ffi.Pointer<RustCallStatus>)>>(
'uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_create_bolt12_invoice');
late final _uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_create_bolt12_invoice =
_uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_create_bolt12_invoicePtr
.asFunction<RustBuffer Function(ffi.Pointer<ffi.Void>, RustBuffer, ffi.Pointer<RustCallStatus>)>();
void uniffi_breez_sdk_liquid_bindings_fn_method_bindingliquidsdk_disconnect(
ffi.Pointer<ffi.Void> ptr,
ffi.Pointer<RustCallStatus> out_status,
@@ -3646,6 +3699,17 @@ class FlutterBreezLiquidBindings {
_uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_check_messagePtr
.asFunction<int Function()>();
int uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_create_bolt12_invoice() {
return _uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_create_bolt12_invoice();
}
late final _uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_create_bolt12_invoicePtr =
_lookup<ffi.NativeFunction<ffi.Uint16 Function()>>(
'uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_create_bolt12_invoice');
late final _uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_create_bolt12_invoice =
_uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_create_bolt12_invoicePtr
.asFunction<int Function()>();
int uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_disconnect() {
return _uniffi_breez_sdk_liquid_bindings_checksum_method_bindingliquidsdk_disconnect();
}
@@ -4185,6 +4249,12 @@ final class wire_cst_check_message_request extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> signature;
}
final class wire_cst_create_bolt_12_invoice_request extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> offer;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> invoice_request;
}
final class wire_cst_fetch_payment_proposed_fees_request extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> swap_id;
}
@@ -4688,11 +4758,11 @@ final class wire_cst_prepare_receive_response extends ffi.Struct {
@ffi.Int32()
external int payment_method;
external ffi.Pointer<wire_cst_receive_amount> amount;
@ffi.Uint64()
external int fees_sat;
external ffi.Pointer<wire_cst_receive_amount> amount;
external ffi.Pointer<ffi.Uint64> min_payer_amount_sat;
external ffi.Pointer<ffi.Uint64> max_payer_amount_sat;
@@ -5268,6 +5338,10 @@ final class wire_cst_check_message_response extends ffi.Struct {
external bool is_valid;
}
final class wire_cst_create_bolt_12_invoice_response extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> invoice;
}
final class wire_cst_wallet_info extends ffi.Struct {
@ffi.Uint64()
external int balance_sat;

View File

@@ -594,6 +594,69 @@ fun asConnectWithSignerRequestList(arr: ReadableArray): List<ConnectWithSignerRe
return list
}
fun asCreateBolt12InvoiceRequest(createBolt12InvoiceRequest: ReadableMap): CreateBolt12InvoiceRequest? {
if (!validateMandatoryFields(
createBolt12InvoiceRequest,
arrayOf(
"offer",
"invoiceRequest",
),
)
) {
return null
}
val offer = createBolt12InvoiceRequest.getString("offer")!!
val invoiceRequest = createBolt12InvoiceRequest.getString("invoiceRequest")!!
return CreateBolt12InvoiceRequest(offer, invoiceRequest)
}
fun readableMapOf(createBolt12InvoiceRequest: CreateBolt12InvoiceRequest): ReadableMap =
readableMapOf(
"offer" to createBolt12InvoiceRequest.offer,
"invoiceRequest" to createBolt12InvoiceRequest.invoiceRequest,
)
fun asCreateBolt12InvoiceRequestList(arr: ReadableArray): List<CreateBolt12InvoiceRequest> {
val list = ArrayList<CreateBolt12InvoiceRequest>()
for (value in arr.toList()) {
when (value) {
is ReadableMap -> list.add(asCreateBolt12InvoiceRequest(value)!!)
else -> throw SdkException.Generic(errUnexpectedType(value))
}
}
return list
}
fun asCreateBolt12InvoiceResponse(createBolt12InvoiceResponse: ReadableMap): CreateBolt12InvoiceResponse? {
if (!validateMandatoryFields(
createBolt12InvoiceResponse,
arrayOf(
"invoice",
),
)
) {
return null
}
val invoice = createBolt12InvoiceResponse.getString("invoice")!!
return CreateBolt12InvoiceResponse(invoice)
}
fun readableMapOf(createBolt12InvoiceResponse: CreateBolt12InvoiceResponse): ReadableMap =
readableMapOf(
"invoice" to createBolt12InvoiceResponse.invoice,
)
fun asCreateBolt12InvoiceResponseList(arr: ReadableArray): List<CreateBolt12InvoiceResponse> {
val list = ArrayList<CreateBolt12InvoiceResponse>()
for (value in arr.toList()) {
when (value) {
is ReadableMap -> list.add(asCreateBolt12InvoiceResponse(value)!!)
else -> throw SdkException.Generic(errUnexpectedType(value))
}
}
return list
}
fun asCurrencyInfo(currencyInfo: ReadableMap): CurrencyInfo? {
if (!validateMandatoryFields(
currencyInfo,

View File

@@ -292,6 +292,24 @@ class BreezSDKLiquidModule(
}
}
@ReactMethod
fun createBolt12Invoice(
req: ReadableMap,
promise: Promise,
) {
executor.execute {
try {
val createBolt12InvoiceRequest =
asCreateBolt12InvoiceRequest(req)
?: run { throw SdkException.Generic(errMissingMandatoryField("req", "CreateBolt12InvoiceRequest")) }
val res = getBindingLiquidSdk().createBolt12Invoice(createBolt12InvoiceRequest)
promise.resolve(readableMapOf(res))
} catch (e: Exception) {
promise.reject(e.javaClass.simpleName.replace("Exception", "Error"), e.message, e)
}
}
}
@ReactMethod
fun fetchLightningLimits(promise: Promise) {
executor.execute {

View File

@@ -713,6 +713,72 @@ enum BreezSDKLiquidMapper {
return connectWithSignerRequestList.map { v -> [String: Any?] in return dictionaryOf(connectWithSignerRequest: v) }
}
static func asCreateBolt12InvoiceRequest(createBolt12InvoiceRequest: [String: Any?]) throws -> CreateBolt12InvoiceRequest {
guard let offer = createBolt12InvoiceRequest["offer"] as? String else {
throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "offer", typeName: "CreateBolt12InvoiceRequest"))
}
guard let invoiceRequest = createBolt12InvoiceRequest["invoiceRequest"] as? String else {
throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "invoiceRequest", typeName: "CreateBolt12InvoiceRequest"))
}
return CreateBolt12InvoiceRequest(offer: offer, invoiceRequest: invoiceRequest)
}
static func dictionaryOf(createBolt12InvoiceRequest: CreateBolt12InvoiceRequest) -> [String: Any?] {
return [
"offer": createBolt12InvoiceRequest.offer,
"invoiceRequest": createBolt12InvoiceRequest.invoiceRequest,
]
}
static func asCreateBolt12InvoiceRequestList(arr: [Any]) throws -> [CreateBolt12InvoiceRequest] {
var list = [CreateBolt12InvoiceRequest]()
for value in arr {
if let val = value as? [String: Any?] {
var createBolt12InvoiceRequest = try asCreateBolt12InvoiceRequest(createBolt12InvoiceRequest: val)
list.append(createBolt12InvoiceRequest)
} else {
throw SdkError.Generic(message: errUnexpectedType(typeName: "CreateBolt12InvoiceRequest"))
}
}
return list
}
static func arrayOf(createBolt12InvoiceRequestList: [CreateBolt12InvoiceRequest]) -> [Any] {
return createBolt12InvoiceRequestList.map { v -> [String: Any?] in return dictionaryOf(createBolt12InvoiceRequest: v) }
}
static func asCreateBolt12InvoiceResponse(createBolt12InvoiceResponse: [String: Any?]) throws -> CreateBolt12InvoiceResponse {
guard let invoice = createBolt12InvoiceResponse["invoice"] as? String else {
throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "invoice", typeName: "CreateBolt12InvoiceResponse"))
}
return CreateBolt12InvoiceResponse(invoice: invoice)
}
static func dictionaryOf(createBolt12InvoiceResponse: CreateBolt12InvoiceResponse) -> [String: Any?] {
return [
"invoice": createBolt12InvoiceResponse.invoice,
]
}
static func asCreateBolt12InvoiceResponseList(arr: [Any]) throws -> [CreateBolt12InvoiceResponse] {
var list = [CreateBolt12InvoiceResponse]()
for value in arr {
if let val = value as? [String: Any?] {
var createBolt12InvoiceResponse = try asCreateBolt12InvoiceResponse(createBolt12InvoiceResponse: val)
list.append(createBolt12InvoiceResponse)
} else {
throw SdkError.Generic(message: errUnexpectedType(typeName: "CreateBolt12InvoiceResponse"))
}
}
return list
}
static func arrayOf(createBolt12InvoiceResponseList: [CreateBolt12InvoiceResponse]) -> [Any] {
return createBolt12InvoiceResponseList.map { v -> [String: Any?] in return dictionaryOf(createBolt12InvoiceResponse: v) }
}
static func asCurrencyInfo(currencyInfo: [String: Any?]) throws -> CurrencyInfo {
guard let name = currencyInfo["name"] as? String else {
throw SdkError.Generic(message: errMissingMandatoryField(fieldName: "name", typeName: "CurrencyInfo"))
@@ -4450,6 +4516,12 @@ enum BreezSDKLiquidMapper {
case "lightning":
return PaymentMethod.lightning
case "bolt11Invoice":
return PaymentMethod.bolt11Invoice
case "bolt12Offer":
return PaymentMethod.bolt12Offer
case "bitcoinAddress":
return PaymentMethod.bitcoinAddress
@@ -4465,6 +4537,12 @@ enum BreezSDKLiquidMapper {
case .lightning:
return "lightning"
case .bolt11Invoice:
return "bolt11Invoice"
case .bolt12Offer:
return "bolt12Offer"
case .bitcoinAddress:
return "bitcoinAddress"

View File

@@ -85,6 +85,12 @@ RCT_EXTERN_METHOD(
reject: (RCTPromiseRejectBlock)reject
)
RCT_EXTERN_METHOD(
createBolt12Invoice: (NSDictionary*)req
resolve: (RCTPromiseResolveBlock)resolve
reject: (RCTPromiseRejectBlock)reject
)
RCT_EXTERN_METHOD(
fetchLightningLimits: (RCTPromiseResolveBlock)resolve
reject: (RCTPromiseRejectBlock)reject

View File

@@ -231,6 +231,17 @@ class RNBreezSDKLiquid: RCTEventEmitter {
}
}
@objc(createBolt12Invoice:resolve:reject:)
func createBolt12Invoice(_ req: [String: Any], resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
do {
let createBolt12InvoiceRequest = try BreezSDKLiquidMapper.asCreateBolt12InvoiceRequest(createBolt12InvoiceRequest: req)
var res = try getBindingLiquidSdk().createBolt12Invoice(req: createBolt12InvoiceRequest)
resolve(BreezSDKLiquidMapper.dictionaryOf(createBolt12InvoiceResponse: res))
} catch let err {
rejectErr(err: err, reject: reject)
}
}
@objc(fetchLightningLimits:reject:)
func fetchLightningLimits(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
do {

View File

@@ -117,6 +117,15 @@ export interface ConnectWithSignerRequest {
config: Config
}
export interface CreateBolt12InvoiceRequest {
offer: string
invoiceRequest: string
}
export interface CreateBolt12InvoiceResponse {
invoice: string
}
export interface CurrencyInfo {
name: string
fractionSize: number
@@ -735,6 +744,8 @@ export type PaymentDetails = {
export enum PaymentMethod {
LIGHTNING = "lightning",
BOLT11_INVOICE = "bolt11Invoice",
BOLT12_OFFER = "bolt12Offer",
BITCOIN_ADDRESS = "bitcoinAddress",
LIQUID_ADDRESS = "liquidAddress"
}
@@ -949,6 +960,11 @@ export const receivePayment = async (req: ReceivePaymentRequest): Promise<Receiv
return response
}
export const createBolt12Invoice = async (req: CreateBolt12InvoiceRequest): Promise<CreateBolt12InvoiceResponse> => {
const response = await BreezSDKLiquid.createBolt12Invoice(req)
return response
}
export const fetchLightningLimits = async (): Promise<LightningPaymentLimitsResponse> => {
const response = await BreezSDKLiquid.fetchLightningLimits()
return response