Skip to content

Feat/nullius improvements - #15

Merged
IyanuOluwaJesuloba merged 24 commits into
mainfrom
feat/nullius-improvements
Jul 18, 2026
Merged

Feat/nullius improvements#15
IyanuOluwaJesuloba merged 24 commits into
mainfrom
feat/nullius-improvements

Conversation

@IyanuOluwaJesuloba

Copy link
Copy Markdown
Collaborator

Addresses logic bugs, missing test coverage, and developer-experience gaps across the full stack.

What changed:

Circuit — avg_balance was committed via Poseidon but never used in the score. Now incorporated with a cap; threshold scaling updated from 600→700 across circuit, SDK, and frontend preview.
Contracts — byte-length validation in submit_proof before the cross-contract call; 5 new submit_proof integration tests (Bronze/Silver/Gold/upgrade/downgrade) using an AlwaysTrueVerifier mock; 4 new payment_gate tests covering quote and send guards.
SDK — encodeG1/G2/encodeScalar exported (no more frontend duplicates); buildSubmitProofTransaction added so components don't hand-roll transaction construction; getLimit added; exponential backoff in waitForConfirmation; runtime deps pinned to exact versions.
Frontend — Silver tier color fixed; COEP header added (enables multi-threaded WASM for snarkjs); ErrorBoundary wired to optional remote reporting; TIER_COLORS centralised in SDK; proof history clear button; PaymentWidget reads VITE_NATIVE_TOKEN/VITE_FEE_COLLECTOR from env; accessibility improvements in ProofGenerator.
CI/scripts — WASM target corrected to wasm32v1-none; feat/fix/chore branches now trigger CI; WASM size check job and security audit job added; deploy.js gets WASM size validation and transient RPC retry; e2e_test.js degrades gracefully without SECRET_KEY.
Docs/config — MIT LICENSE added; .env.example covers all VITE_* vars; .gitignore fixed so .env.example is tracked; README score formula table; CHANGELOG updated.
Tested: cargo test --all passes. Frontend builds clean. No breaking API changes.

avg_balance was hashed into the Poseidon commitment but was never used in
score_proxy, meaning balance had zero impact on the score. This was both a
logic bug and a misleading privacy claim (hiding a value that does nothing).

Changes:
- Add bal_capped = min(avg_balance, 10000) signal in the circuit
- Add bal_capped to score_proxy (contributes up to 10000 units)
- Rescale threshold multiplier from 600 to 700 to match new max proxy
- Mirror updated formula in sdk/src/prover.ts selectThreshold()
- Mirror updated formula in ProofGenerator.tsx live score preview
…istory

Silver was using #6b7280 (grey-500) which is almost identical to Unverified
(#64748b). Changed to #94a3b8 (slate-400) which reads as a metallic silver
and is visually distinct from both Unverified and Bronze. Updated both
ReputationCard.tsx and ProofHistory.tsx to stay consistent.
…mit_proof

The verifier contract expects proof_a: BytesN<64>, proof_b: BytesN<128>,
proof_c: BytesN<64>, commitment: BytesN<32>. Without upfront validation, a
wrong-length Bytes argument would cause an opaque panic deep inside the host
function after burning cross-contract call gas.

Added explicit length checks with descriptive panic messages before the
cross-contract call, and added three unit tests covering the new guards.
…nerator

encodeG1, encodeG2, encodeScalar were private functions in contracts.ts.
ProofGenerator.tsx duplicated all three under different names (encodeG1Bytes,
encodeG2Bytes, encodeScalarBytes) and also had a dead encodeBytes closure
that was defined but never called.

- Export encodeG1/encodeG2/encodeScalar from sdk/src/contracts.ts with JSDoc
- Import them in ProofGenerator.tsx instead of maintaining local copies
- Remove the dead encodeBytes closure
- Encoding logic is now maintained in exactly one place
Added a dedicated method for Freighter-based proof submission that returns
unsigned XDR. ProofGenerator.tsx previously built the full transaction inline
(duplicating SDK logic). It now delegates to this method, reducing the
component to: build -> sign -> send.

- NulliusClient.buildSubmitProofTransaction(walletAddress, bundle) -> XDR string
- ProofGenerator.tsx simplified: removes inline tx construction and duplicate imports
- Encoding, address validation, and account fetch are now all inside the SDK
…waitForConfirmation

The old implementation polled every 1500ms for up to 20 attempts (30s total).
Under testnet load the fixed cadence could timeout before a slow block, and
it also hammers the RPC endpoint unnecessarily early.

New behaviour:
- Starts at 1s delay, doubles each poll, capped at 8s per interval
- Total budget controlled by maxWaitMs (default 30s), not attempt count
- Timeout message now includes elapsed duration for easier debugging
Previously the contract had no tests exercising its core send/quote paths.
Added a mock_registry stub (always returns Silver tier=2) and four new tests:

- quote_returns_correct_fee_and_net_for_silver: verifies 1% fee on 100 XLM
- quote_returns_correct_fee_for_unverified: verifies 5% fee on 100 XLM
- send_rejects_zero_amount: panics with 'Amount must be positive'
- send_rejects_amount_over_tier_limit: panics with 'Amount exceeds tier limit'
  for a Silver wallet sending 1 stroop over the 100,000 XLM cap
The native token address and fee collector were hardcoded in PaymentWidget.tsx.
A real deployment needs a proper treasury address for fee collection, and the
token address differs between testnet and mainnet.

- .env.example: added VITE_ prefixed vars for all three contract IDs, native
  token address, fee collector, and the e2e SECRET_KEY hint
- PaymentWidget.tsx: reads VITE_NATIVE_TOKEN and VITE_FEE_COLLECTOR at runtime,
  falling back to testnet defaults so the demo still works without .env
- .gitignore: narrowed .env.* exclusion to .env.local and .env.*.local so
  .env.example (which contains no secrets) is tracked in version control
- Add MIT LICENSE file (README referenced MIT but no file existed)
- Fix placeholder 'your-repo/nullius' GitHub link in App.tsx footer
- Update README deployed contracts table: replace TBD with reference to
  sdk/src/contract_ids.json which is written by scripts/deploy.js
- Add score formula breakdown table documenting all four circuit components
  including avg_balance (now actually used after previous fix)
…sign export

Two cleanups:

1. sdk/src/prover.ts: added JSDoc block on generateReputationProof explaining
   the snarkjs output ordering vs on-chain input ordering mismatch.
   snarkjs emits [meets_threshold, threshold, commitment] (output first),
   but the registry expects [threshold, commitment, meets_threshold].
   The ProofBundle struct stores values by name so the encoding in contracts.ts
   is always correct — but the asymmetry was an undocumented footgun.

2. frontend/src/hooks/useFreighter.ts: removed the unused 'sign' method and
   its signTransaction import. Components call signTransaction directly with
   the correct networkPassphrase per call site. Added a comment explaining why.
…ting

The componentDidCatch handler previously only logged to console with a TODO
comment. Implemented a proper reportError() function:

- Always logs to console (DevTools visibility in dev)
- When VITE_ERROR_ENDPOINT is set, sends structured JSON via navigator.sendBeacon
  (non-blocking, survives page unload, compatible with Sentry ingest URLs)
- Payload includes: message, stack, componentStack, URL, timestamp
- Swallows all reporting errors so the reporter can never crash the app
- Added VITE_ERROR_ENDPOINT to .env.example with documentation
…le builds

Open ranges (^12.0.0, ^0.7.0, ^0.1.7) mean any minor/patch bump can silently
change proof generation or encoding behaviour — dangerous for a cryptographic
library. Pinned to the exact versions currently installed:

  @stellar/stellar-sdk  ^12.0.0  ->  12.3.0
  snarkjs               ^0.7.0   ->  0.7.6
  circomlibjs           ^0.1.7   ->  0.1.7 (unchanged)
…sion

Added full [Unreleased] entries covering:
- Circuit: avg_balance now used in score computation
- Contracts: byte-length validation in registry, payment_gate quote/send tests
- SDK: buildSubmitProofTransaction, exported encoding helpers, pinned deps,
  exponential backoff in waitForConfirmation, public signal ordering docs
- Frontend: COEP header, Silver tier color, ErrorBoundary remote reporting,
  useFreighter dead code removal, PaymentWidget env var config
- Docs: LICENSE file, README score formula table, placeholder link fix
- Config: .env.example VITE_ vars, .gitignore .env.example fix
…loy.js

Two robustness improvements:

1. WASM size check: after resolving the WASM path, stat the file and exit
   early if it is smaller than 1 KB. A silently-failed build can produce a
   near-empty .wasm file that deploys without error but calls will panic.
   Prints actual size to logs for quick sanity checking.

2. Retry logic in run(): wraps execSync with up to 2 retries for transient
   network errors (connection refused, timeout, ECONNRESET, 429, 503).
   Non-transient errors still exit immediately. Uses a synchronous spin-wait
   (3 s, 6 s) to avoid adding async complexity to the deploy script.

Also: fix stale prerequisite comment (wasm32-unknown-unknown -> wasm32v1-none).
Both ReputationCard.tsx and ProofHistory.tsx defined identical TIER_COLORS
maps. One source of truth prevents them drifting apart again.

- Added TIER_COLORS: Record<Tier, string> to sdk/src/types.ts with JSDoc
  explaining the colour rationale for each tier
- ReputationCard.tsx: removed local definition, imports TIER_COLORS from SDK
- ProofHistory.tsx: removed local definition, imports TIER_COLORS from SDK
- TIER_COLORS is now part of the public SDK API surface (re-exported via index.ts)
Screen readers and keyboard users were poorly served:
- Labels had no htmlFor/id binding (label clicks didn't focus inputs)
- The live score preview had no ARIA live region
- The spinner had no accessible label
- Error box had no role=alert so screen readers wouldn't announce it

Changes:
- All four inputs now have unique id attributes; labels use htmlFor
- inputs share aria-describedby pointing to the score-note hint
- Step indicator gets role=status aria-live=polite with a text label
- Score preview gets role=status aria-live=polite with a full text label
- Score bar gets role=progressbar with aria-valuenow/min/max
- Error box gets role=alert aria-live=assertive
- Submit button gets aria-disabled mirroring the disabled state
- Success box gets role=status aria-live=polite
- Spinner div gets aria-hidden=true (decorative)
…audit jobs

Four improvements to ci.yml:

1. Fix WASM build target: wasm32-unknown-unknown -> wasm32v1-none
   (Soroban contracts require wasm32v1-none since soroban-sdk 21+;
   the old target still compiled but produced non-optimised artifacts)

2. Expand push triggers: add feat/**, fix/**, chore/** branch patterns
   so feature branches get CI without needing a PR open first

3. New wasm-size job: after contracts build, measures each .wasm file and
   fails if any exceeds 100 KB — guards against accidental dep bloat

4. New audit job: runs npm audit (--audit-level=high) and cargo-audit
   on every push; continue-on-error=true makes it advisory for now
The existing contracts:test only ran cargo test (not --all), so crates
that aren't workspace members would be missed. Also added convenience
aliases that mirror the CI steps so contributors can run the same checks
locally before pushing:

  contracts:test       cargo test --all
  contracts:lint       cargo clippy --all-targets -- -D warnings
  contracts:fmt        cargo fmt --all
  contracts:fmt:check  cargo fmt --all -- --check  (used by CI)
  test:all             contracts:test + test:e2e in sequence
… set

Previously, running e2e_test.js without SECRET_KEY would create a random
unfunded keypair and then fail with a cryptic RPC 404 during step 3.

Changes:
- Print a clear warning at startup when SECRET_KEY is absent
- Steps 3, 4, 5 (on-chain) skip gracefully with an explanatory message
- Step 3 additionally checks account existence before sending; if the
  account is unfunded it prints the friendbot URL and exits with code 1
- Final summary distinguishes between offline-only pass and full e2e pass
- This lets CI run steps 1-2 without needing a funded testnet secret
…istry

The submit_proof success path had zero test coverage — only rejection cases
were tested. Added an AlwaysTrueVerifier mock contract and five integration
tests that exercise the full cross-contract call path:

- submit_proof_gold_sets_gold_tier (threshold=85)
- submit_proof_silver_sets_silver_tier (threshold=70)
- submit_proof_bronze_sets_bronze_tier (threshold=40)
- submit_proof_upgrade_allowed: Bronze -> Gold is accepted
- submit_proof_downgrade_not_allowed: Gold -> Bronze is silently ignored

Also added make_proof_bytes() helper to reduce boilerplate across tests.
The mock verifier lives in a test-only mod so it doesn't affect contract size.
Users had no way to wipe their local proof history other than clearing
all localStorage manually. Added a 'Clear' button in the header row of
the history list that removes the localStorage key and resets state.
The button is keyboard accessible and has an aria-label for screen readers.
…aymentWidget

The payment_gate contract exposes a limit() function that returns the max
per-transaction amount for a wallet based on its tier, but the SDK had no
wrapper for it and the frontend never surfaced it to users.

- Added NulliusClient.getLimit(walletAddress): Promise<bigint> in contracts.ts
  Uses simulateTransaction against payment_gate.limit()
- PaymentWidget.tsx fetches the limit on mount and on tier change
- Displays the XLM limit inline in the subtitle next to the tier name
  (e.g. 'Max per transaction: 100,000 XLM') so users know their cap before
  entering an amount
@vercel

vercel Bot commented Jul 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nullius Ready Ready Preview, Comment Jul 18, 2026 1:53am
nullius- Ready Ready Preview, Comment Jul 18, 2026 1:53am

@IyanuOluwaJesuloba
IyanuOluwaJesuloba merged commit 28d5eb9 into main Jul 18, 2026
11 of 13 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.

1 participant