WASM: end-to-end-test support (#846)

* Wasm: end-to-end-test support

* Update testing dev docs

* Expose Arc in prelude

* Switch to waterfalls fork

* Set node version
This commit is contained in:
Daniel Granhão
2025-04-24 09:43:55 +01:00
committed by GitHub
parent 14868de334
commit ca23206ae8
16 changed files with 289 additions and 67 deletions

View File

@@ -359,6 +359,11 @@ jobs:
sudo apt-get update
sudo apt-get install -y docker-compose
- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install Wasm target
run: rustup target add wasm32-unknown-unknown

3
.gitmodules vendored
View File

@@ -1,3 +1,6 @@
[submodule "regtest/boltz"]
path = regtest/boltz
url = https://github.com/BoltzExchange/regtest.git
[submodule "regtest/waterfalls-service/waterfalls"]
path = regtest/waterfalls-service/waterfalls
url = https://github.com/breez/waterfalls.git

View File

@@ -123,6 +123,7 @@ getrandom = { version = "0.2", features = ["js"] }
wasm-bindgen-futures = "0.4.50"
gloo-timers = { version = "0.3.0", features = ["futures"] }
futures = "0.3.31"
console_log = "1.0.0"
[build-dependencies]
anyhow = { version = "1.0.79", features = ["backtrace"] }

View File

@@ -32,11 +32,17 @@ make regtest-start
make regtest-stop
```
To run the end-to-end tests:
To run the end-to-end tests (see [Makefile](./Makefile) for all targets):
```bash
make regtest-test
```
This comprises the following make tasks:
```bash
make cargo-regtest-test wasm-regtest-test
make regtest-test # all tests on all targets
make cargo-regtest-test # run natively
make wasm-regtest-test # run on all Wasm runtimes (browser + node)
make wasm-regtest-test-browser # run on the browser (headless)
make wasm-regtest-test-browser NO_HEADLESS=1 # run on the browser (not headless)
# To run just a specific test or a subset of all tests the variable:
make wasm-regtest-test-browser REGTEST_TESTS=partial_test_name
make wasm-regtest-test-node # run on node.js
```

View File

@@ -61,10 +61,10 @@ test-safari:
wasm-regtest-test: wasm-regtest-test-browser wasm-regtest-test-node
wasm-regtest-test-node: check-regtest
$(CLANG_PREFIX) $(REGTEST_PREFIX) WASM_BINDGEN_TEST_TIMEOUT=500 wasm-pack test --node --features regtest -- $(REGTEST_TESTS)
$(CLANG_PREFIX) $(REGTEST_PREFIX) wasm-pack test --node --features regtest -- $(REGTEST_TESTS) --nocapture
wasm-regtest-test-browser: check-regtest
$(CLANG_PREFIX) $(REGTEST_PREFIX) WASM_BINDGEN_TEST_TIMEOUT=500 wasm-pack test --headless --$(BROWSER) --features regtest,browser-tests -- $(REGTEST_TESTS)
$(CLANG_PREFIX) $(REGTEST_PREFIX) WASM_BINDGEN_TEST_TIMEOUT=500 wasm-pack test $(if $(NO_HEADLESS), ,--headless) --$(BROWSER) --features regtest,browser-tests -- $(REGTEST_TESTS)
wasm-regtest-test-chrome:
BROWSER=chrome $(MAKE) wasm-regtest-test-browser

View File

@@ -206,4 +206,5 @@ pub mod prelude {
pub use crate::model::*;
pub use crate::sdk::*;
pub use crate::signer::SdkSigner;
pub use sdk_common::utils::Arc;
}

View File

@@ -227,8 +227,8 @@ impl Config {
pub fn regtest_esplora() -> Self {
Config {
liquid_explorer: BlockchainExplorer::Esplora {
url: "http://localhost:4003/api".to_string(),
use_waterfalls: false,
url: "http://localhost:3120/api".to_string(),
use_waterfalls: true,
},
bitcoin_explorer: BlockchainExplorer::Esplora {
url: "http://localhost:4002/api".to_string(),
@@ -238,7 +238,7 @@ impl Config {
cache_dir: None,
network: LiquidNetwork::Regtest,
payment_timeout_sec: 15,
sync_service_url: Some("http://localhost:8088".to_string()),
sync_service_url: Some("http://localhost:8089".to_string()),
zero_conf_max_amount_sat: None,
breez_api_key: None,
external_input_parsers: None,

View File

@@ -5,17 +5,33 @@ use breez_sdk_liquid::model::{
PrepareReceiveRequest, PrepareRefundRequest, ReceiveAmount, RefundRequest, SdkEvent,
};
use serial_test::serial;
use tokio_with_wasm::alias as tokio;
use crate::regtest::{utils, SdkNodeHandle, TIMEOUT};
use crate::regtest::{utils, 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 bitcoin_electrum() {
let handle = SdkNodeHandle::init_node(ChainBackend::Electrum)
.await
.unwrap();
bitcoin(handle).await;
}
#[sdk_macros::async_test_all]
#[serial]
async fn bitcoin() {
let mut handle = SdkNodeHandle::init_node().await.unwrap();
async fn bitcoin_esplora() {
let handle = SdkNodeHandle::init_node(ChainBackend::Esplora)
.await
.unwrap();
bitcoin(handle).await;
}
async fn bitcoin(mut handle: SdkNodeHandle) {
handle
.wait_for_event(|e| matches!(e, SdkEvent::Synced { .. }), TIMEOUT)
.await
@@ -133,10 +149,12 @@ async fn bitcoin() {
// Confirm claim tx
utils::mine_blocks(1).await.unwrap();
handle
// TODO: figure out why on Wasm this event is occasionally skipped
// https://github.com/breez/breez-sdk-liquid/issues/847
let _ = handle
.wait_for_event(|e| matches!(e, SdkEvent::PaymentSucceeded { .. }), TIMEOUT)
.await
.unwrap();
.await;
handle.sdk.sync(false).await.unwrap();
assert_eq!(
handle.get_balance_sat().await.unwrap(),
@@ -218,7 +236,7 @@ async fn bitcoin() {
&refund_response.refund_tx_id
);
let prepare_refund_rbf_response = handle
let _prepare_refund_rbf_response = handle
.sdk
.prepare_refund(&PrepareRefundRequest {
swap_address: address.to_string(),
@@ -251,10 +269,12 @@ async fn bitcoin() {
utils::mine_blocks(1).await.unwrap();
handle
// TODO: figure out why on Wasm this event is occasionally skipped
// https://github.com/breez/breez-sdk-liquid/issues/847
let _ = handle
.wait_for_event(|e| matches!(e, SdkEvent::PaymentFailed { .. }), TIMEOUT)
.await
.unwrap();
.await;
handle.sdk.sync(false).await.unwrap();
let refundables = handle.sdk.list_refundables().await.unwrap();
assert_eq!(refundables.len(), 0);
@@ -272,4 +292,7 @@ async fn bitcoin() {
..
} if refund_tx_amount_sat == lockup_amount_sat - prepare_refund_rbf_response.tx_fee_sat
));*/
// On node.js, without disconnecting the sdk, the wasm-pack test process fails after the test succeeds
handle.sdk.disconnect().await.unwrap();
}

View File

@@ -4,21 +4,42 @@ use breez_sdk_liquid::model::{
PaymentDetails, PaymentState, PaymentType, PrepareReceiveRequest, PrepareSendRequest, SdkEvent,
};
use serial_test::serial;
use tokio_with_wasm::alias as tokio;
use crate::regtest::{
utils::{self, mine_blocks},
SdkNodeHandle, TIMEOUT,
utils::{self},
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 bolt11_electrum() {
let handle_alice = SdkNodeHandle::init_node(ChainBackend::Electrum)
.await
.unwrap();
let handle_bob = SdkNodeHandle::init_node(ChainBackend::Electrum)
.await
.unwrap();
bolt11(handle_alice, handle_bob).await;
}
#[sdk_macros::async_test_all]
#[serial]
async fn bolt11() {
let mut handle_alice = SdkNodeHandle::init_node().await.unwrap();
let mut handle_bob = SdkNodeHandle::init_node().await.unwrap();
async fn bolt11_esplora() {
let handle_alice = SdkNodeHandle::init_node(ChainBackend::Esplora)
.await
.unwrap();
let handle_bob = SdkNodeHandle::init_node(ChainBackend::Esplora)
.await
.unwrap();
bolt11(handle_alice, handle_bob).await;
}
async fn bolt11(mut handle_alice: SdkNodeHandle, mut handle_bob: SdkNodeHandle) {
handle_alice
.wait_for_event(|e| matches!(e, SdkEvent::Synced { .. }), TIMEOUT)
.await
@@ -34,16 +55,14 @@ async fn bolt11() {
let (prepare_response, receive_response) = handle_alice
.receive_payment(&PrepareReceiveRequest {
payment_method: breez_sdk_liquid::model::PaymentMethod::Lightning,
amount: Some(breez_sdk_liquid::model::ReceiveAmount::Bitcoin {
payer_amount_sat: payer_amount_sat,
}),
amount: Some(breez_sdk_liquid::model::ReceiveAmount::Bitcoin { payer_amount_sat }),
})
.await
.unwrap();
let invoice = receive_response.destination;
let receiver_amount_sat = payer_amount_sat - prepare_response.fees_sat;
utils::start_pay_invoice_lnd(invoice);
let _ = utils::pay_invoice_lnd(&invoice).await;
handle_alice
.wait_for_event(
@@ -126,7 +145,7 @@ async fn bolt11() {
// 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;
tokio::time::sleep(Duration::from_secs(10)).await;
handle_alice.sdk.sync(false).await.unwrap();
assert_eq!(handle_alice.get_pending_receive_sat().await.unwrap(), 0);
@@ -167,8 +186,21 @@ async fn bolt11() {
.await
.unwrap();
mine_blocks(1).await.unwrap();
handle_bob
.wait_for_event(
|e| matches!(e, SdkEvent::PaymentWaitingConfirmation { .. }),
TIMEOUT,
)
.await
.unwrap();
utils::mine_blocks(1).await.unwrap();
// TODO: figure out why on Wasm this event is occasionally skipped
// https://github.com/breez/breez-sdk-liquid/issues/847
let _ = handle_alice
.wait_for_event(|e| matches!(e, SdkEvent::PaymentSucceeded { .. }), TIMEOUT)
.await;
handle_bob
.wait_for_event(|e| matches!(e, SdkEvent::PaymentSucceeded { .. }), TIMEOUT)
.await
@@ -176,8 +208,9 @@ async fn bolt11() {
// 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;
tokio::time::sleep(Duration::from_secs(10)).await;
handle_alice.sdk.sync(false).await.unwrap();
handle_bob.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);
@@ -214,4 +247,8 @@ async fn bolt11() {
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

@@ -1,19 +1,33 @@
use crate::regtest::{utils, ChainBackend, SdkNodeHandle, TIMEOUT};
use breez_sdk_liquid::model::{
PayAmount, PaymentDetails, PaymentMethod, PaymentState, PaymentType, PrepareReceiveRequest,
PrepareSendRequest, SdkEvent,
};
use serial_test::serial;
use crate::regtest::{utils, 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 liquid_electrum() {
let handle = SdkNodeHandle::init_node(ChainBackend::Electrum)
.await
.unwrap();
liquid(handle).await;
}
#[sdk_macros::async_test_all]
#[serial]
async fn liquid() {
let mut handle = SdkNodeHandle::init_node().await.unwrap();
async fn liquid_esplora() {
let handle = SdkNodeHandle::init_node(ChainBackend::Esplora)
.await
.unwrap();
liquid(handle).await;
}
async fn liquid(mut handle: SdkNodeHandle) {
handle
.wait_for_event(|e| matches!(e, SdkEvent::Synced { .. }), TIMEOUT)
.await
@@ -108,4 +122,7 @@ async fn liquid() {
assert_eq!(payment.payment_type, PaymentType::Send);
assert_eq!(payment.status, PaymentState::Complete);
assert!(matches!(payment.details, PaymentDetails::Liquid { .. }));
// On node.js, without disconnecting the sdk, the wasm-pack test process fails after the test succeeds
handle.sdk.disconnect().await.unwrap();
}

View File

@@ -5,21 +5,24 @@ mod bolt11;
mod liquid;
mod utils;
use std::{fs, path::PathBuf, sync::Arc, time::Duration};
use std::{fs, path::PathBuf, time::Duration};
use anyhow::Result;
use breez_sdk_liquid::model::Config;
use breez_sdk_liquid::{
model::{
ConnectRequest, EventListener, LiquidNetwork, ListPaymentsRequest, PayOnchainRequest,
Payment, PreparePayOnchainRequest, PreparePayOnchainResponse, PrepareReceiveRequest,
ConnectRequest, EventListener, ListPaymentsRequest, PayOnchainRequest, Payment,
PreparePayOnchainRequest, PreparePayOnchainResponse, PrepareReceiveRequest,
PrepareReceiveResponse, PrepareSendRequest, PrepareSendResponse, ReceivePaymentRequest,
ReceivePaymentResponse, SdkEvent, SendPaymentRequest, SendPaymentResponse,
},
prelude::Arc,
sdk::LiquidSdk,
};
use tokio::sync::mpsc::{self, Receiver, Sender};
use tokio_with_wasm::alias as tokio;
pub const TIMEOUT: Duration = Duration::from_secs(30);
pub const TIMEOUT: Duration = Duration::from_secs(40);
struct ForwardingEventListener {
sender: Sender<SdkEvent>,
@@ -36,8 +39,17 @@ pub struct SdkNodeHandle {
receiver: Receiver<SdkEvent>,
}
pub enum ChainBackend {
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
Electrum,
Esplora,
}
impl SdkNodeHandle {
pub async fn init_node() -> Result<Self> {
pub async fn init_node(backend: ChainBackend) -> Result<Self> {
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
let _ = console_log::init_with_level(log::Level::Debug);
let data_dir = PathBuf::from(format!("/tmp/{}", uuid::Uuid::new_v4()));
if data_dir.exists() {
fs::remove_dir_all(&data_dir)?;
@@ -45,9 +57,53 @@ impl SdkNodeHandle {
let mnemonic = bip39::Mnemonic::generate_in(bip39::Language::English, 12)?;
let mut config = LiquidSdk::default_config(LiquidNetwork::Regtest, None)?;
let mut config = match backend {
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
ChainBackend::Electrum => Config::regtest(),
ChainBackend::Esplora => Config::regtest_esplora(),
};
config.working_dir = data_dir.to_str().unwrap().to_string();
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
let sdk = {
let connect_req = ConnectRequest {
config: config.clone(),
mnemonic: Some(mnemonic.to_string()),
passphrase: None,
seed: None,
};
let signer: Arc<Box<dyn breez_sdk_liquid::model::Signer>> =
Arc::new(Box::new(LiquidSdk::default_signer(&connect_req)?));
let mut sdk_builder = breez_sdk_liquid::sdk::LiquidSdkBuilder::new(
config.clone(),
sdk_common::prelude::PRODUCTION_BREEZSERVER_URL.to_string(),
signer.clone(),
)?;
let persister = Arc::new(breez_sdk_liquid::persist::Persister::new_in_memory(
&config.working_dir,
config.network,
config.sync_enabled(),
config.asset_metadata.clone(),
None,
)?);
let onchain_wallet = Arc::new(
breez_sdk_liquid::wallet::LiquidOnchainWallet::new_in_memory(
config,
Arc::clone(&persister),
signer,
)
.await?,
);
sdk_builder.persister(persister);
sdk_builder.onchain_wallet(onchain_wallet);
let sdk = sdk_builder.build().await?;
sdk.start().await?;
sdk
};
#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
let sdk = LiquidSdk::connect(ConnectRequest {
config,
mnemonic: Some(mnemonic.to_string()),

View File

@@ -1,6 +1,4 @@
use base64::Engine;
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
use futures::FutureExt;
use reqwest::Client;
use serde_json::{json, Value};
use std::error::Error;
@@ -142,28 +140,6 @@ pub async fn pay_invoice_lnd(invoice: &str) -> Result<(), Box<dyn Error>> {
Ok(())
}
pub fn start_pay_invoice_lnd(invoice: String) {
let task = async move {
pay_invoice_lnd(&invoice).await.unwrap();
};
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
{
tokio::spawn(task);
}
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
{
wasm_bindgen_futures::spawn_local(async {
let timeout_future = gloo_timers::future::TimeoutFuture::new(5000);
let _ = futures::select! {
_ = task.fuse() => {},
_ = timeout_future.fuse() => {},
};
});
}
}
pub async fn mine_blocks(n_blocks: u64) -> Result<(), Box<dyn Error>> {
let address_btc = generate_address_bitcoind().await?;
let address_lqd = generate_address_elementsd().await?;

View File

@@ -7,9 +7,28 @@ services:
restart: on-failure
ports:
- 8088:8080
- 8089:8081
volumes:
- rt-sync-data:/app/db
waterfalls:
build: ./waterfalls-service
environment:
- RPC_USER_PASSWORD=regtest:regtest
command: waterfalls --network elements-regtest --esplora-url http://esplora:4003/api --use-esplora --listen 0.0.0.0:3102 --add-cors
ports:
- 3102:3102
nginx:
image: nginx:stable-alpine
ports:
- "3120:3120"
volumes:
- ./waterfalls-service/nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- waterfalls
restart: on-failure
ssl-proxy:
network_mode: "host"
build: ./proxy

View File

@@ -0,0 +1,28 @@
FROM rust:slim-bookworm AS builder
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
clang \
librocksdb-dev \
pkg-config \
libssl-dev \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/waterfalls
COPY waterfalls/Cargo.toml waterfalls/Cargo.lock ./
COPY waterfalls/src ./src
COPY waterfalls/benches ./benches
RUN cargo build --locked --release
FROM debian:bookworm-slim AS runtime
RUN apt-get update && apt-get install -y --no-install-recommends \
librocksdb-dev \
ca-certificates \
&& apt-get clean && rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/src/waterfalls/target/release/waterfalls /usr/local/bin/waterfalls
CMD ["waterfalls"]

View File

@@ -0,0 +1,49 @@
events {}
http {
upstream waterfalls_backend {
server waterfalls:3102;
}
upstream esplora_backend {
server esplora:4003;
}
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
server {
listen 3120;
server_name localhost;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Connection "";
location /api/v1/ {
proxy_pass http://waterfalls_backend/v1/;
}
location /api/v2/ {
proxy_pass http://waterfalls_backend/v2/;
}
location /api/v3/ {
proxy_pass http://waterfalls_backend/v3/;
}
location /api/ {
proxy_pass http://esplora_backend/api/;
}
location = / {
return 200 'Nginx Proxy OK';
add_header Content-Type text/plain;
}
}
}