Ver Fonte

Nutshell wallet (#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
thesimplekid há 3 semanas atrás
pai
commit
f4c857c3e7

+ 24 - 2
.github/workflows/nutshell_itest.yml

@@ -3,7 +3,8 @@ name: Nutshell integration
 on: [push, pull_request]
 
 jobs:
-  integration-tests:
+  nutshell-integration-tests:
+    name: Nutshell Mint Integration Tests
     runs-on: ubuntu-latest
     steps:
       - name: Pull and start mint
@@ -19,8 +20,29 @@ jobs:
         uses: DeterminateSystems/magic-nix-cache-action@v6
       - name: Rust Cache
         uses: Swatinem/rust-cache@v2
-      - name: Test
+      - name: Test Nutshell
         run: nix develop -i -L .#stable --command just test-nutshell
       - name: Show logs if tests fail
         if: failure()
         run: docker logs nutshell
+
+  nutshell-wallet-integration-tests:
+    name: Nutshell Wallet Integration Tests
+    runs-on: ubuntu-latest
+    steps:
+      - name: checkout
+        uses: actions/checkout@v4
+      - name: Pull Nutshell Docker image
+        run: docker pull cashubtc/nutshell:latest
+      - name: Install Nix
+        uses: DeterminateSystems/nix-installer-action@v11
+      - name: Nix Cache
+        uses: DeterminateSystems/magic-nix-cache-action@v6
+      - name: Rust Cache
+        uses: Swatinem/rust-cache@v2
+      - name: Test Nutshell Wallet
+        run: |
+          nix develop -i -L .#integration --command just nutshell-wallet-itest
+      - name: Show Docker logs if tests fail
+        if: failure()
+        run: docker logs nutshell-wallet || true

+ 251 - 0
crates/cdk-integration-tests/tests/nutshell_wallet.rs

@@ -0,0 +1,251 @@
+use std::time::Duration;
+
+use cdk_fake_wallet::create_fake_invoice;
+use reqwest::Client;
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+use tokio::time::sleep;
+
+/// Response from the invoice creation endpoint
+#[derive(Debug, Serialize, Deserialize)]
+struct InvoiceResponse {
+    payment_request: String,
+    checking_id: Option<String>,
+}
+
+/// Maximum number of attempts to check invoice payment status
+const MAX_PAYMENT_CHECK_ATTEMPTS: u8 = 20;
+/// Delay between payment status checks in milliseconds
+const PAYMENT_CHECK_DELAY_MS: u64 = 500;
+/// Default test amount in satoshis
+const DEFAULT_TEST_AMOUNT: u64 = 10000;
+
+/// Helper function to mint tokens via Lightning invoice
+async fn mint_tokens(base_url: &str, amount: u64) -> String {
+    let client = Client::new();
+
+    // Create an invoice for the specified amount
+    let invoice_url = format!("{}/lightning/create_invoice?amount={}", base_url, amount);
+
+    let invoice_response = client
+        .post(&invoice_url)
+        .send()
+        .await
+        .expect("Failed to send invoice creation request")
+        .json::<InvoiceResponse>()
+        .await
+        .expect("Failed to parse invoice response");
+
+    println!("Created invoice: {}", invoice_response.payment_request);
+
+    invoice_response.payment_request
+}
+
+/// Helper function to wait for payment confirmation
+async fn wait_for_payment_confirmation(base_url: &str, payment_request: &str) {
+    let client = Client::new();
+    let check_url = format!(
+        "{}/lightning/invoice_state?payment_request={}",
+        base_url, payment_request
+    );
+
+    let mut payment_confirmed = false;
+
+    for attempt in 1..=MAX_PAYMENT_CHECK_ATTEMPTS {
+        println!(
+            "Checking invoice state (attempt {}/{})...",
+            attempt, MAX_PAYMENT_CHECK_ATTEMPTS
+        );
+
+        let response = client
+            .get(&check_url)
+            .send()
+            .await
+            .expect("Failed to send payment check request");
+
+        if response.status().is_success() {
+            let state: Value = response
+                .json()
+                .await
+                .expect("Failed to parse payment state response");
+            println!("Payment state: {:?}", state);
+
+            if let Some(result) = state.get("result") {
+                if result == 1 {
+                    payment_confirmed = true;
+                    break;
+                }
+            }
+        } else {
+            println!("Failed to check payment state: {}", response.status());
+        }
+
+        sleep(Duration::from_millis(PAYMENT_CHECK_DELAY_MS)).await;
+    }
+
+    if !payment_confirmed {
+        panic!("Payment not confirmed after maximum attempts");
+    }
+}
+
+/// Helper function to get the current wallet balance
+async fn get_wallet_balance(base_url: &str) -> u64 {
+    let client = Client::new();
+    let balance_url = format!("{}/balance", base_url);
+
+    let balance_response = client
+        .get(&balance_url)
+        .send()
+        .await
+        .expect("Failed to send balance request");
+
+    let balance: Value = balance_response
+        .json()
+        .await
+        .expect("Failed to parse balance response");
+
+    println!("Wallet balance: {:?}", balance);
+
+    balance["balance"]
+        .as_u64()
+        .expect("Could not parse balance as u64")
+}
+
+/// Test the Nutshell wallet's ability to mint tokens from a Lightning invoice
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_nutshell_wallet_mint() {
+    // Get the wallet URL from environment variable
+    let base_url = std::env::var("WALLET_URL").expect("Wallet url is not set");
+
+    // Step 1: Create an invoice and mint tokens
+    let amount = DEFAULT_TEST_AMOUNT;
+    let payment_request = mint_tokens(&base_url, amount).await;
+
+    // Step 2: Wait for the invoice to be paid
+    wait_for_payment_confirmation(&base_url, &payment_request).await;
+
+    // Step 3: Check the wallet balance
+    let available_balance = get_wallet_balance(&base_url).await;
+
+    // Verify the balance is at least the amount we minted
+    assert!(
+        available_balance >= amount,
+        "Balance should be at least {} but was {}",
+        amount,
+        available_balance
+    );
+}
+
+/// Test the Nutshell wallet's ability to mint tokens from a Lightning invoice
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_nutshell_wallet_swap() {
+    // Get the wallet URL from environment variable
+    let base_url = std::env::var("WALLET_URL").expect("Wallet url is not set");
+
+    // Step 1: Create an invoice and mint tokens
+    let amount = DEFAULT_TEST_AMOUNT;
+    let payment_request = mint_tokens(&base_url, amount).await;
+
+    // Step 2: Wait for the invoice to be paid
+    wait_for_payment_confirmation(&base_url, &payment_request).await;
+
+    let send_amount = 100;
+    let send_url = format!("{}/send?amount={}", base_url, send_amount);
+    let client = Client::new();
+
+    let response: Value = client
+        .post(&send_url)
+        .send()
+        .await
+        .expect("Failed to send payment check request")
+        .json()
+        .await
+        .expect("Valid json");
+
+    // Extract the token and remove the surrounding quotes
+    let token_with_quotes = response
+        .get("token")
+        .expect("Missing token")
+        .as_str()
+        .expect("Token is not a string");
+    let token = token_with_quotes.trim_matches('"');
+
+    let receive_url = format!("{}/receive?token={}", base_url, token);
+
+    let response: Value = client
+        .post(&receive_url)
+        .send()
+        .await
+        .expect("Failed to receive request")
+        .json()
+        .await
+        .expect("Valid json");
+
+    let balance = response
+        .get("balance")
+        .expect("Bal in response")
+        .as_u64()
+        .expect("Valid num");
+    let initial_balance = response
+        .get("initial_balance")
+        .expect("Bal in response")
+        .as_u64()
+        .expect("Valid num");
+
+    let token_received = balance - initial_balance;
+
+    assert_eq!(token_received, send_amount);
+}
+
+/// Test the Nutshell wallet's ability to melt tokens to pay a Lightning invoice
+#[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+async fn test_nutshell_wallet_melt() {
+    // Get the wallet URL from environment variable
+    let base_url = std::env::var("WALLET_URL").expect("Wallet url is not set");
+
+    // Step 1: Create an invoice and mint tokens
+    let amount = DEFAULT_TEST_AMOUNT;
+    let payment_request = mint_tokens(&base_url, amount).await;
+
+    // Step 2: Wait for the invoice to be paid
+    wait_for_payment_confirmation(&base_url, &payment_request).await;
+
+    // Get initial balance
+    let initial_balance = get_wallet_balance(&base_url).await;
+    println!("Initial balance: {}", initial_balance);
+
+    // Step 3: Create a fake invoice to pay
+    let payment_amount = 1000; // 1000 sats
+    let fake_invoice = create_fake_invoice(payment_amount, "Test payment".to_string());
+    let pay_url = format!("{}/lightning/pay_invoice?bolt11={}", base_url, fake_invoice);
+    let client = Client::new();
+
+    // Step 4: Pay the invoice
+    let _response: Value = client
+        .post(&pay_url)
+        .send()
+        .await
+        .expect("Failed to send pay request")
+        .json()
+        .await
+        .expect("Failed to parse pay response");
+
+    let final_balance = get_wallet_balance(&base_url).await;
+    println!("Final balance: {}", final_balance);
+
+    assert!(
+        initial_balance > final_balance,
+        "Balance should decrease after payment"
+    );
+
+    let balance_difference = initial_balance - final_balance;
+    println!("Balance decreased by: {}", balance_difference);
+
+    // The balance difference should be at least the payment amount
+    assert!(
+        balance_difference >= (payment_amount / 1000),
+        "Balance should decrease by at least the payment amount ({}) but decreased by {}",
+        payment_amount,
+        balance_difference
+    );
+}

+ 21 - 1
flake.nix

@@ -265,9 +265,29 @@
               inherit nativeBuildInputs;
             } // envVars);
 
+            # Shell with Docker for integration tests
+            integration = pkgs.mkShell ({
+              shellHook = ''
+                ${_shellHook}
+                # Ensure Docker is available
+                if ! command -v docker &> /dev/null; then
+                  echo "Docker is not installed or not in PATH"
+                  echo "Please install Docker to run integration tests"
+                  exit 1
+                fi
+                echo "Docker is available at $(which docker)"
+                echo "Docker version: $(docker --version)"
+              '';
+              buildInputs = buildInputs ++ [ 
+                stable_toolchain
+                pkgs.docker-client
+              ];
+              inherit nativeBuildInputs;
+            } // envVars);
+
           in
           {
-            inherit msrv stable nightly;
+            inherit msrv stable nightly integration;
             default = stable;
           };
       }

+ 4 - 0
justfile

@@ -109,6 +109,10 @@ fake-auth-mint-itest db openid_discovery:
   #!/usr/bin/env bash
   ./misc/fake_auth_itests.sh "{{db}}" "{{openid_discovery}}"
 
+nutshell-wallet-itest:
+  #!/usr/bin/env bash
+  ./misc/nutshell_wallet_itest.sh
+
 run-examples:
   cargo r --example p2pk
   cargo r --example mint-token

+ 161 - 0
misc/nutshell_wallet_itest.sh

@@ -0,0 +1,161 @@
+#!/usr/bin/env bash
+
+set -e
+
+# Configuration
+MINT_PORT=8085
+WALLET_PORT=4448
+MINT_CONTAINER_NAME="nutshell-mint"
+WALLET_CONTAINER_NAME="nutshell-wallet"
+# Use host.docker.internal for the mint URL so containers can access it
+MINT_URL="http://0.0.0.0:${MINT_PORT}"
+WALLET_URL="http://localhost:${WALLET_PORT}"
+CDK_MINTD_PID=""
+
+# Function to clean up resources
+cleanup() {
+  echo "Cleaning up resources..."
+  
+  if docker ps -a | grep -q ${WALLET_CONTAINER_NAME}; then
+    echo "Stopping and removing Docker container '${WALLET_CONTAINER_NAME}'..."
+    docker stop ${WALLET_CONTAINER_NAME} >/dev/null 2>&1
+    docker rm ${WALLET_CONTAINER_NAME} >/dev/null 2>&1
+  fi
+  
+  if [ -n "$CDK_MINTD_PID" ]; then
+    echo "Stopping mintd process (PID: $CDK_MINTD_PID)..."
+    kill -TERM $CDK_MINTD_PID >/dev/null 2>&1 || true
+  fi
+  
+  # Unset variables
+  unset MINT_URL WALLET_URL MINT_PORT WALLET_PORT MINT_CONTAINER_NAME WALLET_CONTAINER_NAME
+  unset CDK_MINTD_PID CDK_MINTD_URL CDK_MINTD_WORK_DIR CDK_MINTD_LISTEN_HOST CDK_MINTD_LISTEN_PORT
+  unset CDK_MINTD_LN_BACKEND CDK_MINTD_FAKE_WALLET_SUPPORTED_UNITS CDK_MINTD_MNEMONIC
+  unset CDK_MINTD_FAKE_WALLET_FEE_PERCENT CDK_MINTD_FAKE_WALLET_RESERVE_FEE_MIN CDK_MINTD_DATABASE
+  unset TEST_STATUS
+  echo "Cleanup complete."
+}
+
+# Set up trap to call cleanup function on script exit
+trap cleanup EXIT INT TERM
+
+
+
+# Create a temporary directory for mintd
+CDK_ITESTS=$(mktemp -d)
+echo "Created temporary directory for mintd: $CDK_ITESTS"
+
+export CDK_MINTD_URL="$MINT_URL"
+export CDK_MINTD_WORK_DIR="$CDK_ITESTS"
+export CDK_MINTD_LISTEN_HOST="127.0.0.1"
+export CDK_MINTD_LISTEN_PORT="8085"
+export CDK_MINTD_LN_BACKEND="fakewallet"
+export CDK_MINTD_FAKE_WALLET_SUPPORTED_UNITS="sat,usd"
+export CDK_MINTD_MNEMONIC="eye survey guilt napkin crystal cup whisper salt luggage manage unveil loyal"
+export CDK_MINTD_FAKE_WALLET_FEE_PERCENT="0"
+export CDK_MINTD_FAKE_WALLET_RESERVE_FEE_MIN="1"
+export CDK_MINTD_DATABASE="redb"
+
+
+echo "Starting fake mintd"
+cargo run --bin cdk-mintd --features "redb" &
+CDK_MINTD_PID=$!
+
+# Wait for the mint to be ready
+echo "Waiting for mintd to start..."
+TIMEOUT=300
+START_TIME=$(date +%s)
+
+# Try different URLs since the endpoint might vary
+URLS=("http://localhost:${MINT_PORT}/v1/info" "http://127.0.0.1:${MINT_PORT}/v1/info" "http://0.0.0.0:${MINT_PORT}/v1/info")
+
+# Loop until one of the endpoints returns a 200 OK status or timeout is reached
+while true; do
+    # Get the current time
+    CURRENT_TIME=$(date +%s)
+    
+    # Calculate the elapsed time
+    ELAPSED_TIME=$((CURRENT_TIME - START_TIME))
+
+    # Check if the elapsed time exceeds the timeout
+    if [ $ELAPSED_TIME -ge $TIMEOUT ]; then
+        echo "Timeout of $TIMEOUT seconds reached. Exiting..."
+        exit 1
+    fi
+
+    # Try each URL
+    for URL in "${URLS[@]}"; do
+        # Make a request to the endpoint and capture the HTTP status code
+        HTTP_STATUS=$(curl -o /dev/null -s -w "%{http_code}" "$URL" || echo "000")
+        
+        # Check if the HTTP status is 200 OK
+        if [ "$HTTP_STATUS" -eq 200 ]; then
+            echo "Received 200 OK from $URL"
+            MINT_URL=$(echo "$URL" | sed 's|/v1/info||')
+            echo "Setting MINT_URL to $MINT_URL"
+            export MINT_URL
+            break 2  # Break out of both loops
+        else
+            echo "Waiting for 200 OK response from $URL, current status: $HTTP_STATUS"
+        fi
+    done
+    
+    # Wait before retrying
+    sleep 5
+done
+
+
+
+
+# Check if Docker is available and accessible
+if docker info > /dev/null 2>&1; then
+  echo "Docker is available, starting Nutshell wallet container"
+  # Use the MINT_URL which is already set to host.docker.internal
+  docker run -d --name ${WALLET_CONTAINER_NAME} \
+    --network=host \
+    -p ${WALLET_PORT}:${WALLET_PORT} \
+    -e MINT_URL=${MINT_URL} \
+    cashubtc/nutshell:latest \
+    poetry run cashu -d
+else
+  echo "Docker is not accessible, skipping Nutshell wallet container setup"
+  # Set a flag to indicate we're not using Docker
+  export NO_DOCKER=true
+fi
+
+# Wait for the mint to be ready
+echo "Waiting for Nutshell Mint to start..."
+sleep 5
+
+# Check if the Mint API is responding (use localhost for local curl check)
+echo "Checking if Nutshell Mint API is available..."
+if curl -s "http://localhost:${MINT_PORT}/v1/info" > /dev/null; then
+  echo "Nutshell Mint is running and accessible at ${MINT_URL}"
+else
+  echo "Warning: Nutshell Mint API is not responding. It might not be ready yet."
+fi
+
+# Only check wallet if Docker is available
+if [ -z "$NO_DOCKER" ]; then
+  # Check if the Wallet API is responding
+  echo "Checking if Nutshell Wallet API is available..."
+  if curl -s "${WALLET_URL}/info" > /dev/null; then
+    echo "Nutshell Wallet is running in container '${WALLET_CONTAINER_NAME}'"
+    echo "You can access it at ${WALLET_URL}"
+  else
+    echo "Warning: Nutshell Wallet API is not responding. The container might not be ready yet."
+  fi
+fi
+
+# Export URLs as environment variables
+export MINT_URL=${MINT_URL}
+export WALLET_URL=${WALLET_URL}
+
+# Run the integration test
+echo "Running integration test..."
+cargo test -p cdk-integration-tests --tests nutshell_wallet
+TEST_STATUS=$?
+
+# Exit with the test status
+echo "Integration test completed with status: $TEST_STATUS"
+exit $TEST_STATUS