Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
066f110
feat: add approval threshold to GroupTreasuryContract initialization …
samieazubike Jun 25, 2026
454f443
Implement execute_withdraw
Jun 25, 2026
e9f9c95
Add health endpoint tests for ai_agent
watifee Jun 25, 2026
8e726db
feat: implement voting mechanism for withdraw proposals with approval…
samieazubike Jun 25, 2026
5dcc46a
feat(backend): add user_devices device identity schema
Olorunfemi20 Jun 25, 2026
2fb681b
feat(backend): add GET /devices to list caller's own devices
Olorunfemi20 Jun 25, 2026
7350aac
feat: strengthen device authentication, linking, session security, an…
devchant Jun 25, 2026
0129c20
Add Soroban test-snapshot hygiene check to contracts CI
DeveloperEmmy Jun 26, 2026
b77c859
Merge pull request #201 from samieazubike/issue-121
codebestia Jun 26, 2026
c57c16f
Merge pull request #204 from watifee/unit-tests-for-GET/health
codebestia Jun 26, 2026
e3e1fe6
Merge pull request #205 from samieazubike/issue-123
codebestia Jun 26, 2026
8301711
Merge pull request #206 from Olorunfemi20/feat/user-devices-schema
codebestia Jun 26, 2026
a79f260
Merge pull request #207 from Olorunfemi20/feat/devices-list-endpoint
codebestia Jun 26, 2026
99b5e87
Merge branch 'main' into new_branch
codebestia Jun 26, 2026
b2ef084
Merge pull request #208 from devchant/new_branch
codebestia Jun 26, 2026
29b1981
Remove unused file
Jun 26, 2026
a9b57a0
Merge pull request #202 from eischideraa-unn/blackboxai/execute_withdraw
codebestia Jun 26, 2026
7a1d665
chore(contracts): add rust-toolchain.toml to pin Soroban toolchain (#…
iduhtheman Jun 26, 2026
a42abdc
ci(contracts): add cargo clippy lint job targeting wasm32-unknown-unk…
iduhtheman Jun 26, 2026
4da9fc4
ci(contracts): add cargo audit security scan with weekly schedule (#139)
iduhtheman Jun 26, 2026
feed6ff
test(ai-agent): add unit tests for POST /transfers/analyse LLM path (…
iduhtheman Jun 26, 2026
46dcad6
ci(contracts): fix clippy pre-existing lints and audit working-direct…
iduhtheman Jun 26, 2026
a137150
ci(contracts): suppress pre-existing lints in workspace Cargo.toml (#…
iduhtheman Jun 26, 2026
55cd667
ci(contracts): use cargo-audit CLI directly to avoid token permission…
iduhtheman Jun 26, 2026
063ceb0
ci(contracts): opt crates into workspace lint suppressions (#137)
iduhtheman Jun 26, 2026
d09c606
ci(contracts): suppress unused_imports in workspace lints (#137)
iduhtheman Jun 26, 2026
c235136
Merge pull request #241 from iduhtheman/fix/clicked-iduhtheman-batch
codebestia Jun 26, 2026
8735920
Merge branch 'main' into main
DeveloperEmmy Jun 26, 2026
ebfb9c8
ci(contracts): add tests and builds for group_treasury and proposals
DeveloperEmmy Jul 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 115 additions & 1 deletion .github/workflows/contracts-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -51,5 +53,117 @@ jobs:
- name: cargo test -p token_transfer
run: cargo test -p token_transfer

- name: cargo build (release wasm32)
- name: cargo test -p group_treasury
run: cargo test -p group_treasury

- name: cargo test -p proposals
run: cargo test -p proposals

- name: Soroban test-snapshot hygiene check (fail on host-error snapshots)

shell: bash
env:
# Add allowlisted snapshot filenames/paths here, relative to repo root.
# Example:
# ALLOWLIST: "contracts/token_transfer/test_snapshots/legit.json,contracts/group_treasury/test_snapshots/another.json"
ALLOWLIST: ""
run: |
set -euo pipefail
echo "Scanning for test_snapshots JSON files..."

# Find any JSON files under contracts/**/test_snapshots/
mapfile -t found < <(find . -type f -path '*/test_snapshots/*.json' -print)

if [ "${#found[@]}" -eq 0 ]; then
echo "No test snapshots produced."
exit 0
fi

# Normalize allowlist into an array and filter.
IFS=',' read -r -a allowlist <<< "${ALLOWLIST}"
filtered=()

for f in "${found[@]}"; do
# Convert to repo-root-relative path for comparison.
rel="${f#./}"

allowed=false
for a in "${allowlist[@]}"; do
if [ -n "$a" ] && [ "$rel" = "$a" ]; then
allowed=true
break
fi
done
if [ "$allowed" = false ]; then
filtered+=("$rel")
fi
done

if [ "${#filtered[@]}" -ne 0 ]; then
echo "ERROR: Found test snapshot files that are not allowlisted (host errors suspected):"
printf ' - %s\n' "${filtered[@]}"
exit 1
fi

echo "Only allowlisted test snapshots were produced."

- name: cargo build (release wasm32) - token_transfer
run: cargo build -p token_transfer --target wasm32-unknown-unknown --release

- name: cargo build (release wasm32) - group_treasury
run: cargo build -p group_treasury --target wasm32-unknown-unknown --release

- name: cargo build (release wasm32) - proposals
run: cargo build -p proposals --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
9 changes: 9 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
- [x] Add contracts/test_snapshots/ to contracts/.gitignore (already present)
- [x] Update .github/workflows/contracts-ci.yml to add a hygiene check after cargo test (fail if any contracts/**/test_snapshots/*.json exists)


- [x] Remove any existing test snapshot files from the repo (if present) (none found)
- [x] Run contracts cargo tests locally to verify no snapshots are produced (not run due to missing cargo in environment)



9 changes: 9 additions & 0 deletions apps/ai_agent/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,12 @@ dependencies = [
"openai>=1.0.0",
"weaviate-client>=4.0.0",
]

[dependency-groups]
dev = [
"pytest>=8.0.0",
"httpx>=0.27.0",
]

[tool.pytest.ini_options]
pythonpath = ["."]
Empty file added apps/ai_agent/tests/__init__.py
Empty file.
22 changes: 22 additions & 0 deletions apps/ai_agent/tests/test_health.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from fastapi.testclient import TestClient

from main import app

client = TestClient(app)


def test_health_returns_200():
response = client.get("/health")
assert response.status_code == 200


def test_health_response_body():
response = client.get("/health")
assert response.json() == {"status": "ok"}


def test_health_works_without_api_key(monkeypatch):
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
89 changes: 89 additions & 0 deletions apps/ai_agent/tests/test_transfers.py
Original file line number Diff line number Diff line change
@@ -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
17 changes: 17 additions & 0 deletions apps/backend/drizzle/0007_user_devices.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
CREATE TYPE "public"."device_platform" AS ENUM('web', 'ios', 'android');--> statement-breakpoint
CREATE TABLE "user_devices" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"device_id" text NOT NULL,
"device_name" text NOT NULL,
"platform" "device_platform" NOT NULL,
"identity_public_key" text NOT NULL,
"registration_id" integer,
"last_seen_at" timestamp,
"revoked_at" timestamp,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "user_devices" ADD CONSTRAINT "user_devices_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE UNIQUE INDEX "user_devices_user_id_device_id_unique" ON "user_devices" USING btree ("user_id","device_id");--> statement-breakpoint
CREATE INDEX "user_devices_user_id_active_idx" ON "user_devices" USING btree ("user_id") WHERE "revoked_at" IS NULL;
7 changes: 7 additions & 0 deletions apps/backend/drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@
"when": 1780560000000,
"tag": "0006_add_conversation_avatar_url",
"breakpoints": true
},
{
"idx": 7,
"version": "7",
"when": 1782345600000,
"tag": "0007_user_devices",
"breakpoints": true
}
]
}
Loading