Skip to content

Commit f4c857c

Browse files
authored
Nutshell wallet (cashubtc#695)
* chore: Add nutshell wallet integration test script feat: Add MINT_URL configuration for docker container in nutshell wallet integration test script chore: Make nutshell_wallet_itest.sh executable fix: Update MINT_URL to use host.docker.internal for Docker container access feat: Add Docker container startup for Cashu daemon in wallet integration test script chore: Update Docker image to use Docker Hub repository feat: Add cleanup trap to stop Docker container and unset variables feat: Add initial test stub for nutshell wallet mint functionality test: Add Cashu wallet mint integration test feat: Add just command for nutshell wallet integration test refactor: Organize imports and improve code formatting in nutshell wallet test fix: Update Cashu Docker image and test URL for correct container access fix: Update Docker container name and image for Cashu wallet test script fix: Handle Docker container name conflict in nutshell wallet integration test fix: Update Docker image to cashubtc/nutshell:latest in wallet integration test script feat: Add support for running separate Nutshell mint and wallet containers feat: Update Nutshell mint and wallet container configurations for integration testing fix: Update wallet port and container configuration in integration test script chore: Export MINT_URL and WALLET_URL as environment variables fix: Update invoice creation and state checking in nutshell wallet test fix: Update MINT_URL to use host.docker.internal for container access fix: Update nutshell wallet mint test to handle invoice payment state refactor: Improve Nutshell wallet test with better error handling and robustness refactor: Improve code formatting and logging in nutshell wallet test refactor: Replace anyhow with expect for error handling in Nutshell wallet test refactor: Simplify error handling in Nutshell wallet mint test refactor: Replace `?` with `expect()` in Nutshell wallet test refactor: Simplify nutshell wallet test by removing unused code and improving error handling refactor: Extract minting and balance helper functions in nutshell wallet test feat: Add test for Nutshell wallet token swap functionality fix: Trim quotes from token in nutshell wallet swap test refactor: Remove debug print statements and improve code readability refactor: Improve test_nutshell_wallet_melt with payment state checking and balance verification refactor: Update Nutshell wallet integration test script configuration feat: Extract common mint startup function into shared script refactor: Simplify nutshell wallet integration test script and improve startup process feat: Add mintd process termination and test status capture in integration test script refactor: Capitalize env vars and ensure comprehensive cleanup in trap feat: nutshell wallet test * ci: Add test step for Nutshell wallet integration tests * ci: Split Nutshell integration tests into separate jobs * feat: nutshell wallet test * ci: Add Docker setup and increase timeout for Nutshell wallet integration tests * chore: Improve Nutshell wallet integration test script robustness * fix: Remove -i flag from Nix develop and improve Docker accessibility check * fix: payment processor * fix: wallet tests * feat: Add integration shell with Docker and Rust stable for testing * ci: Simplify Nutshell wallet integration test workflow and script * fix: Improve mintd endpoint detection and error handling in integration test script * fix: wallet tests
1 parent 0eb5805 commit f4c857c

File tree

5 files changed

+461
-3
lines changed

5 files changed

+461
-3
lines changed

.github/workflows/nutshell_itest.yml

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ name: Nutshell integration
33
on: [push, pull_request]
44

55
jobs:
6-
integration-tests:
6+
nutshell-integration-tests:
7+
name: Nutshell Mint Integration Tests
78
runs-on: ubuntu-latest
89
steps:
910
- name: Pull and start mint
@@ -19,8 +20,29 @@ jobs:
1920
uses: DeterminateSystems/magic-nix-cache-action@v6
2021
- name: Rust Cache
2122
uses: Swatinem/rust-cache@v2
22-
- name: Test
23+
- name: Test Nutshell
2324
run: nix develop -i -L .#stable --command just test-nutshell
2425
- name: Show logs if tests fail
2526
if: failure()
2627
run: docker logs nutshell
28+
29+
nutshell-wallet-integration-tests:
30+
name: Nutshell Wallet Integration Tests
31+
runs-on: ubuntu-latest
32+
steps:
33+
- name: checkout
34+
uses: actions/checkout@v4
35+
- name: Pull Nutshell Docker image
36+
run: docker pull cashubtc/nutshell:latest
37+
- name: Install Nix
38+
uses: DeterminateSystems/nix-installer-action@v11
39+
- name: Nix Cache
40+
uses: DeterminateSystems/magic-nix-cache-action@v6
41+
- name: Rust Cache
42+
uses: Swatinem/rust-cache@v2
43+
- name: Test Nutshell Wallet
44+
run: |
45+
nix develop -i -L .#integration --command just nutshell-wallet-itest
46+
- name: Show Docker logs if tests fail
47+
if: failure()
48+
run: docker logs nutshell-wallet || true
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
use std::time::Duration;
2+
3+
use cdk_fake_wallet::create_fake_invoice;
4+
use reqwest::Client;
5+
use serde::{Deserialize, Serialize};
6+
use serde_json::Value;
7+
use tokio::time::sleep;
8+
9+
/// Response from the invoice creation endpoint
10+
#[derive(Debug, Serialize, Deserialize)]
11+
struct InvoiceResponse {
12+
payment_request: String,
13+
checking_id: Option<String>,
14+
}
15+
16+
/// Maximum number of attempts to check invoice payment status
17+
const MAX_PAYMENT_CHECK_ATTEMPTS: u8 = 20;
18+
/// Delay between payment status checks in milliseconds
19+
const PAYMENT_CHECK_DELAY_MS: u64 = 500;
20+
/// Default test amount in satoshis
21+
const DEFAULT_TEST_AMOUNT: u64 = 10000;
22+
23+
/// Helper function to mint tokens via Lightning invoice
24+
async fn mint_tokens(base_url: &str, amount: u64) -> String {
25+
let client = Client::new();
26+
27+
// Create an invoice for the specified amount
28+
let invoice_url = format!("{}/lightning/create_invoice?amount={}", base_url, amount);
29+
30+
let invoice_response = client
31+
.post(&invoice_url)
32+
.send()
33+
.await
34+
.expect("Failed to send invoice creation request")
35+
.json::<InvoiceResponse>()
36+
.await
37+
.expect("Failed to parse invoice response");
38+
39+
println!("Created invoice: {}", invoice_response.payment_request);
40+
41+
invoice_response.payment_request
42+
}
43+
44+
/// Helper function to wait for payment confirmation
45+
async fn wait_for_payment_confirmation(base_url: &str, payment_request: &str) {
46+
let client = Client::new();
47+
let check_url = format!(
48+
"{}/lightning/invoice_state?payment_request={}",
49+
base_url, payment_request
50+
);
51+
52+
let mut payment_confirmed = false;
53+
54+
for attempt in 1..=MAX_PAYMENT_CHECK_ATTEMPTS {
55+
println!(
56+
"Checking invoice state (attempt {}/{})...",
57+
attempt, MAX_PAYMENT_CHECK_ATTEMPTS
58+
);
59+
60+
let response = client
61+
.get(&check_url)
62+
.send()
63+
.await
64+
.expect("Failed to send payment check request");
65+
66+
if response.status().is_success() {
67+
let state: Value = response
68+
.json()
69+
.await
70+
.expect("Failed to parse payment state response");
71+
println!("Payment state: {:?}", state);
72+
73+
if let Some(result) = state.get("result") {
74+
if result == 1 {
75+
payment_confirmed = true;
76+
break;
77+
}
78+
}
79+
} else {
80+
println!("Failed to check payment state: {}", response.status());
81+
}
82+
83+
sleep(Duration::from_millis(PAYMENT_CHECK_DELAY_MS)).await;
84+
}
85+
86+
if !payment_confirmed {
87+
panic!("Payment not confirmed after maximum attempts");
88+
}
89+
}
90+
91+
/// Helper function to get the current wallet balance
92+
async fn get_wallet_balance(base_url: &str) -> u64 {
93+
let client = Client::new();
94+
let balance_url = format!("{}/balance", base_url);
95+
96+
let balance_response = client
97+
.get(&balance_url)
98+
.send()
99+
.await
100+
.expect("Failed to send balance request");
101+
102+
let balance: Value = balance_response
103+
.json()
104+
.await
105+
.expect("Failed to parse balance response");
106+
107+
println!("Wallet balance: {:?}", balance);
108+
109+
balance["balance"]
110+
.as_u64()
111+
.expect("Could not parse balance as u64")
112+
}
113+
114+
/// Test the Nutshell wallet's ability to mint tokens from a Lightning invoice
115+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
116+
async fn test_nutshell_wallet_mint() {
117+
// Get the wallet URL from environment variable
118+
let base_url = std::env::var("WALLET_URL").expect("Wallet url is not set");
119+
120+
// Step 1: Create an invoice and mint tokens
121+
let amount = DEFAULT_TEST_AMOUNT;
122+
let payment_request = mint_tokens(&base_url, amount).await;
123+
124+
// Step 2: Wait for the invoice to be paid
125+
wait_for_payment_confirmation(&base_url, &payment_request).await;
126+
127+
// Step 3: Check the wallet balance
128+
let available_balance = get_wallet_balance(&base_url).await;
129+
130+
// Verify the balance is at least the amount we minted
131+
assert!(
132+
available_balance >= amount,
133+
"Balance should be at least {} but was {}",
134+
amount,
135+
available_balance
136+
);
137+
}
138+
139+
/// Test the Nutshell wallet's ability to mint tokens from a Lightning invoice
140+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
141+
async fn test_nutshell_wallet_swap() {
142+
// Get the wallet URL from environment variable
143+
let base_url = std::env::var("WALLET_URL").expect("Wallet url is not set");
144+
145+
// Step 1: Create an invoice and mint tokens
146+
let amount = DEFAULT_TEST_AMOUNT;
147+
let payment_request = mint_tokens(&base_url, amount).await;
148+
149+
// Step 2: Wait for the invoice to be paid
150+
wait_for_payment_confirmation(&base_url, &payment_request).await;
151+
152+
let send_amount = 100;
153+
let send_url = format!("{}/send?amount={}", base_url, send_amount);
154+
let client = Client::new();
155+
156+
let response: Value = client
157+
.post(&send_url)
158+
.send()
159+
.await
160+
.expect("Failed to send payment check request")
161+
.json()
162+
.await
163+
.expect("Valid json");
164+
165+
// Extract the token and remove the surrounding quotes
166+
let token_with_quotes = response
167+
.get("token")
168+
.expect("Missing token")
169+
.as_str()
170+
.expect("Token is not a string");
171+
let token = token_with_quotes.trim_matches('"');
172+
173+
let receive_url = format!("{}/receive?token={}", base_url, token);
174+
175+
let response: Value = client
176+
.post(&receive_url)
177+
.send()
178+
.await
179+
.expect("Failed to receive request")
180+
.json()
181+
.await
182+
.expect("Valid json");
183+
184+
let balance = response
185+
.get("balance")
186+
.expect("Bal in response")
187+
.as_u64()
188+
.expect("Valid num");
189+
let initial_balance = response
190+
.get("initial_balance")
191+
.expect("Bal in response")
192+
.as_u64()
193+
.expect("Valid num");
194+
195+
let token_received = balance - initial_balance;
196+
197+
assert_eq!(token_received, send_amount);
198+
}
199+
200+
/// Test the Nutshell wallet's ability to melt tokens to pay a Lightning invoice
201+
#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
202+
async fn test_nutshell_wallet_melt() {
203+
// Get the wallet URL from environment variable
204+
let base_url = std::env::var("WALLET_URL").expect("Wallet url is not set");
205+
206+
// Step 1: Create an invoice and mint tokens
207+
let amount = DEFAULT_TEST_AMOUNT;
208+
let payment_request = mint_tokens(&base_url, amount).await;
209+
210+
// Step 2: Wait for the invoice to be paid
211+
wait_for_payment_confirmation(&base_url, &payment_request).await;
212+
213+
// Get initial balance
214+
let initial_balance = get_wallet_balance(&base_url).await;
215+
println!("Initial balance: {}", initial_balance);
216+
217+
// Step 3: Create a fake invoice to pay
218+
let payment_amount = 1000; // 1000 sats
219+
let fake_invoice = create_fake_invoice(payment_amount, "Test payment".to_string());
220+
let pay_url = format!("{}/lightning/pay_invoice?bolt11={}", base_url, fake_invoice);
221+
let client = Client::new();
222+
223+
// Step 4: Pay the invoice
224+
let _response: Value = client
225+
.post(&pay_url)
226+
.send()
227+
.await
228+
.expect("Failed to send pay request")
229+
.json()
230+
.await
231+
.expect("Failed to parse pay response");
232+
233+
let final_balance = get_wallet_balance(&base_url).await;
234+
println!("Final balance: {}", final_balance);
235+
236+
assert!(
237+
initial_balance > final_balance,
238+
"Balance should decrease after payment"
239+
);
240+
241+
let balance_difference = initial_balance - final_balance;
242+
println!("Balance decreased by: {}", balance_difference);
243+
244+
// The balance difference should be at least the payment amount
245+
assert!(
246+
balance_difference >= (payment_amount / 1000),
247+
"Balance should decrease by at least the payment amount ({}) but decreased by {}",
248+
payment_amount,
249+
balance_difference
250+
);
251+
}

flake.nix

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,9 +265,29 @@
265265
inherit nativeBuildInputs;
266266
} // envVars);
267267

268+
# Shell with Docker for integration tests
269+
integration = pkgs.mkShell ({
270+
shellHook = ''
271+
${_shellHook}
272+
# Ensure Docker is available
273+
if ! command -v docker &> /dev/null; then
274+
echo "Docker is not installed or not in PATH"
275+
echo "Please install Docker to run integration tests"
276+
exit 1
277+
fi
278+
echo "Docker is available at $(which docker)"
279+
echo "Docker version: $(docker --version)"
280+
'';
281+
buildInputs = buildInputs ++ [
282+
stable_toolchain
283+
pkgs.docker-client
284+
];
285+
inherit nativeBuildInputs;
286+
} // envVars);
287+
268288
in
269289
{
270-
inherit msrv stable nightly;
290+
inherit msrv stable nightly integration;
271291
default = stable;
272292
};
273293
}

justfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ fake-auth-mint-itest db openid_discovery:
109109
#!/usr/bin/env bash
110110
./misc/fake_auth_itests.sh "{{db}}" "{{openid_discovery}}"
111111

112+
nutshell-wallet-itest:
113+
#!/usr/bin/env bash
114+
./misc/nutshell_wallet_itest.sh
115+
112116
run-examples:
113117
cargo r --example p2pk
114118
cargo r --example mint-token

0 commit comments

Comments
 (0)