diff --git a/.github/workflows/contracts-ci.yml b/.github/workflows/contracts-ci.yml index b54cc2b..41dc124 100644 --- a/.github/workflows/contracts-ci.yml +++ b/.github/workflows/contracts-ci.yml @@ -17,6 +17,8 @@ on: paths: - 'contracts/**' - '.github/workflows/contracts-ci.yml' + schedule: + - cron: '0 8 * * 1' # Every Monday at 08:00 UTC jobs: test-and-build: @@ -53,3 +55,54 @@ jobs: - name: cargo build (release wasm32) run: cargo build -p token_transfer --target wasm32-unknown-unknown --release + + clippy: + name: cargo clippy (wasm32) + runs-on: ubuntu-latest + + defaults: + run: + working-directory: contracts + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + components: clippy + + - name: Cache cargo registry and build artifacts + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry/index + ~/.cargo/registry/cache + ~/.cargo/git/db + contracts/target + key: ${{ runner.os }}-cargo-contracts-${{ hashFiles('contracts/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-contracts- + + - name: cargo clippy (wasm32, zero warnings) + run: cargo clippy --workspace --target wasm32-unknown-unknown -- -D warnings -A dead_code -A clippy::too-many-arguments + + audit: + name: cargo audit (security) + runs-on: ubuntu-latest + + defaults: + run: + working-directory: contracts + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install cargo-audit + run: cargo install cargo-audit --locked + + - name: Run cargo audit + run: cargo audit diff --git a/apps/ai_agent/tests/test_transfers.py b/apps/ai_agent/tests/test_transfers.py new file mode 100644 index 0000000..e3216ec --- /dev/null +++ b/apps/ai_agent/tests/test_transfers.py @@ -0,0 +1,89 @@ +from fastapi.testclient import TestClient +from unittest.mock import MagicMock, patch +import json +import pytest + +from main import app + +client = TestClient(app) + + +# Helper to build a fake OpenAI response +def _fake_openai_response(payload: dict): + msg = MagicMock() + msg.content = json.dumps(payload) + choice = MagicMock() + choice.message = msg + resp = MagicMock() + resp.choices = [choice] + return resp + + +def test_llm_path_flagged_transfer(): + with patch("main._openai_client") as mock_client_fn: + mock_client = MagicMock() + mock_client_fn.return_value = mock_client + mock_client.chat.completions.create.return_value = _fake_openai_response( + {"flagged": True, "reason": "Suspicious memo", "confidence": 0.9} + ) + response = client.post("/transfers/analyse", json={ + "amount": 100.0, "sender": "GABC", "recipient": "GDEF", "memo": "test" + }) + assert response.status_code == 200 + data = response.json() + assert data["flagged"] is True + assert data["confidence"] == 0.9 + + +def test_llm_path_clean_transfer(): + with patch("main._openai_client") as mock_client_fn: + mock_client = MagicMock() + mock_client_fn.return_value = mock_client + mock_client.chat.completions.create.return_value = _fake_openai_response( + {"flagged": False, "reason": None, "confidence": 0.1} + ) + response = client.post("/transfers/analyse", json={ + "amount": 500.0, "sender": "GABC", "recipient": "GDEF", "memo": "payment" + }) + assert response.status_code == 200 + data = response.json() + assert data["flagged"] is False + assert isinstance(data["confidence"], float) + + +def test_llm_path_missing_confidence_defaults_to_zero(): + with patch("main._openai_client") as mock_client_fn: + mock_client = MagicMock() + mock_client_fn.return_value = mock_client + mock_client.chat.completions.create.return_value = _fake_openai_response( + {"flagged": False, "reason": None} + ) + response = client.post("/transfers/analyse", json={ + "amount": 200.0, "sender": "GABC", "recipient": "GDEF", "memo": "normal" + }) + assert response.status_code == 200 + data = response.json() + assert data["confidence"] == 0.0 + + +def test_llm_path_missing_flagged_defaults_to_false(): + with patch("main._openai_client") as mock_client_fn: + mock_client = MagicMock() + mock_client_fn.return_value = mock_client + mock_client.chat.completions.create.return_value = _fake_openai_response( + {"reason": None, "confidence": 0.5} + ) + response = client.post("/transfers/analyse", json={ + "amount": 300.0, "sender": "GABC", "recipient": "GDEF", "memo": "salary" + }) + assert response.status_code == 200 + data = response.json() + assert data["flagged"] is False + + +def test_llm_path_missing_api_key_returns_500(monkeypatch): + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + response = client.post("/transfers/analyse", json={ + "amount": 100.0, "sender": "GABC", "recipient": "GDEF", "memo": "test" + }) + assert response.status_code == 500 diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index a04ea4f..afda6c2 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -4,6 +4,15 @@ members = [ "contracts/*", ] +[workspace.lints.rust] +dead_code = "allow" +unused_variables = "allow" +unused_mut = "allow" +unused_imports = "allow" + +[workspace.lints.clippy] +too_many_arguments = "allow" + [workspace.dependencies] soroban-sdk = "22.0.0" diff --git a/contracts/contracts/group_treasury/Cargo.toml b/contracts/contracts/group_treasury/Cargo.toml index 3402b88..e5c7a3c 100644 --- a/contracts/contracts/group_treasury/Cargo.toml +++ b/contracts/contracts/group_treasury/Cargo.toml @@ -13,3 +13,6 @@ soroban-sdk = { workspace = true } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } + +[lints] +workspace = true diff --git a/contracts/contracts/proposals/Cargo.toml b/contracts/contracts/proposals/Cargo.toml index 9b92bc4..ea5e632 100644 --- a/contracts/contracts/proposals/Cargo.toml +++ b/contracts/contracts/proposals/Cargo.toml @@ -13,3 +13,6 @@ soroban-sdk = { workspace = true } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } + +[lints] +workspace = true diff --git a/contracts/contracts/token_transfer/Cargo.toml b/contracts/contracts/token_transfer/Cargo.toml index d1410a4..f274875 100644 --- a/contracts/contracts/token_transfer/Cargo.toml +++ b/contracts/contracts/token_transfer/Cargo.toml @@ -13,3 +13,6 @@ soroban-sdk = { workspace = true } [dev-dependencies] soroban-sdk = { workspace = true, features = ["testutils"] } + +[lints] +workspace = true diff --git a/contracts/rust-toolchain.toml b/contracts/rust-toolchain.toml new file mode 100644 index 0000000..c0d3e4d --- /dev/null +++ b/contracts/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "stable" +targets = ["wasm32-unknown-unknown"] +components = ["clippy", "rustfmt"]