Add Config (#267)

* Add config

* Add rustdocs to Config, send_payment (#271)

---------

Co-authored-by: ok300 <106775972+ok300@users.noreply.github.com>
This commit is contained in:
Ross Savage
2024-06-01 06:32:45 +02:00
committed by GitHub
parent ccba0adf30
commit 5d9a8f18a3
29 changed files with 735 additions and 197 deletions

View File

@@ -74,10 +74,11 @@ async fn main() -> Result<()> {
let mnemonic = persistence.get_or_create_mnemonic()?;
let network = args.network.unwrap_or(Network::Testnet);
let mut config = LiquidSdk::default_config(network);
config.working_dir = data_dir_str;
let sdk = LiquidSdk::connect(ConnectRequest {
mnemonic: mnemonic.to_string(),
data_dir: Some(data_dir_str),
network,
config,
})
.await?;
let listener_id = sdk

View File

@@ -55,10 +55,17 @@ typedef struct wire_cst_prepare_send_response {
uint64_t fees_sat;
} wire_cst_prepare_send_response;
typedef struct wire_cst_config {
struct wire_cst_list_prim_u_8_strict *boltz_url;
struct wire_cst_list_prim_u_8_strict *electrum_url;
struct wire_cst_list_prim_u_8_strict *working_dir;
int32_t network;
uint64_t payment_timeout_sec;
} wire_cst_config;
typedef struct wire_cst_connect_request {
struct wire_cst_list_prim_u_8_strict *mnemonic;
struct wire_cst_list_prim_u_8_strict *data_dir;
int32_t network;
struct wire_cst_config config;
} wire_cst_connect_request;
typedef struct wire_cst_payment {
@@ -271,6 +278,8 @@ void frbgen_breez_liquid_wire__crate__bindings__breez_log_stream(int64_t port_,
void frbgen_breez_liquid_wire__crate__bindings__connect(int64_t port_,
struct wire_cst_connect_request *req);
void frbgen_breez_liquid_wire__crate__bindings__default_config(int64_t port_, int32_t network);
void frbgen_breez_liquid_wire__crate__bindings__parse_invoice(int64_t port_,
struct wire_cst_list_prim_u_8_strict *input);
@@ -337,6 +346,7 @@ static int64_t dummy_method_to_enforce_bundling(void) {
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__BindingLiquidSdk_sync);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__breez_log_stream);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__connect);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__default_config);
dummy_var ^= ((int64_t) (void*) frbgen_breez_liquid_wire__crate__bindings__parse_invoice);
dummy_var ^= ((int64_t) (void*) store_dart_post_cobject);
return dummy_var;

View File

@@ -22,6 +22,11 @@
{%- match func.return_type() -%}
{%- when Some with (return_type) %}
val res = {{ obj_interface }}{{ func.name()|fn_name|unquote }}({%- call kt::arg_list(func) -%})
{%- if func.name() == "default_config" %}
val workingDir = File(reactApplicationContext.filesDir.toString() + "/breezLiquidSdk")
res.workingDir = workingDir.absolutePath
{%- endif -%}
{%- match return_type %}
{%- when Type::Optional(inner) %}
{%- let unboxed = inner.as_ref() %}

View File

@@ -36,6 +36,19 @@ class BreezLiquidSDKModule(reactContext: ReactApplicationContext) : ReactContext
throw LiquidSdkException.Generic("Not initialized")
}
@Throws(LiquidSdkException::class)
private fun ensureWorkingDir(workingDir: String) {
try {
val workingDirFile = File(workingDir)
if (!workingDirFile.exists() && !workingDirFile.mkdirs()) {
throw LiquidSdkException.Generic("Mandatory field workingDir must contain a writable directory")
}
} catch (e: SecurityException) {
throw LiquidSdkException.Generic("Mandatory field workingDir must contain a writable directory")
}
}
@ReactMethod
fun addListener(eventName: String) {}
@@ -73,7 +86,9 @@ class BreezLiquidSDKModule(reactContext: ReactApplicationContext) : ReactContext
executor.execute {
try {
var connectRequest = asConnectRequest(req) ?: run { throw LiquidSdkException.Generic(errMissingMandatoryField("req", "ConnectRequest")) }
connectRequest.dataDir = connectRequest.dataDir?.takeUnless { it.isEmpty() } ?: run { reactApplicationContext.filesDir.toString() + "/breezLiquidSdk" }
ensureWorkingDir(connectRequest.config.workingDir)
bindingLiquidSdk = connect(connectRequest)
promise.resolve(readableMapOf("status" to "ok"))
} catch (e: Exception) {

View File

@@ -21,6 +21,9 @@
{%- match func.return_type() -%}
{%- when Some with (return_type) %}
var res = {%- call swift::throws_decl(func) -%}{{ obj_interface }}{{ func.name()|fn_name|unquote }}({%- call swift::arg_list(func) -%})
{%- if func.name() == "default_config" %}
res.workingDir = RNBreezLiquidSDK.breezLiquidSdkDirectory.path
{%- endif -%}
{%- match return_type %}
{%- when Type::Optional(inner) %}
{%- let unboxed = inner.as_ref() %}

View File

@@ -11,10 +11,16 @@ class RNBreezLiquidSDK: RCTEventEmitter {
private var bindingLiquidSdk: BindingLiquidSdk!
static var defaultDataDir: URL {
let applicationDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
return applicationDirectory.appendingPathComponent("breezLiquidSdk", isDirectory: true)
static var breezLiquidSdkDirectory: URL {
let applicationDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
let breezLiquidSdkDirectory = applicationDirectory.appendingPathComponent("breezLiquidSdk", isDirectory: true)
if !FileManager.default.fileExists(atPath: breezLiquidSdkDirectory.path) {
try! FileManager.default.createDirectory(atPath: breezLiquidSdkDirectory.path, withIntermediateDirectories: true)
}
return breezLiquidSdkDirectory
}
override init() {
@@ -56,6 +62,16 @@ class RNBreezLiquidSDK: RCTEventEmitter {
throw LiquidSdkError.Generic(message: "Not initialized")
}
private func ensureWorkingDir(workingDir: String) throws {
do {
if !FileManager.default.fileExists(atPath: workingDir) {
try FileManager.default.createDirectory(atPath: workingDir, withIntermediateDirectories: true)
}
} catch {
throw LiquidSdkError.Generic(message: "Mandatory field workingDir must contain a writable directory")
}
}
{% let obj_interface = "BreezLiquidSDK." -%}
{% for func in ci.function_definitions() %}
{%- if func.name()|ignored_function == false -%}
@@ -81,7 +97,8 @@ class RNBreezLiquidSDK: RCTEventEmitter {
do {
var connectRequest = try BreezLiquidSDKMapper.asConnectRequest(connectRequest: req)
connectRequest.dataDir = connectRequest.dataDir == nil || connectRequest.dataDir!.isEmpty ? RNBreezLiquidSDK.defaultDataDir.path : connectRequest.dataDir
try ensureWorkingDir(workingDir: connectRequest.config.workingDir)
bindingLiquidSdk = try BreezLiquidSDK.connect(req: connectRequest)
resolve(["status": "ok"])
} catch let err {

View File

@@ -1,4 +1,4 @@
import { NativeModules, Platform, NativeEventEmitter } from "react-native"
import { NativeModules, Platform, EmitterSubscription, NativeEventEmitter } from "react-native"
const LINKING_ERROR =
`The package 'react-native-breez-liquid-sdk' doesn't seem to be linked. Make sure: \n\n` +
@@ -20,7 +20,7 @@ const BreezLiquidSDK = NativeModules.RNBreezLiquidSDK
const BreezLiquidSDKEmitter = new NativeEventEmitter(BreezLiquidSDK)
{%- import "macros.ts" as ts %}
{%- include "Types.ts" %}
{% include "Helpers.ts" -%}
{% include "Helpers.ts" %}
{% for func in ci.function_definitions() %}
{%- if func.name()|ignored_function == false -%}
{%- include "TopLevelFunctionTemplate.ts" %}

View File

@@ -23,15 +23,22 @@ enum PaymentError {
"SignerError",
};
dictionary Config {
string boltz_url;
string electrum_url;
string working_dir;
Network network;
u64 payment_timeout_sec;
};
enum Network {
"Mainnet",
"Testnet",
};
dictionary ConnectRequest {
Config config;
string mnemonic;
Network network;
string? data_dir = null;
};
dictionary GetInfoRequest {
@@ -166,6 +173,8 @@ namespace breez_liquid_sdk {
[Throws=LiquidSdkError]
void set_logger(Logger logger);
Config default_config(Network network);
[Throws=PaymentError]
LNInvoice parse_invoice(string invoice);
};

View File

@@ -58,6 +58,10 @@ pub fn connect(req: ConnectRequest) -> Result<Arc<BindingLiquidSdk>, LiquidSdkEr
})
}
pub fn default_config(network: Network) -> Config {
LiquidSdk::default_config(network)
}
pub fn parse_invoice(input: String) -> Result<LNInvoice, PaymentError> {
LiquidSdk::parse_invoice(&input)
}

View File

@@ -7,9 +7,9 @@ class SDKListener: breez_liquid_sdk.EventListener {
try {
var mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
var connectReq = breez_liquid_sdk.ConnectRequest(mnemonic, breez_liquid_sdk.Network.LIQUID_TESTNET)
var sdk = breez_liquid_sdk.connect(connectReq)
var config = breez_liquid_sdk.defaultConfig(breez_liquid_sdk.Network.TESTNET)
var connectRequest = breez_liquid_sdk.ConnectRequest(config, mnemonic)
var sdk = breez_liquid_sdk.connect(connectRequest)
var listenerId = sdk.addEventListener(SDKListener())

View File

@@ -8,9 +8,9 @@ class SDKListener(breez_liquid_sdk.EventListener):
def test():
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
connect_req = breez_liquid_sdk.ConnectRequest(mnemonic=mnemonic, network=breez_liquid_sdk.Network.LIQUID_TESTNET)
sdk = breez_liquid_sdk.connect(connect_req)
config = breez_liquid_sdk.default_config(breez_liquid_sdk.Network.TESTNET)
connect_request = breez_liquid_sdk.ConnectRequest(config=config, mnemonic=mnemonic)
sdk = breez_liquid_sdk.connect(connect_request)
listener_id = sdk.add_event_listener(SDKListener())

View File

@@ -7,9 +7,9 @@ class SDKListener: EventListener {
}
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let connectReq = breez_liquid_sdk.ConnectRequest(mnemonic: mnemonic, network: .liquidTestnet);
let sdk = try breez_liquid_sdk.connect(req: connectReq);
let config = breez_liquid_sdk.defaultConfig(network: .testnet);
let connectRequest = breez_liquid_sdk.ConnectRequest(config: config, mnemonic: mnemonic);
let sdk = try breez_liquid_sdk.connect(req: connectRequest);
let listenerId = try sdk.addEventListener(listener: SDKListener());

View File

@@ -59,6 +59,10 @@ pub fn breez_log_stream(s: StreamSink<LogEntry>) -> Result<()> {
Ok(())
}
pub fn default_config(network: Network) -> Config {
LiquidSdk::default_config(network)
}
pub fn parse_invoice(input: String) -> Result<LNInvoice, PaymentError> {
LiquidSdk::parse_invoice(&input)
}

View File

@@ -157,13 +157,24 @@ impl CstDecode<u64> for *mut u64 {
unsafe { *flutter_rust_bridge::for_generated::box_from_leak_ptr(self) }
}
}
impl CstDecode<crate::model::Config> for wire_cst_config {
// Codec=Cst (C-struct based), see doc to use other codecs
fn cst_decode(self) -> crate::model::Config {
crate::model::Config {
boltz_url: self.boltz_url.cst_decode(),
electrum_url: self.electrum_url.cst_decode(),
working_dir: self.working_dir.cst_decode(),
network: self.network.cst_decode(),
payment_timeout_sec: self.payment_timeout_sec.cst_decode(),
}
}
}
impl CstDecode<crate::model::ConnectRequest> for wire_cst_connect_request {
// Codec=Cst (C-struct based), see doc to use other codecs
fn cst_decode(self) -> crate::model::ConnectRequest {
crate::model::ConnectRequest {
mnemonic: self.mnemonic.cst_decode(),
data_dir: self.data_dir.cst_decode(),
network: self.network.cst_decode(),
config: self.config.cst_decode(),
}
}
}
@@ -472,12 +483,27 @@ impl Default for wire_cst_backup_request {
Self::new_with_null_ptr()
}
}
impl NewWithNullPtr for wire_cst_config {
fn new_with_null_ptr() -> Self {
Self {
boltz_url: core::ptr::null_mut(),
electrum_url: core::ptr::null_mut(),
working_dir: core::ptr::null_mut(),
network: Default::default(),
payment_timeout_sec: Default::default(),
}
}
}
impl Default for wire_cst_config {
fn default() -> Self {
Self::new_with_null_ptr()
}
}
impl NewWithNullPtr for wire_cst_connect_request {
fn new_with_null_ptr() -> Self {
Self {
mnemonic: core::ptr::null_mut(),
data_dir: core::ptr::null_mut(),
network: Default::default(),
config: Default::default(),
}
}
}
@@ -844,6 +870,14 @@ pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__connect(
wire__crate__bindings__connect_impl(port_, req)
}
#[no_mangle]
pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__default_config(
port_: i64,
network: i32,
) {
wire__crate__bindings__default_config_impl(port_, network)
}
#[no_mangle]
pub extern "C" fn frbgen_breez_liquid_wire__crate__bindings__parse_invoice(
port_: i64,
@@ -1002,10 +1036,18 @@ pub struct wire_cst_backup_request {
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct wire_cst_config {
boltz_url: *mut wire_cst_list_prim_u_8_strict,
electrum_url: *mut wire_cst_list_prim_u_8_strict,
working_dir: *mut wire_cst_list_prim_u_8_strict,
network: i32,
payment_timeout_sec: u64,
}
#[repr(C)]
#[derive(Clone, Copy)]
pub struct wire_cst_connect_request {
mnemonic: *mut wire_cst_list_prim_u_8_strict,
data_dir: *mut wire_cst_list_prim_u_8_strict,
network: i32,
config: wire_cst_config,
}
#[repr(C)]
#[derive(Clone, Copy)]

View File

@@ -34,7 +34,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
default_rust_auto_opaque = RustAutoOpaqueNom,
);
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.0.0-dev.36";
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -532134055;
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1028270774;
// Section: executor
@@ -390,6 +390,26 @@ fn wire__crate__bindings__connect_impl(
},
)
}
fn wire__crate__bindings__default_config_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
network: impl CstDecode<crate::model::Network>,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::<flutter_rust_bridge::for_generated::DcoCodec, _, _>(
flutter_rust_bridge::for_generated::TaskInfo {
debug_name: "default_config",
port: Some(port_),
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
},
move || {
let api_network = network.cst_decode();
move |context| {
transform_result_dco((move || {
Result::<_, ()>::Ok(crate::bindings::default_config(api_network))
})())
}
},
)
}
fn wire__crate__bindings__parse_invoice_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
input: impl CstDecode<String>,
@@ -553,16 +573,32 @@ impl SseDecode for bool {
}
}
impl SseDecode for crate::model::Config {
// 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_boltzUrl = <String>::sse_decode(deserializer);
let mut var_electrumUrl = <String>::sse_decode(deserializer);
let mut var_workingDir = <String>::sse_decode(deserializer);
let mut var_network = <crate::model::Network>::sse_decode(deserializer);
let mut var_paymentTimeoutSec = <u64>::sse_decode(deserializer);
return crate::model::Config {
boltz_url: var_boltzUrl,
electrum_url: var_electrumUrl,
working_dir: var_workingDir,
network: var_network,
payment_timeout_sec: var_paymentTimeoutSec,
};
}
}
impl SseDecode for crate::model::ConnectRequest {
// 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_mnemonic = <String>::sse_decode(deserializer);
let mut var_dataDir = <Option<String>>::sse_decode(deserializer);
let mut var_network = <crate::model::Network>::sse_decode(deserializer);
let mut var_config = <crate::model::Config>::sse_decode(deserializer);
return crate::model::ConnectRequest {
mnemonic: var_mnemonic,
data_dir: var_dataDir,
network: var_network,
config: var_config,
};
}
}
@@ -1113,12 +1149,30 @@ impl flutter_rust_bridge::IntoIntoDart<crate::model::BackupRequest>
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::model::Config {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[
self.boltz_url.into_into_dart().into_dart(),
self.electrum_url.into_into_dart().into_dart(),
self.working_dir.into_into_dart().into_dart(),
self.network.into_into_dart().into_dart(),
self.payment_timeout_sec.into_into_dart().into_dart(),
]
.into_dart()
}
}
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::model::Config {}
impl flutter_rust_bridge::IntoIntoDart<crate::model::Config> for crate::model::Config {
fn into_into_dart(self) -> crate::model::Config {
self
}
}
// Codec=Dco (DartCObject based), see doc to use other codecs
impl flutter_rust_bridge::IntoDart for crate::model::ConnectRequest {
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
[
self.mnemonic.into_into_dart().into_dart(),
self.data_dir.into_into_dart().into_dart(),
self.network.into_into_dart().into_dart(),
self.config.into_into_dart().into_dart(),
]
.into_dart()
}
@@ -1603,12 +1657,22 @@ impl SseEncode for bool {
}
}
impl SseEncode for crate::model::Config {
// 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.boltz_url, serializer);
<String>::sse_encode(self.electrum_url, serializer);
<String>::sse_encode(self.working_dir, serializer);
<crate::model::Network>::sse_encode(self.network, serializer);
<u64>::sse_encode(self.payment_timeout_sec, serializer);
}
}
impl SseEncode for crate::model::ConnectRequest {
// 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.mnemonic, serializer);
<Option<String>>::sse_encode(self.data_dir, serializer);
<crate::model::Network>::sse_encode(self.network, serializer);
<crate::model::Config>::sse_encode(self.config, serializer);
}
}

View File

@@ -1,11 +1,13 @@
use anyhow::{anyhow, Result};
use boltz_client::network::electrum::ElectrumConfig;
use boltz_client::network::Chain;
use boltz_client::swaps::boltzv2::{
CreateReverseResponse, CreateSubmarineResponse, Leaf, SwapTree,
CreateReverseResponse, CreateSubmarineResponse, Leaf, SwapTree, BOLTZ_MAINNET_URL_V2,
BOLTZ_TESTNET_URL_V2,
};
use boltz_client::{Keypair, LBtcSwapScriptV2, ToHex};
use lwk_signer::SwSigner;
use lwk_wollet::{ElectrumUrl, ElementsNetwork, WolletDescriptor};
use lwk_wollet::{ElectrumClient, ElectrumUrl, ElementsNetwork, WolletDescriptor};
use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef};
use rusqlite::ToSql;
use serde::{Deserialize, Serialize};
@@ -13,6 +15,49 @@ use serde::{Deserialize, Serialize};
use crate::error::PaymentError;
use crate::utils;
/// Configuration for the Liquid SDK
#[derive(Clone, Debug, Serialize)]
pub struct Config {
pub boltz_url: String,
pub electrum_url: String,
/// Directory in which all SDK files (DB, log, cache) are stored.
///
/// Prefix can be a relative or absolute path to this directory.
pub working_dir: String,
pub network: Network,
/// Send payment timeout. See [crate::sdk::LiquidSdk::send_payment]
pub payment_timeout_sec: u64,
}
impl Config {
pub(crate) fn get_electrum_client(&self) -> Result<ElectrumClient, lwk_wollet::Error> {
ElectrumClient::new(&ElectrumUrl::new(&self.electrum_url, true, true))
}
pub(crate) fn get_electrum_config(&self) -> ElectrumConfig {
ElectrumConfig::new(self.network.into(), &self.electrum_url, true, true, 100)
}
pub fn mainnet() -> Self {
Config {
boltz_url: BOLTZ_MAINNET_URL_V2.to_owned(),
electrum_url: "blockstream.info:995".to_string(),
working_dir: ".".to_string(),
network: Network::Mainnet,
payment_timeout_sec: 15,
}
}
pub fn testnet() -> Self {
Config {
boltz_url: BOLTZ_TESTNET_URL_V2.to_owned(),
electrum_url: "blockstream.info:465".to_string(),
working_dir: ".".to_string(),
network: Network::Testnet,
payment_timeout_sec: 15,
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize)]
pub enum Network {
/// Mainnet Bitcoin and Liquid chains
@@ -84,39 +129,18 @@ pub enum LiquidSdkEvent {
}
pub struct LiquidSdkOptions {
pub config: Config,
pub signer: SwSigner,
pub network: Network,
/// Output script descriptor
///
/// See <https://github.com/bitcoin/bips/pull/1143>
pub descriptor: WolletDescriptor,
/// Absolute or relative path to the data dir, including the dir name.
///
/// If not set, it defaults to [crate::DEFAULT_DATA_DIR].
pub data_dir_path: Option<String>,
/// Custom Electrum URL. If set, it must match the specified network.
///
/// If not set, it defaults to a Blockstream instance.
pub electrum_url: Option<ElectrumUrl>,
}
impl LiquidSdkOptions {
pub(crate) fn get_electrum_url(&self) -> ElectrumUrl {
self.electrum_url.clone().unwrap_or({
let (url, validate_domain, tls) = match &self.network {
Network::Mainnet => ("blockstream.info:995", true, true),
Network::Testnet => ("blockstream.info:465", true, true),
};
ElectrumUrl::new(url, tls, validate_domain)
})
}
}
#[derive(Debug, Serialize)]
pub struct ConnectRequest {
pub mnemonic: String,
pub data_dir: Option<String>,
pub network: Network,
pub config: Config,
}
#[derive(Debug, Serialize)]

View File

@@ -13,7 +13,6 @@ use boltz_client::network::Chain;
use boltz_client::swaps::boltzv2;
use boltz_client::ToHex;
use boltz_client::{
network::electrum::ElectrumConfig,
swaps::{
boltz::{RevSwapStates, SubSwapStates},
boltzv2::*,
@@ -30,8 +29,7 @@ use lwk_wollet::elements::LockTime;
use lwk_wollet::hashes::{sha256, Hash};
use lwk_wollet::{
elements::{Address, Transaction},
BlockchainBackend, ElectrumClient, ElectrumUrl, ElementsNetwork, FsPersister,
Wollet as LwkWollet, WolletDescriptor,
BlockchainBackend, ElementsNetwork, FsPersister, Wollet as LwkWollet, WolletDescriptor,
};
use tokio::sync::{watch, Mutex, RwLock};
use tokio::time::MissedTickBehavior;
@@ -56,14 +54,12 @@ pub const LIQUID_CLAIM_TX_FEERATE_MSAT: f32 = 100.0;
pub const DEFAULT_DATA_DIR: &str = ".data";
pub struct LiquidSdk {
electrum_url: ElectrumUrl,
network: Network,
config: Config,
/// LWK Wollet, a watch-only Liquid wallet for this instance
lwk_wollet: Arc<Mutex<LwkWollet>>,
/// LWK Signer, for signing Liquid transactions
lwk_signer: SwSigner,
persister: Arc<Persister>,
data_dir_path: String,
event_manager: Arc<EventManager>,
status_stream: Arc<BoltzStatusStream>,
is_started: RwLock<bool>,
@@ -73,16 +69,15 @@ pub struct LiquidSdk {
impl LiquidSdk {
pub async fn connect(req: ConnectRequest) -> Result<Arc<LiquidSdk>> {
let is_mainnet = req.network == Network::Mainnet;
let config = req.config;
let is_mainnet = config.network == Network::Mainnet;
let signer = SwSigner::new(&req.mnemonic, is_mainnet)?;
let descriptor = LiquidSdk::get_descriptor(&signer, req.network)?;
let descriptor = LiquidSdk::get_descriptor(&signer, config.network)?;
let sdk = LiquidSdk::new(LiquidSdkOptions {
signer,
descriptor,
electrum_url: None,
data_dir_path: req.data_dir,
network: req.network,
config,
})?;
sdk.start().await?;
@@ -90,33 +85,30 @@ impl LiquidSdk {
}
fn new(opts: LiquidSdkOptions) -> Result<Arc<Self>> {
let network = opts.network;
let elements_network: ElementsNetwork = opts.network.into();
let electrum_url = opts.get_electrum_url();
let data_dir_path = opts.data_dir_path.unwrap_or(DEFAULT_DATA_DIR.to_string());
let config = opts.config;
let elements_network: ElementsNetwork = config.network.into();
let lwk_persister = FsPersister::new(&data_dir_path, network.into(), &opts.descriptor)?;
fs::create_dir_all(&config.working_dir)?;
let lwk_persister = FsPersister::new(
config.working_dir.clone(),
elements_network,
&opts.descriptor,
)?;
let lwk_wollet = LwkWollet::new(elements_network, lwk_persister, opts.descriptor)?;
fs::create_dir_all(&data_dir_path)?;
let persister = Arc::new(Persister::new(&data_dir_path, network)?);
let persister = Arc::new(Persister::new(&config.working_dir, config.network)?);
persister.init()?;
let event_manager = Arc::new(EventManager::new());
let status_stream = Arc::new(BoltzStatusStream::new(
Self::boltz_url_v2(network),
persister.clone(),
));
let status_stream = Arc::new(BoltzStatusStream::new(&config.boltz_url, persister.clone()));
let (shutdown_sender, shutdown_receiver) = watch::channel::<()>(());
let sdk = Arc::new(LiquidSdk {
config,
lwk_wollet: Arc::new(Mutex::new(lwk_wollet)),
network,
electrum_url,
lwk_signer: opts.signer,
persister,
data_dir_path,
event_manager,
status_stream,
is_started: RwLock::new(false),
@@ -729,24 +721,7 @@ impl LiquidSdk {
}
pub(crate) fn boltz_client_v2(&self) -> BoltzApiClientV2 {
BoltzApiClientV2::new(Self::boltz_url_v2(self.network))
}
pub(crate) fn boltz_url_v2(network: Network) -> &'static str {
match network {
Network::Testnet => BOLTZ_TESTNET_URL_V2,
Network::Mainnet => BOLTZ_MAINNET_URL_V2,
}
}
fn network_config(&self) -> ElectrumConfig {
ElectrumConfig::new(
self.network.into(),
&self.electrum_url.to_string(),
true,
true,
100,
)
BoltzApiClientV2::new(&self.config.boltz_url)
}
async fn build_tx(
@@ -756,7 +731,7 @@ impl LiquidSdk {
amount_sat: u64,
) -> Result<Transaction, PaymentError> {
let lwk_wollet = self.lwk_wollet.lock().await;
let mut pset = lwk_wollet::TxBuilder::new(self.network.into())
let mut pset = lwk_wollet::TxBuilder::new(self.config.network.into())
.add_lbtc_recipient(
&ElementsAddress::from_str(recipient_address).map_err(|e| {
PaymentError::Generic {
@@ -780,7 +755,7 @@ impl LiquidSdk {
.parse::<Bolt11Invoice>()
.map_err(|_| PaymentError::InvalidInvoice)?;
match (invoice.network().to_string().as_str(), self.network) {
match (invoice.network().to_string().as_str(), self.config.network) {
("bitcoin", Network::Mainnet) => {}
("testnet", Network::Testnet) => {}
_ => return Err(PaymentError::InvalidInvoice),
@@ -815,7 +790,7 @@ impl LiquidSdk {
async fn get_broadcast_fee_estimation(&self, amount_sat: u64) -> Result<u64> {
// TODO Replace this with own address when LWK supports taproot
// https://github.com/Blockstream/lwk/issues/31
let temp_p2tr_addr = match self.network {
let temp_p2tr_addr = match self.config.network {
Network::Mainnet => "lq1pqvzxvqhrf54dd4sny4cag7497pe38252qefk46t92frs7us8r80ja9ha8r5me09nn22m4tmdqp5p4wafq3s59cql3v9n45t5trwtxrmxfsyxjnstkctj",
Network::Testnet => "tlq1pq0wqu32e2xacxeyps22x8gjre4qk3u6r70pj4r62hzczxeyz8x3yxucrpn79zy28plc4x37aaf33kwt6dz2nn6gtkya6h02mwpzy4eh69zzexq7cf5y5"
};
@@ -871,12 +846,12 @@ impl LiquidSdk {
swap_script: &LBtcSwapScriptV2,
) -> Result<LBtcSwapTxV2, PaymentError> {
let output_address = self.next_unused_address().await?.to_string();
let network_config = self.network_config();
let network_config = self.config.get_electrum_config();
Ok(LBtcSwapTxV2::new_refund(
swap_script.clone(),
&output_address,
&network_config,
Self::boltz_url_v2(self.network).to_string(),
self.config.clone().boltz_url,
swap_id.to_string(),
)?)
}
@@ -895,7 +870,8 @@ impl LiquidSdk {
Some((&self.boltz_client_v2(), &swap.id)),
)?;
let refund_tx_id = refund_tx.broadcast(&tx, &self.network_config(), is_lowball)?;
let refund_tx_id =
refund_tx.broadcast(&tx, &self.config.get_electrum_config(), is_lowball)?;
info!(
"Successfully broadcast cooperative refund for Send Swap {}",
&swap.id
@@ -925,9 +901,9 @@ impl LiquidSdk {
info!("locktime info: locktime_from_height = {locktime_from_height:?}, swap_script.locktime = {:?}", swap_script.locktime);
match utils::is_locktime_expired(locktime_from_height, swap_script.locktime) {
true => {
let tx =
refund_tx.sign_refund(&swap.get_refund_keypair()?, broadcast_fees_sat, None)?;
let refund_tx_id = refund_tx.broadcast(&tx, &self.network_config(), is_lowball)?;
let tx = refund_tx.sign_refund(&swap.get_refund_keypair()?, broadcast_fees_sat, None)?;
let refund_tx_id =
refund_tx.broadcast(&tx, &self.config.get_electrum_config(), is_lowball)?;
info!(
"Successfully broadcast non-cooperative refund for Send Swap {}",
swap.id
@@ -950,7 +926,7 @@ impl LiquidSdk {
let broadcast_fees_sat =
Amount::from_sat(self.get_broadcast_fee_estimation(amount_sat).await?);
let client = self.boltz_client_v2();
let is_lowball = match self.network {
let is_lowball = match self.config.network {
Network::Mainnet => None,
Network::Testnet => Some((&client, boltz_client::network::Chain::LiquidTestnet)),
};
@@ -1031,7 +1007,7 @@ impl LiquidSdk {
)
.await?;
let electrum_client = ElectrumClient::new(&self.electrum_url)?;
let electrum_client = self.config.get_electrum_client()?;
let lockup_tx_id = electrum_client.broadcast(&lockup_tx)?.to_string();
debug!(
@@ -1040,6 +1016,13 @@ impl LiquidSdk {
Ok(lockup_tx_id)
}
/// Creates, initiates and starts monitoring the progress of a Send Payment.
///
/// Depending on [Config]'s `payment_timeout_sec`, this function will return:
/// - a [PaymentError::PaymentTimeout], if the payment could not be initiated in this time
/// - a [PaymentState::Pending] payment, if the payment could be initiated, but didn't yet
/// complete in this time
/// - a [PaymentState::Complete] payment, if the payment was successfully completed in this time
pub async fn send_payment(
&self,
req: &PrepareSendResponse,
@@ -1110,7 +1093,7 @@ impl LiquidSdk {
swap_id: String,
accept_zero_conf: bool,
) -> Result<Payment, PaymentError> {
let timeout_fut = tokio::time::sleep(Duration::from_secs(15));
let timeout_fut = tokio::time::sleep(Duration::from_secs(self.config.payment_timeout_sec));
tokio::pin!(timeout_fut);
let mut events_stream = self.event_manager.subscribe();
@@ -1178,8 +1161,8 @@ impl LiquidSdk {
let claim_tx_wrapper = LBtcSwapTxV2::new_claim(
swap_script,
claim_address,
&self.network_config(),
Self::boltz_url_v2(self.network).into(),
&self.config.get_electrum_config(),
self.config.clone().boltz_url,
ongoing_receive_swap.id.clone(),
)?;
@@ -1194,8 +1177,8 @@ impl LiquidSdk {
let claim_tx_id = claim_tx_wrapper.broadcast(
&claim_tx,
&self.network_config(),
Some((&self.boltz_client_v2(), self.network.into())),
&self.config.get_electrum_config(),
Some((&self.boltz_client_v2(), self.config.network.into())),
)?;
info!("Successfully broadcast claim tx {claim_tx_id} for Receive Swap {swap_id}");
debug!("Claim Tx {:?}", claim_tx);
@@ -1329,7 +1312,7 @@ impl LiquidSdk {
/// it inserts or updates a corresponding entry in our Payments table.
async fn sync_payments_with_chain_data(&self, with_scan: bool) -> Result<()> {
if with_scan {
let mut electrum_client = ElectrumClient::new(&self.electrum_url)?;
let mut electrum_client = self.config.get_electrum_client()?;
let mut lwk_wollet = self.lwk_wollet.lock().await;
lwk_wollet::full_scan_with_electrum_client(&mut lwk_wollet, &mut electrum_client)?;
}
@@ -1378,9 +1361,11 @@ impl LiquidSdk {
info!("Retrieving preimage from non-cooperative claim tx");
let id = &swap.id;
let electrum_client = ElectrumClient::new(&self.electrum_url)?;
let electrum_client = self.config.get_electrum_client()?;
let swap_script = swap.get_swap_script()?;
let swap_script_pk = swap_script.to_address(self.network.into())?.script_pubkey();
let swap_script_pk = swap_script
.to_address(self.config.network.into())?
.script_pubkey();
debug!("Found Send Swap swap_script_pk: {swap_script_pk:?}");
// Get tx history of the swap script (lockup address)
@@ -1445,8 +1430,8 @@ impl LiquidSdk {
/// Empties all Liquid Wallet caches for this network type.
pub fn empty_wallet_cache(&self) -> Result<()> {
let mut path = PathBuf::from(self.data_dir_path.clone());
path.push(Into::<ElementsNetwork>::into(self.network).as_str());
let mut path = PathBuf::from(self.config.working_dir.clone());
path.push(Into::<ElementsNetwork>::into(self.config.network).as_str());
path.push("enc_cache");
fs::remove_dir_all(&path)?;
@@ -1484,6 +1469,13 @@ impl LiquidSdk {
self.persister.restore_from_backup(backup_path)
}
pub fn default_config(network: Network) -> Config {
match network {
Network::Mainnet => Config::mainnet(),
Network::Testnet => Config::testnet(),
}
}
pub fn parse_invoice(input: &str) -> Result<LNInvoice, PaymentError> {
let input = input
.strip_prefix("lightning:")
@@ -1566,7 +1558,7 @@ mod tests {
use tempdir::TempDir;
use crate::model::*;
use crate::sdk::{LiquidSdk, Network};
use crate::sdk::LiquidSdk;
const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
@@ -1594,10 +1586,11 @@ mod tests {
#[tokio::test]
async fn normal_submarine_swap() -> Result<()> {
let (_data_dir, data_dir_str) = create_temp_dir()?;
let mut config = Config::testnet();
config.working_dir = data_dir_str;
let sdk = LiquidSdk::connect(ConnectRequest {
mnemonic: TEST_MNEMONIC.to_string(),
data_dir: Some(data_dir_str),
network: Network::Testnet,
config,
})
.await?;
@@ -1612,10 +1605,11 @@ mod tests {
#[tokio::test]
async fn reverse_submarine_swap() -> Result<()> {
let (_data_dir, data_dir_str) = create_temp_dir()?;
let mut config = Config::testnet();
config.working_dir = data_dir_str;
let sdk = LiquidSdk::connect(ConnectRequest {
mnemonic: TEST_MNEMONIC.to_string(),
data_dir: Some(data_dir_str),
network: Network::Testnet,
config,
})
.await?;

View File

@@ -18,6 +18,9 @@ Future<BindingLiquidSdk> connect({required ConnectRequest req, dynamic hint}) =>
Stream<LogEntry> breezLogStream({dynamic hint}) =>
RustLib.instance.api.crateBindingsBreezLogStream(hint: hint);
Future<Config> defaultConfig({required Network network, dynamic hint}) =>
RustLib.instance.api.crateBindingsDefaultConfig(network: network, hint: hint);
Future<LNInvoice> parseInvoice({required String input, dynamic hint}) =>
RustLib.instance.api.crateBindingsParseInvoice(input: input, hint: hint);

View File

@@ -53,7 +53,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
String get codegenVersion => '2.0.0-dev.36';
@override
int get rustContentHash => -532134055;
int get rustContentHash => 1028270774;
static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig(
stem: 'breez_liquid_sdk',
@@ -100,6 +100,8 @@ abstract class RustLibApi extends BaseApi {
Future<BindingLiquidSdk> crateBindingsConnect({required ConnectRequest req, dynamic hint});
Future<Config> crateBindingsDefaultConfig({required Network network, dynamic hint});
Future<LNInvoice> crateBindingsParseInvoice({required String input, dynamic hint});
RustArcIncrementStrongCountFnType get rust_arc_increment_strong_count_BindingLiquidSdk;
@@ -485,6 +487,29 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: ["req"],
);
@override
Future<Config> crateBindingsDefaultConfig({required Network network, dynamic hint}) {
return handler.executeNormal(NormalTask(
callFfi: (port_) {
var arg0 = cst_encode_network(network);
return wire.wire__crate__bindings__default_config(port_, arg0);
},
codec: DcoCodec(
decodeSuccessData: dco_decode_config,
decodeErrorData: null,
),
constMeta: kCrateBindingsDefaultConfigConstMeta,
argValues: [network],
apiImpl: this,
hint: hint,
));
}
TaskConstMeta get kCrateBindingsDefaultConfigConstMeta => const TaskConstMeta(
debugName: "default_config",
argNames: ["network"],
);
@override
Future<LNInvoice> crateBindingsParseInvoice({required String input, dynamic hint}) {
return handler.executeNormal(NormalTask(
@@ -637,15 +662,28 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return dco_decode_u_64(raw);
}
@protected
Config dco_decode_config(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
final arr = raw as List<dynamic>;
if (arr.length != 5) throw Exception('unexpected arr length: expect 5 but see ${arr.length}');
return Config(
boltzUrl: dco_decode_String(arr[0]),
electrumUrl: dco_decode_String(arr[1]),
workingDir: dco_decode_String(arr[2]),
network: dco_decode_network(arr[3]),
paymentTimeoutSec: dco_decode_u_64(arr[4]),
);
}
@protected
ConnectRequest dco_decode_connect_request(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
final arr = raw as List<dynamic>;
if (arr.length != 3) throw Exception('unexpected arr length: expect 3 but see ${arr.length}');
if (arr.length != 2) throw Exception('unexpected arr length: expect 2 but see ${arr.length}');
return ConnectRequest(
mnemonic: dco_decode_String(arr[0]),
dataDir: dco_decode_opt_String(arr[1]),
network: dco_decode_network(arr[2]),
config: dco_decode_config(arr[1]),
);
}
@@ -1134,13 +1172,28 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
return (sse_decode_u_64(deserializer));
}
@protected
Config sse_decode_config(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
var var_boltzUrl = sse_decode_String(deserializer);
var var_electrumUrl = sse_decode_String(deserializer);
var var_workingDir = sse_decode_String(deserializer);
var var_network = sse_decode_network(deserializer);
var var_paymentTimeoutSec = sse_decode_u_64(deserializer);
return Config(
boltzUrl: var_boltzUrl,
electrumUrl: var_electrumUrl,
workingDir: var_workingDir,
network: var_network,
paymentTimeoutSec: var_paymentTimeoutSec);
}
@protected
ConnectRequest sse_decode_connect_request(SseDeserializer deserializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
var var_mnemonic = sse_decode_String(deserializer);
var var_dataDir = sse_decode_opt_String(deserializer);
var var_network = sse_decode_network(deserializer);
return ConnectRequest(mnemonic: var_mnemonic, dataDir: var_dataDir, network: var_network);
var var_config = sse_decode_config(deserializer);
return ConnectRequest(mnemonic: var_mnemonic, config: var_config);
}
@protected
@@ -1719,12 +1772,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
sse_encode_u_64(self, serializer);
}
@protected
void sse_encode_config(Config self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_String(self.boltzUrl, serializer);
sse_encode_String(self.electrumUrl, serializer);
sse_encode_String(self.workingDir, serializer);
sse_encode_network(self.network, serializer);
sse_encode_u_64(self.paymentTimeoutSec, serializer);
}
@protected
void sse_encode_connect_request(ConnectRequest self, SseSerializer serializer) {
// Codec=Sse (Serialization based), see doc to use other codecs
sse_encode_String(self.mnemonic, serializer);
sse_encode_opt_String(self.dataDir, serializer);
sse_encode_network(self.network, serializer);
sse_encode_config(self.config, serializer);
}
@protected

View File

@@ -85,6 +85,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
BigInt dco_decode_box_autoadd_u_64(dynamic raw);
@protected
Config dco_decode_config(dynamic raw);
@protected
ConnectRequest dco_decode_connect_request(dynamic raw);
@@ -246,6 +249,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
BigInt sse_decode_box_autoadd_u_64(SseDeserializer deserializer);
@protected
Config sse_decode_config(SseDeserializer deserializer);
@protected
ConnectRequest sse_decode_connect_request(SseDeserializer deserializer);
@@ -575,11 +581,19 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
cst_api_fill_to_wire_restore_request(apiObj, wireObj.ref);
}
@protected
void cst_api_fill_to_wire_config(Config apiObj, wire_cst_config wireObj) {
wireObj.boltz_url = cst_encode_String(apiObj.boltzUrl);
wireObj.electrum_url = cst_encode_String(apiObj.electrumUrl);
wireObj.working_dir = cst_encode_String(apiObj.workingDir);
wireObj.network = cst_encode_network(apiObj.network);
wireObj.payment_timeout_sec = cst_encode_u_64(apiObj.paymentTimeoutSec);
}
@protected
void cst_api_fill_to_wire_connect_request(ConnectRequest apiObj, wire_cst_connect_request wireObj) {
wireObj.mnemonic = cst_encode_String(apiObj.mnemonic);
wireObj.data_dir = cst_encode_opt_String(apiObj.dataDir);
wireObj.network = cst_encode_network(apiObj.network);
cst_api_fill_to_wire_config(apiObj.config, wireObj.config);
}
@protected
@@ -922,6 +936,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_box_autoadd_u_64(BigInt self, SseSerializer serializer);
@protected
void sse_encode_config(Config self, SseSerializer serializer);
@protected
void sse_encode_connect_request(ConnectRequest self, SseSerializer serializer);
@@ -1308,6 +1325,22 @@ class RustLibWire implements BaseWire {
late final _wire__crate__bindings__connect = _wire__crate__bindings__connectPtr
.asFunction<void Function(int, ffi.Pointer<wire_cst_connect_request>)>();
void wire__crate__bindings__default_config(
int port_,
int network,
) {
return _wire__crate__bindings__default_config(
port_,
network,
);
}
late final _wire__crate__bindings__default_configPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int64, ffi.Int32)>>(
'frbgen_breez_liquid_wire__crate__bindings__default_config');
late final _wire__crate__bindings__default_config =
_wire__crate__bindings__default_configPtr.asFunction<void Function(int, int)>();
void wire__crate__bindings__parse_invoice(
int port_,
ffi.Pointer<wire_cst_list_prim_u_8_strict> input,
@@ -1577,13 +1610,24 @@ final class wire_cst_prepare_send_response extends ffi.Struct {
external int fees_sat;
}
final class wire_cst_connect_request extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> mnemonic;
final class wire_cst_config extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> boltz_url;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> data_dir;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> electrum_url;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> working_dir;
@ffi.Int32()
external int network;
@ffi.Uint64()
external int payment_timeout_sec;
}
final class wire_cst_connect_request extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> mnemonic;
external wire_cst_config config;
}
final class wire_cst_payment extends ffi.Struct {

View File

@@ -28,19 +28,55 @@ class BackupRequest {
other is BackupRequest && runtimeType == other.runtimeType && backupPath == other.backupPath;
}
class ConnectRequest {
final String mnemonic;
final String? dataDir;
final Network network;
/// Configuration for the Liquid SDK
class Config {
final String boltzUrl;
final String electrumUrl;
const ConnectRequest({
required this.mnemonic,
this.dataDir,
/// Directory in which all SDK files (DB, log) are stored.
final String workingDir;
final Network network;
final BigInt paymentTimeoutSec;
const Config({
required this.boltzUrl,
required this.electrumUrl,
required this.workingDir,
required this.network,
required this.paymentTimeoutSec,
});
@override
int get hashCode => mnemonic.hashCode ^ dataDir.hashCode ^ network.hashCode;
int get hashCode =>
boltzUrl.hashCode ^
electrumUrl.hashCode ^
workingDir.hashCode ^
network.hashCode ^
paymentTimeoutSec.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Config &&
runtimeType == other.runtimeType &&
boltzUrl == other.boltzUrl &&
electrumUrl == other.electrumUrl &&
workingDir == other.workingDir &&
network == other.network &&
paymentTimeoutSec == other.paymentTimeoutSec;
}
class ConnectRequest {
final String mnemonic;
final Config config;
const ConnectRequest({
required this.mnemonic,
required this.config,
});
@override
int get hashCode => mnemonic.hashCode ^ config.hashCode;
@override
bool operator ==(Object other) =>
@@ -48,8 +84,7 @@ class ConnectRequest {
other is ConnectRequest &&
runtimeType == other.runtimeType &&
mnemonic == other.mnemonic &&
dataDir == other.dataDir &&
network == other.network;
config == other.config;
}
class GetInfoRequest {

View File

@@ -294,6 +294,22 @@ class FlutterBreezLiquidBindings {
_frbgen_breez_liquid_wire__crate__bindings__connectPtr
.asFunction<void Function(int, ffi.Pointer<wire_cst_connect_request>)>();
void frbgen_breez_liquid_wire__crate__bindings__default_config(
int port_,
int network,
) {
return _frbgen_breez_liquid_wire__crate__bindings__default_config(
port_,
network,
);
}
late final _frbgen_breez_liquid_wire__crate__bindings__default_configPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Int64, ffi.Int32)>>(
'frbgen_breez_liquid_wire__crate__bindings__default_config');
late final _frbgen_breez_liquid_wire__crate__bindings__default_config =
_frbgen_breez_liquid_wire__crate__bindings__default_configPtr.asFunction<void Function(int, int)>();
void frbgen_breez_liquid_wire__crate__bindings__parse_invoice(
int port_,
ffi.Pointer<wire_cst_list_prim_u_8_strict> input,
@@ -589,13 +605,24 @@ final class wire_cst_prepare_send_response extends ffi.Struct {
external int fees_sat;
}
final class wire_cst_connect_request extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> mnemonic;
final class wire_cst_config extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> boltz_url;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> data_dir;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> electrum_url;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> working_dir;
@ffi.Int32()
external int network;
@ffi.Uint64()
external int payment_timeout_sec;
}
final class wire_cst_connect_request extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> mnemonic;
external wire_cst_config config;
}
final class wire_cst_payment extends ffi.Struct {

View File

@@ -34,32 +34,78 @@ fun asBackupRequestList(arr: ReadableArray): List<BackupRequest> {
return list
}
fun asConnectRequest(connectRequest: ReadableMap): ConnectRequest? {
fun asConfig(config: ReadableMap): Config? {
if (!validateMandatoryFields(
connectRequest,
config,
arrayOf(
"mnemonic",
"boltzUrl",
"electrumUrl",
"workingDir",
"network",
"paymentTimeoutSec",
),
)
) {
return null
}
val mnemonic = connectRequest.getString("mnemonic")!!
val network = connectRequest.getString("network")?.let { asNetwork(it) }!!
val dataDir = if (hasNonNullKey(connectRequest, "dataDir")) connectRequest.getString("dataDir") else null
return ConnectRequest(
mnemonic,
val boltzUrl = config.getString("boltzUrl")!!
val electrumUrl = config.getString("electrumUrl")!!
val workingDir = config.getString("workingDir")!!
val network = config.getString("network")?.let { asNetwork(it) }!!
val paymentTimeoutSec = config.getDouble("paymentTimeoutSec").toULong()
return Config(
boltzUrl,
electrumUrl,
workingDir,
network,
dataDir,
paymentTimeoutSec,
)
}
fun readableMapOf(config: Config): ReadableMap {
return readableMapOf(
"boltzUrl" to config.boltzUrl,
"electrumUrl" to config.electrumUrl,
"workingDir" to config.workingDir,
"network" to config.network.name.lowercase(),
"paymentTimeoutSec" to config.paymentTimeoutSec,
)
}
fun asConfigList(arr: ReadableArray): List<Config> {
val list = ArrayList<Config>()
for (value in arr.toArrayList()) {
when (value) {
is ReadableMap -> list.add(asConfig(value)!!)
else -> throw LiquidSdkException.Generic(errUnexpectedType("${value::class.java.name}"))
}
}
return list
}
fun asConnectRequest(connectRequest: ReadableMap): ConnectRequest? {
if (!validateMandatoryFields(
connectRequest,
arrayOf(
"config",
"mnemonic",
),
)
) {
return null
}
val config = connectRequest.getMap("config")?.let { asConfig(it) }!!
val mnemonic = connectRequest.getString("mnemonic")!!
return ConnectRequest(
config,
mnemonic,
)
}
fun readableMapOf(connectRequest: ConnectRequest): ReadableMap {
return readableMapOf(
"config" to readableMapOf(connectRequest.config),
"mnemonic" to connectRequest.mnemonic,
"network" to connectRequest.network.name.lowercase(),
"dataDir" to connectRequest.dataDir,
)
}

View File

@@ -3,6 +3,7 @@ package com.breezliquidsdk
import breez_liquid_sdk.*
import com.facebook.react.bridge.*
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter
import java.io.File
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
@@ -34,12 +35,44 @@ class BreezLiquidSDKModule(reactContext: ReactApplicationContext) : ReactContext
throw LiquidSdkException.Generic("Not initialized")
}
@Throws(LiquidSdkException::class)
private fun ensureWorkingDir(workingDir: String) {
try {
val workingDirFile = File(workingDir)
if (!workingDirFile.exists() && !workingDirFile.mkdirs()) {
throw LiquidSdkException.Generic("Mandatory field workingDir must contain a writable directory")
}
} catch (e: SecurityException) {
throw LiquidSdkException.Generic("Mandatory field workingDir must contain a writable directory")
}
}
@ReactMethod
fun addListener(eventName: String) {}
@ReactMethod
fun removeListeners(count: Int) {}
@ReactMethod
fun defaultConfig(
network: String,
promise: Promise,
) {
executor.execute {
try {
val networkTmp = asNetwork(network)
val res = defaultConfig(networkTmp)
val workingDir = File(reactApplicationContext.filesDir.toString() + "/breezLiquidSdk")
res.workingDir = workingDir.absolutePath
promise.resolve(readableMapOf(res))
} catch (e: Exception) {
promise.reject(e.javaClass.simpleName.replace("Exception", "Error"), e.message, e)
}
}
}
@ReactMethod
fun parseInvoice(
invoice: String,
@@ -86,9 +119,9 @@ class BreezLiquidSDKModule(reactContext: ReactApplicationContext) : ReactContext
asConnectRequest(
req,
) ?: run { throw LiquidSdkException.Generic(errMissingMandatoryField("req", "ConnectRequest")) }
connectRequest.dataDir = connectRequest.dataDir?.takeUnless {
it.isEmpty()
} ?: run { reactApplicationContext.filesDir.toString() + "/breezLiquidSdk" }
ensureWorkingDir(connectRequest.config.workingDir)
bindingLiquidSdk = connect(connectRequest)
promise.resolve(readableMapOf("status" to "ok"))
} catch (e: Exception) {

View File

@@ -8,7 +8,7 @@
import React, { useState } from "react"
import { SafeAreaView, ScrollView, StatusBar, Text, TouchableOpacity, View } from "react-native"
import { addEventListener, Network, getInfo, connect, removeEventListener } from "@breeztech/react-native-breez-liquid-sdk"
import { addEventListener, connect, defaultConfig, getInfo, Network, removeEventListener } from "@breeztech/react-native-breez-liquid-sdk"
import { generateMnemonic } from "@dreson4/react-native-quick-bip39"
import { getSecureItem, setSecureItem } from "./utils/storage"
@@ -49,7 +49,10 @@ const App = () => {
setSecureItem(MNEMONIC_STORE, mnemonic)
}
await connect({ mnemonic, network: Network.LIQUID_TESTNET })
const config = await defaultConfig(Network.TESTNET)
addLine("defaultConfig", JSON.stringify(config))
await connect({ config, mnemonic })
addLine("connect", null)
listenerId = await addEventListener(eventHandler)

View File

@@ -38,35 +38,81 @@ enum BreezLiquidSDKMapper {
return backupRequestList.map { v -> [String: Any?] in dictionaryOf(backupRequest: v) }
}
static func asConnectRequest(connectRequest: [String: Any?]) throws -> ConnectRequest {
guard let mnemonic = connectRequest["mnemonic"] as? String else {
throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "mnemonic", typeName: "ConnectRequest"))
static func asConfig(config: [String: Any?]) throws -> Config {
guard let boltzUrl = config["boltzUrl"] as? String else {
throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "boltzUrl", typeName: "Config"))
}
guard let networkTmp = connectRequest["network"] as? String else {
throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "network", typeName: "ConnectRequest"))
guard let electrumUrl = config["electrumUrl"] as? String else {
throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "electrumUrl", typeName: "Config"))
}
guard let workingDir = config["workingDir"] as? String else {
throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "workingDir", typeName: "Config"))
}
guard let networkTmp = config["network"] as? String else {
throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "network", typeName: "Config"))
}
let network = try asNetwork(network: networkTmp)
var dataDir: String?
if hasNonNilKey(data: connectRequest, key: "dataDir") {
guard let dataDirTmp = connectRequest["dataDir"] as? String else {
throw LiquidSdkError.Generic(message: errUnexpectedValue(fieldName: "dataDir"))
guard let paymentTimeoutSec = config["paymentTimeoutSec"] as? UInt64 else {
throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "paymentTimeoutSec", typeName: "Config"))
}
dataDir = dataDirTmp
return Config(
boltzUrl: boltzUrl,
electrumUrl: electrumUrl,
workingDir: workingDir,
network: network,
paymentTimeoutSec: paymentTimeoutSec
)
}
static func dictionaryOf(config: Config) -> [String: Any?] {
return [
"boltzUrl": config.boltzUrl,
"electrumUrl": config.electrumUrl,
"workingDir": config.workingDir,
"network": valueOf(network: config.network),
"paymentTimeoutSec": config.paymentTimeoutSec,
]
}
static func asConfigList(arr: [Any]) throws -> [Config] {
var list = [Config]()
for value in arr {
if let val = value as? [String: Any?] {
var config = try asConfig(config: val)
list.append(config)
} else {
throw LiquidSdkError.Generic(message: errUnexpectedType(typeName: "Config"))
}
}
return list
}
static func arrayOf(configList: [Config]) -> [Any] {
return configList.map { v -> [String: Any?] in dictionaryOf(config: v) }
}
static func asConnectRequest(connectRequest: [String: Any?]) throws -> ConnectRequest {
guard let configTmp = connectRequest["config"] as? [String: Any?] else {
throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "config", typeName: "ConnectRequest"))
}
let config = try asConfig(config: configTmp)
guard let mnemonic = connectRequest["mnemonic"] as? String else {
throw LiquidSdkError.Generic(message: errMissingMandatoryField(fieldName: "mnemonic", typeName: "ConnectRequest"))
}
return ConnectRequest(
mnemonic: mnemonic,
network: network,
dataDir: dataDir
config: config,
mnemonic: mnemonic
)
}
static func dictionaryOf(connectRequest: ConnectRequest) -> [String: Any?] {
return [
"config": dictionaryOf(config: connectRequest.config),
"mnemonic": connectRequest.mnemonic,
"network": valueOf(network: connectRequest.network),
"dataDir": connectRequest.dataDir == nil ? nil : connectRequest.dataDir,
]
}

View File

@@ -3,6 +3,12 @@
@interface RCT_EXTERN_MODULE(RNBreezLiquidSDK, RCTEventEmitter)
RCT_EXTERN_METHOD(
defaultConfig: (NSString*)network
resolve: (RCTPromiseResolveBlock)resolve
reject: (RCTPromiseRejectBlock)reject
)
RCT_EXTERN_METHOD(
parseInvoice: (NSString*)invoice
resolve: (RCTPromiseResolveBlock)resolve

View File

@@ -11,10 +11,15 @@ class RNBreezLiquidSDK: RCTEventEmitter {
private var bindingLiquidSdk: BindingLiquidSdk!
static var defaultDataDir: URL {
static var breezLiquidSdkDirectory: URL {
let applicationDirectory = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
let breezLiquidSdkDirectory = applicationDirectory.appendingPathComponent("breezLiquidSdk", isDirectory: true)
return applicationDirectory.appendingPathComponent("breezLiquidSdk", isDirectory: true)
if !FileManager.default.fileExists(atPath: breezLiquidSdkDirectory.path) {
try! FileManager.default.createDirectory(atPath: breezLiquidSdkDirectory.path, withIntermediateDirectories: true)
}
return breezLiquidSdkDirectory
}
override init() {
@@ -56,6 +61,28 @@ class RNBreezLiquidSDK: RCTEventEmitter {
throw LiquidSdkError.Generic(message: "Not initialized")
}
private func ensureWorkingDir(workingDir: String) throws {
do {
if !FileManager.default.fileExists(atPath: workingDir) {
try FileManager.default.createDirectory(atPath: workingDir, withIntermediateDirectories: true)
}
} catch {
throw LiquidSdkError.Generic(message: "Mandatory field workingDir must contain a writable directory")
}
}
@objc(defaultConfig:resolve:reject:)
func defaultConfig(_ network: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
do {
let networkTmp = try BreezLiquidSDKMapper.asNetwork(network: network)
var res = BreezLiquidSDK.defaultConfig(network: networkTmp)
res.workingDir = RNBreezLiquidSDK.breezLiquidSdkDirectory.path
resolve(BreezLiquidSDKMapper.dictionaryOf(config: res))
} catch let err {
rejectErr(err: err, reject: reject)
}
}
@objc(parseInvoice:resolve:reject:)
func parseInvoice(_ invoice: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
do {
@@ -85,7 +112,8 @@ class RNBreezLiquidSDK: RCTEventEmitter {
do {
var connectRequest = try BreezLiquidSDKMapper.asConnectRequest(connectRequest: req)
connectRequest.dataDir = connectRequest.dataDir == nil || connectRequest.dataDir!.isEmpty ? RNBreezLiquidSDK.defaultDataDir.path : connectRequest.dataDir
try ensureWorkingDir(workingDir: connectRequest.config.workingDir)
bindingLiquidSdk = try BreezLiquidSDK.connect(req: connectRequest)
resolve(["status": "ok"])
} catch let err {

View File

@@ -1,4 +1,4 @@
import { NativeModules, Platform, NativeEventEmitter } from "react-native"
import { NativeModules, Platform, EmitterSubscription, NativeEventEmitter } from "react-native"
const LINKING_ERROR =
`The package 'react-native-breez-liquid-sdk' doesn't seem to be linked. Make sure: \n\n` +
@@ -23,10 +23,17 @@ export interface BackupRequest {
backupPath?: string
}
export interface ConnectRequest {
mnemonic: string
export interface Config {
boltzUrl: string
electrumUrl: string
workingDir: string
network: Network
dataDir?: string
paymentTimeoutSec: number
}
export interface ConnectRequest {
config: Config
mnemonic: string
}
export interface GetInfoRequest {
@@ -193,6 +200,12 @@ export const setLogger = async (logger: Logger): Promise<EmitterSubscription> =>
return subscription
}
export const defaultConfig = async (network: Network): Promise<Config> => {
const response = await BreezLiquidSDK.defaultConfig(network)
return response
}
export const parseInvoice = async (invoice: string): Promise<LnInvoice> => {
const response = await BreezLiquidSDK.parseInvoice(invoice)
return response