Feat/nullius improvements - #15
Merged
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.