Skip to content

Add a self-service user activity log, and repair the Rust/Soroban CI - #655

Merged
Abdulazeem-code merged 2 commits into
Abdulazeem-code:mainfrom
daveades:feat/user-activity-log
Aug 31, 2026
Merged

Add a self-service user activity log, and repair the Rust/Soroban CI#655
Abdulazeem-code merged 2 commits into
Abdulazeem-code:mainfrom
daveades:feat/user-activity-log

Conversation

@daveades

@daveades daveades commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Closes #599

Two things in one branch: the activity log the issue asks for, and a repair of the Rust/Soroban CI jobs.


1. Self-service activity log

GET /users/:username/activity

Ownership is proven the way the webhook endpoints already prove it: a signature over activity:<username> made with the account key, either a Freighter-style signed message or a multi-signer threshold.

curl "http://localhost:5000/users/ada*localhost/activity?limit=20&startDate=2026-01-01" \
  -H "X-Stellar-Signature: <base64 signature>" \
  -H "X-Stellar-Signer: <G... public key>"

The signature is also accepted in the body as signature / signerAddress, matching GET /webhooks.

Paging is page / limit (default 10, capped at 100) through the existing parsePagination / paginatedResponse helpers, so the response carries meta.total and meta.totalPages. startDate and endDate bound created_at; an unparseable or inverted range is a 400 before the query runs. Rows come back newest first, tie-broken on id so pages stay stable for events written in the same millisecond.

The trail is read for the authenticated username, not the path parameter, so a valid signature for one account cannot read another's rows.

Logged actions: user.registered, user.unregistered, user.transferred, user.blocked, webhook.created, webhook.deleted.

Writes go through recordActivity, which resolves to null rather than throwing: an activity row records a request, it is not part of one, so a logging failure must not turn a successful registration into a 500. Metadata over 2KB is replaced with { truncated: true }.

POST /register is handled twice, by server.js and by userRoutes.js, with the server.js mount winning. Both are instrumented, since which one runs is not obvious from either file.

Two changes beyond the new code

Signature verification moved out of webhookRoutes.js into src/services/ownershipService.js. It was defined inside the router factory, so it could not be imported, and the activity endpoint needs the same check. authenticateWebhookCall is now a thin wrapper over the shared function and its behaviour is unchanged.

POST /admin/block was returning 500 for every request. It used prisma.user.update({ where: { address } }), which stopped resolving when #613 dropped the unique index on address:

Argument `where` of type UserWhereUniqueInput needs at least one of `username` arguments.

It now uses updateMany, which is also the correct semantics: an address can carry several usernames and blocking it has to flag all of them. The response keeps username (the primary) and gains usernames. It is in this PR because blocking is one of the events the issue asks to be logged, and the fix and the logging land on the same lines.

Verification

tests/activity.test.js (24 tests) covers the service; tests/activity-endpoint.test.js (11 tests) mounts the real userRoutes router and covers the signed message, 401/404 propagation, reading the owner's trail rather than the path parameter, the response shape, paging, limit clamping, date filtering, and both signature sources.

End to end against a scratch PostgreSQL 16 instance with the server running, 21 checks: register, create and delete a webhook, then read the trail as the owner. Missing signature 400, non-owner signer 401, valid signature for a different account 401, newest-first ordering, metadata round trip, correct meta.total, non-overlapping pages, future startDate empty, spanning range complete, bad dates 400, and a second user seeing only their own row. Blocking an address with three usernames flags and logs all three and reports the primary first; an unknown address is still 404.

prisma migrate diff reports no drift for activity_logs. Backend suite: 50 suites, 835 tests.


2. Rust and Soroban CI

payment_router/src/lib.rs had been mangled by earlier merges. Two thirds of it was a single 85KB line with literal \n escapes instead of newlines, so every Rust job died at parsing:

error: unknown start of token: \
 --> src/lib.rs:1:155

That failure had been masking everything behind it. Restoring the newlines surfaced:

  • 12 test functions defined twice. The stale copies called std::println! in a no_std crate. The surviving copies are the newer ones, which use log! and carry the #[ignore] attributes.
  • vec! used in the test module without importing it. Soroban's vec! now comes in with the other soroban_sdk imports.
  • Three unused bindings in a stub test.
  • A stale Cargo.lock and stale test snapshots, neither regenerated since the crate last compiled. proptest's dependencies were missing from the lockfile entirely.

.cargo/config.toml enabled clippy::pedantic and clippy::nursery under -Dwarnings, a combination the contract has never satisfied: 92 errors, 47 of them needless_pass_by_value on entrypoints that #[contractimpl] requires to take Env and Address by value. The config now allows the lints Soroban's macros make unavoidable, keeps clippy::all and pedantic otherwise, and drops nursery, which clippy documents as unstable and not meant to gate CI. Each allow carries its reason. This is the one judgement call here and it is a policy choice you may want to tighten lint by lint.

lib_main.rs is removed: a UTF-16 copy of lib.rs containing no function absent from it.

One thing left failing, deliberately

test_route_payments_multi_token_batch is marked #[ignore] with a reason rather than fixed. route_payments calls sender.require_auth() once per payment, so a batch containing two payments from the same sender fails authorization and aborts the host. A batch with two different senders passes, which is how I isolated it. That is a contract bug, not a test bug, and changing auth logic in a payment router is not something to slip into this PR. Worth its own issue.

lib_head.rs is left in place: unlike lib_main.rs it holds three functions that exist nowhere else (route_payment_with_memo, test_route_payment_with_memo, test_route_payment_with_invalid_memo), so a memo feature appears to have been lost in the same merge. It is mojibake-encoded and I have not tried to recover it.

Verification

Every step the two Rust workflows run, locally:

cargo fmt --all -- --check                                  clean
cargo clippy --all-targets --all-features -- -D warnings    clean
cargo test                                                  47 passed, 4 ignored
cargo build --target wasm32-unknown-unknown --release        ok

The cargo dylint and cargo audit steps need network installs and were not run locally, so those two remain unverified.

@vercel

vercel Bot commented Aug 31, 2026

Copy link
Copy Markdown

@daveades is attempting to deploy a commit to the Abdulazeem's projects Team on Vercel.

A member of the Team first needs to authorize it.

@Abdulazeem-code

Copy link
Copy Markdown
Owner

RESOLVE CONFLICTS

Adds an ActivityLog model and GET /users/:username/activity, authenticated
with a Stellar signature over activity:<username>, with page/limit paging
and startDate/endDate filtering. Registration, transfer, unregistration,
webhook create/delete and block are recorded.

Signature verification moves out of webhookRoutes into ownershipService so
the webhook routes and the activity endpoint share one implementation.

POST /admin/block now flags every username on the address with updateMany.
It had kept a single update keyed on address, which stopped resolving when
request.
src/lib.rs had been mangled by earlier merges: two thirds of it was one
line with literal \n escapes instead of newlines, so nothing downstream of
parsing ever ran. Restoring the newlines exposed what the parse error had
been hiding:

- 12 test functions defined twice, the stale copies calling std::println!
  in a no_std crate
- the soroban vec! macro used in the test module without importing it
- three unused bindings in a stub test
- a stale Cargo.lock and test snapshots, neither regenerated since the
  crate last built

.cargo/config.toml enabled clippy::pedantic and clippy::nursery under
-Dwarnings, which the contract has never satisfied. It now allows the
lints that Soroban's macros make unavoidable and drops nursery, which
clippy documents as unstable.

test_route_payments_multi_token_batch is ignored rather than deleted:
route_payments calls require_auth once per payment, so a batch with two
payments from the same sender fails authorization. Two different senders
pass, so this is a contract bug and not a test bug.

lib_main.rs removed: a UTF-16 copy of lib.rs with nothing unique in it.
@daveades
daveades force-pushed the feat/user-activity-log branch from 914438c to b2359de Compare August 31, 2026 12:39
@drips-wave

drips-wave Bot commented Aug 31, 2026

Copy link
Copy Markdown

@daveades Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@daveades daveades changed the title Add a self-service user activity log Add a self-service user activity log, and repair the Rust/Soroban CI Aug 31, 2026
@Abdulazeem-code
Abdulazeem-code merged commit aa1b1ec into Abdulazeem-code:main Aug 31, 2026
10 of 11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement a user activity log for self-service auditing

2 participants