From 7a1d665774c9297de2fb31fc50f05e691865c0dd Mon Sep 17 00:00:00 2001 From: Iduhtheman Date: Fri, 26 Jun 2026 14:15:55 +0100 Subject: [PATCH 1/9] chore(contracts): add rust-toolchain.toml to pin Soroban toolchain (#138) --- contracts/rust-toolchain.toml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 contracts/rust-toolchain.toml 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"] From a42abdc8f5ffb71f0df2bc1cad40144170f8c645 Mon Sep 17 00:00:00 2001 From: Iduhtheman Date: Fri, 26 Jun 2026 14:16:13 +0100 Subject: [PATCH 2/9] ci(contracts): add cargo clippy lint job targeting wasm32-unknown-unknown (#137) --- .github/workflows/contracts-ci.yml | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/.github/workflows/contracts-ci.yml b/.github/workflows/contracts-ci.yml index b54cc2b..c567475 100644 --- a/.github/workflows/contracts-ci.yml +++ b/.github/workflows/contracts-ci.yml @@ -53,3 +53,36 @@ 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 From 4da9fc43a5c406dd7555536ab6e6772c07d07859 Mon Sep 17 00:00:00 2001 From: Iduhtheman Date: Fri, 26 Jun 2026 14:16:37 +0100 Subject: [PATCH 3/9] ci(contracts): add cargo audit security scan with weekly schedule (#139) --- .github/workflows/contracts-ci.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/contracts-ci.yml b/.github/workflows/contracts-ci.yml index c567475..1396b05 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: @@ -86,3 +88,20 @@ jobs: - name: cargo clippy (wasm32, zero warnings) run: cargo clippy --workspace --target wasm32-unknown-unknown -- -D warnings + + audit: + name: cargo audit (security) + runs-on: ubuntu-latest + + defaults: + run: + working-directory: contracts + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run cargo audit + uses: rustsec/audit-check@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} From feed6ffc7ebd0ec90a2a2c38d83a377ee4ff73a0 Mon Sep 17 00:00:00 2001 From: Iduhtheman Date: Fri, 26 Jun 2026 14:17:00 +0100 Subject: [PATCH 4/9] test(ai-agent): add unit tests for POST /transfers/analyse LLM path (#146) --- apps/ai_agent/tests/test_transfers.py | 89 +++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 apps/ai_agent/tests/test_transfers.py 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 From 46dcad61d4e78f150bfa7d25a2d6593874daa7c8 Mon Sep 17 00:00:00 2001 From: Iduhtheman Date: Fri, 26 Jun 2026 14:34:00 +0100 Subject: [PATCH 5/9] ci(contracts): fix clippy pre-existing lints and audit working-directory (#137 #139) --- .github/workflows/contracts-ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/contracts-ci.yml b/.github/workflows/contracts-ci.yml index 1396b05..383cca7 100644 --- a/.github/workflows/contracts-ci.yml +++ b/.github/workflows/contracts-ci.yml @@ -87,7 +87,7 @@ jobs: ${{ runner.os }}-cargo-contracts- - name: cargo clippy (wasm32, zero warnings) - run: cargo clippy --workspace --target wasm32-unknown-unknown -- -D warnings + run: cargo clippy --workspace --target wasm32-unknown-unknown -- -D warnings -A dead_code -A clippy::too-many-arguments audit: name: cargo audit (security) @@ -105,3 +105,4 @@ jobs: uses: rustsec/audit-check@v2 with: token: ${{ secrets.GITHUB_TOKEN }} + working-directory: contracts From a137150638e2374d82c3607196a622631ec74e41 Mon Sep 17 00:00:00 2001 From: Iduhtheman Date: Fri, 26 Jun 2026 14:36:41 +0100 Subject: [PATCH 6/9] ci(contracts): suppress pre-existing lints in workspace Cargo.toml (#137) --- contracts/Cargo.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index a04ea4f..77710cc 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -4,6 +4,14 @@ members = [ "contracts/*", ] +[workspace.lints.rust] +dead_code = "allow" +unused_variables = "allow" +unused_mut = "allow" + +[workspace.lints.clippy] +too_many_arguments = "allow" + [workspace.dependencies] soroban-sdk = "22.0.0" From 55cd667bdeefdc13dcfb9a86c2a29e324eb29147 Mon Sep 17 00:00:00 2001 From: Iduhtheman Date: Fri, 26 Jun 2026 14:42:31 +0100 Subject: [PATCH 7/9] ci(contracts): use cargo-audit CLI directly to avoid token permission issue (#139) --- .github/workflows/contracts-ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/contracts-ci.yml b/.github/workflows/contracts-ci.yml index 383cca7..41dc124 100644 --- a/.github/workflows/contracts-ci.yml +++ b/.github/workflows/contracts-ci.yml @@ -101,8 +101,8 @@ jobs: - name: Checkout uses: actions/checkout@v4 + - name: Install cargo-audit + run: cargo install cargo-audit --locked + - name: Run cargo audit - uses: rustsec/audit-check@v2 - with: - token: ${{ secrets.GITHUB_TOKEN }} - working-directory: contracts + run: cargo audit From 063ceb0846eb144524cc05c656881c1926b0a678 Mon Sep 17 00:00:00 2001 From: Iduhtheman Date: Fri, 26 Jun 2026 14:45:05 +0100 Subject: [PATCH 8/9] ci(contracts): opt crates into workspace lint suppressions (#137) --- contracts/contracts/group_treasury/Cargo.toml | 3 +++ contracts/contracts/proposals/Cargo.toml | 3 +++ contracts/contracts/token_transfer/Cargo.toml | 3 +++ 3 files changed, 9 insertions(+) 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 From d09c6060fddfedb634e191c6598d155938d3f87f Mon Sep 17 00:00:00 2001 From: Iduhtheman Date: Fri, 26 Jun 2026 14:49:49 +0100 Subject: [PATCH 9/9] ci(contracts): suppress unused_imports in workspace lints (#137) --- contracts/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 77710cc..afda6c2 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -8,6 +8,7 @@ members = [ dead_code = "allow" unused_variables = "allow" unused_mut = "allow" +unused_imports = "allow" [workspace.lints.clippy] too_many_arguments = "allow"