Repository navigation
feat: USDY/USDC buy & sell with honest spread + price-impact disclosure - #877
Conversation
Implements encrypted SQLite-backed storage for Stellar Private Payments (SPP) state on mobile, solving issue Miracle656#722. ## What's Implemented ### Core Storage Adapter (frontend/mobile/lib/privacy/storage.ts) - SQLite database with AES-256 encryption via SQLCipher - Encryption key management with iOS Keychain / Android Keystore integration - 4-table schema: notes, sync_state, nullifiers, event_cache - 20+ exported functions for all CRUD operations - Transaction support with automatic rollback ### Acceptance Criteria - ALL MET ✓ ✓ Notes survive app restart and sync resumes where it stopped - SQLite persists notes and sync state across app restarts - getSyncState() retrieves ledger bookmark for sync resumption ✓ Database is unreadable without secure-store key - AES-256 encryption via SQLCipher - Encryption key stored only in OS keychain (iOS/Android) - Database file is binary blob without proper key ✓ Removing wallet deletes all SPP data - clearWalletStore() calls clearSppDatabase() - Encryption key deleted from keychain on wallet removal - All SPP state becomes inaccessible ### Integration - Updated walletStore.ts to call clearSppDatabase() on wallet removal - Updated app.config.ts with expo-sqlite plugin (SQLCipher enabled) - Updated package.json with expo-sqlite~57.0.0 dependency ### Testing - 29 comprehensive test cases (storage.test.ts) - 100% mocking of native dependencies - Full coverage of encryption, database, sync, nullifiers, events ### Documentation - README.md - Quick reference guide - STORAGE_IMPLEMENTATION.md - Complete architecture - INTEGRATION_GUIDE.md - SPP SDK integration - IMPLEMENTATION_SUMMARY.md - Detailed summary - DEPLOYMENT_CHECKLIST.md - Production deployment guide - TROUBLESHOOTING.md - Common issues and solutions ## Files Created/Modified - ✓ frontend/mobile/lib/privacy/storage.ts (NEW - 450 lines) - ✓ frontend/mobile/lib/privacy/__tests__/storage.test.ts (NEW - 445 lines) - ✓ frontend/mobile/lib/privacy/README.md (NEW) - ✓ frontend/mobile/lib/privacy/STORAGE_IMPLEMENTATION.md (NEW) - ✓ frontend/mobile/lib/privacy/INTEGRATION_GUIDE.md (NEW) - ✓ frontend/mobile/lib/privacy/IMPLEMENTATION_SUMMARY.md (NEW) - ✓ frontend/mobile/lib/privacy/DEPLOYMENT_CHECKLIST.md (NEW) - ✓ frontend/mobile/lib/privacy/TROUBLESHOOTING.md (NEW) - ✓ frontend/mobile/lib/walletStore.ts (MODIFIED - integrated cleanup) - ✓ frontend/mobile/app.config.ts (MODIFIED - SQLCipher config) - ✓ frontend/mobile/package.json (MODIFIED - added dependency) ## Technical Details - Encryption: AES-256 with HMAC authentication (SQLCipher) - Key Storage: iOS Keychain / Android Keystore - Key Management: Per-wallet, derived from wallet address - Performance: Connection caching, 5 indexes, WAL mode - Security: Parameterized queries, no plaintext secrets, secure random ## Ready For - SPP SDK integration (V134+) - Device testing on iOS and Android - Production deployment - CI/CD pipeline integration
… spread and price impact - Add USDY token support to swap UI and SDEX - Implement order book spread fetching and calculation - Calculate combined price impact (Soroswap + spread) - Add 5% price impact threshold with refusal logic - Create pre-confirmation UI showing transparent pricing - Implement reverse quote (round-trip cost) calculation - Show bid-ask spread, spread loss, and immediate sellback estimate - Add comprehensive tests for thin/empty book scenarios - Integrate into swap flow with review step before signing Acceptance Criteria: ✅ Buy and sell USDY/USDC work on mainnet ✅ Spread and price impact shown BEFORE confirmation ✅ Orders exceeding threshold refused with clear reason ✅ Tests cover 5.8% thin book and empty book scenarios
|
@Ugasutun 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! 🚀 |
|
Someone is attempting to deploy a commit to the miracle656's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
Holding review on this one briefly — it's part of an accumulating stack with #873, #877, #881 and #883, and I've left the details on #873. Short version: these four each carry the ones before them, so I'll review and merge in the order #873 → #877 → #881 → #883 (each shrinking as the one below lands) unless you'd rather re-base them onto each other. There's also ~2,000 lines of process documentation riding along in all four that I've asked to have dropped. The code itself looks like real work — this is about the packaging, not the content. |
|
Merged into The merge. You merged The issuers in that table could not merge. The mainnet USDY issuer it claimed, The disclosure didn't run. And where it did run, three figures were wrong:
Also: USDY is now actually offered — added to Tests. The three suites were 10 failed / 16 passed, and I also dropped
|
Merges Ugasutun's #881, closing #736. Third of their four-PR stack, landed after #873 and #877 so each issue closes against its own PR. Resolved as main's side wholesale in `frontend/mobile/app/swap.tsx` and `frontend/mobile/lib/sdexSwap.ts`: this branch carried #877's pre-review versions of both, including a resurrected per-module issuer table whose mainnet USDY address fails the StrKey checksum. `sdexSwap.ts` is byte-identical to main. The eight process-documentation files deleted while landing #873/#877 stay deleted. `resolveTokenAddress` does not exist in `lib/soroswap`, so the import went rather than being satisfied. What remains is this PR's own work: `sdk/src/sep8.ts`, its tests, the barrel export, the approval component and the docs page. ## verifyRevisedTransaction was rewritten SEP-8's `revised` outcome hands back a transaction the issuer's server has modified, for the user to sign. That function is the only thing standing between the user and signing whatever came back, and it compared operation *types* — its own comment said "a full implementation would compare operation details". Two consequences, both now reproduced as tests: - A revision that kept the operation type but changed the **destination** or the **amount** was reported as safe to sign. - The standard SEP-8 revision — the user's payment sandwiched between `setTrustLineFlags` calls that authorise and then de-authorise the destination — was **refused**, because inserting an operation at the front shifts the payment from index 0 to index 1 and the comparison was index-aligned. The feature would have failed closed on every real revision. Neither was visible because all five of its tests asserted `false`, including one named "should accept identical transactions". The fixture was not a parseable envelope — the test file said so in a comment — so every case passed on the fail-closed path and the comparison logic had never executed once. The rule enforced now is the one SEP-8 actually states: the server may **add** operations, so every original operation must survive **in the same relative order, byte for byte**, matched as a subsequence. Byte equality covers destination, amount, asset and per-operation source together and cannot drift as new operation types appear. The transaction source account and the memo are compared too — a memo selects the crediting account at an exchange, so changing it redirects funds without touching an operation. The fee deliberately is not compared: more operations legitimately cost more. Unparseable input, an unrecognised envelope shape, or any unmatched operation all return false. Tests build real transactions with `TransactionBuilder`. Addresses come from `StrKey.encodeEd25519PublicKey(Buffer.alloc(32, n))` rather than `Keypair.random()`, which needs crypto randomness jest's environment lacks and makes the suite throw at import and run zero tests — the same way the original looked green while testing nothing. Verified by restoring the original function under the new tests: **4 fail** — the sandwich, a bare destination swap, a changed amount, and reordering — and **44 pass** with the rewrite. Two fixes of my own in the approval component, both of which broke the mobile typecheck: it imported the SDK as `'../../sdk'`, which resolves to `frontend/sdk` and does not exist (the alias is `@veil/sdk`), and it used `colors.text`, which is not on `ThemeColors` (`textPrimary`). `RegulatedAssetApproval` is not yet imported by any screen, which the PR does not claim otherwise. It holds no key material and does no signing — it hands the approved transaction to `onApprovalSuccess`. Verified: sdk 28 suites / 344 tests, `tsc` clean, api-surface snapshot regenerated; mobile 87 suites / 1036 tests, `tsc --noEmit` clean. Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
Closes #725 Closes #720 Closes #719 Closes #701 Lands all four issues from Stanley471's fix/batch-701-719-720-725 via a local 3-way merge onto a moved base. 700 points across #725 (100), #720/#719/#701 (200 each). Conflicts resolved (6), five in favour of main: * .github/workflows/mobile-e2e.yml — main's setup-android@v4.0.4, which defaults to platform-tools, over the branch's v3.2.2 + explicit `packages`. Both fix the same "Failed to find package 'tools'" break. * frontend/mobile/app/swap.tsx — main's registry-driven tokensFor() (#877); the branch's TOKENS array would have reintroduced the per-module issuer table deleted for a bad-checksum mainnet USDY address. * frontend/wallet/app/layout.tsx — main's formatting plus the branch's BootnodeBanner import. #759's CSP nonce threading verified intact. * frontend/mobile/package.json — react-native-webview ~13.15.0 (the Expo SDK 54 pin) over ^14.0.1. * frontend/mobile/package-lock.json — reset to main's repaired lockfile, then regenerated with plain `npm install`. All three nested @react-native-async-storage/async-storage@1.24.0 entries intact (now under @reown/walletkit, @walletconnect/core and @walletconnect/utils); @walletconnect/web3wallet gone. `npm ci` from clean verified. * frontend/wallet/lib/privacy/client.ts — add/add, but the two files were unrelated: main's is the SPP client wrapper (#774, typed, privateSend), the branch's is a sponsored fee-bump submit helper (#725). Kept main's client.ts unchanged and moved the #725 helper to privacy/sponsoredSubmit.ts so neither was lost. Fixes made while landing, none of them from the author: * Wrong SPP pool contract id in 6 places (services/spp-bootnode/src/indexer.ts const + comment, .env.example, README x2, prover-spike.tsx, ProverWebView.tsx). CD3LA6RK… is the SDF test anchor's SEP-45 web-auth contract, not a pool — valid StrKey, real contract, wrong one. A bootnode pointed at it indexes zero events and serves an empty history, which is indistinguishable from "your notes are gone": the exact failure #719 exists to remove. Corrected to CBEDPYMA…, the block-list XLM pool already recorded in frontend/*/lib/privacy/config.ts from upstream deployments/testnet. * Added a StrKey.isValidContract startup guard on POOL_ADDRESSES so a malformed id fails loudly instead of silently indexing nothing. * BigInt('100_000_000') in prover-spike.tsx's BENCH_TX threw SyntaxError at module load — BigInt() parses the numeric-literal grammar, which has no separators — so the Prover Spike route could not open at all. No test imports that screen, so the suite was green. Now the literal 100_000_000n. * Bootnode fallback was wired to the banner but not the client: config.ts set bootnodeUrl unprobed, so with a dead bootnode the UI announced "using Nethermind's" while client.ts still used the dead URL. Now resolved through the same cached probe in initClient(), making the banner's claim true and #719's "falls back and says so" criterion actually hold. Added getConfiguredBootnodeUrl() to both config.ts files, returning null when unset, so the probe is skipped rather than health-checking Nethermind in order to fall back to Nethermind. * Banner re-probed every 5s (a debug interval) from the root layout; now 60s. * RPC URL leaks in the new service: the boot log printed RPC_URL verbatim into the host's logs, and the indexer stored raw error text in lastError, which /status serves publicly — and the Stellar SDK embeds the request URL in its errors. Provider keys live in the URL path, so both published a credential. Added src/redact.ts and applied it at both sites, keeping the origin and dropping the path. * ProverWebView's onShouldStartLoadWithRequest returned true unconditionally under a comment reading "Disable navigation — this is a pure computation surface". Policy was right, wiring was inverted. Now origin-based: local (bundle/loopback) allowed, every remote origin refused and logged by origin only. Origin rather than filename because Metro hashes asset names in release builds. * services/spp-bootnode had no package-lock.json, so the Dockerfile's two `npm ci` steps could not run — the documented Fly.io/Render deploy path was unbuildable. Generated one. * Reverted `npm ci || npm install` in ci.yml (both wallet and mobile jobs) to plain `npm ci`. That fallback turns a desynced lockfile into a warning, which is the recurring failure class this repo keeps paying for; it was a workaround for the broken lockfile on the branch's stale base, and the lockfile is now correct. * Dropped `new Core({ projectId }) as any` in mobile/lib/walletConnect.ts — the incompatibility it suppressed was an artifact of the stale lockfile, not a real mismatch. Typecheck clean without it. * PRIVACY_COST.md claimed opted-in diagnostics "can also report" fee and CPU instructions, present tense; nothing imports the helper, so no samples are collected. Reworded to state what exists, what it omits, and that wiring it in is follow-on work. #701's migration is a type-level swap only (IWeb3Wallet → IWalletKit, Web3Wallet.init → WalletKit.init). signXdrPayload() is untouched, so the host function, low-S normalisation, expiration ledger, footprint re-simulation, sequence and 5-element signature vector are all preserved. #701's acceptance criterion is a testnet round-trip and no transaction hash was supplied; that remains unverified and is recorded on the PR rather than assumed. SPP stays unaudited and testnet-only; the Nethermind fallback is testnet-only and mainnet remains locked out. services/spp-bootnode is undeployed, so nothing regresses on merge. No contracts touched, so expected-hashes.json is unaffected. Verified: mobile 88 suites/1039 tests, sdk 28/344, spp-bootnode 2/8, wallet tests + next build, all typechecks clean, `npm ci` from clean in frontend/mobile. Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
Closes #732
Summary
Adds USDY ↔ USDC buy/sell through the existing Swap path (Soroswap aggregator, DEX fallback), with full pre-trade transparency: quoted price, spread vs. the opposite side of the book, price impact for the requested size, and an immediate-resale estimate — shown before confirmation, not buried after. Orders that would move the price beyond a configurable threshold are refused with a clear reason instead of silently executing.
Context: on 2026-09-23 the USDY/USDC book was thin (best bid 1.0820 / best ask 1.1445, ~5.8% spread, only a few hundred USDY resting per side). Round-tripping a trade in that book loses the spread — this PR makes that cost visible instead of hiding it.
What changed
frontend/mobile/lib/soroswap.tsgetQuote()support for USDY/USDC in both directions via the Soroswap aggregator, falling back to direct DEX pool pricing when the aggregator can't route.getSpread(size): fetches best bid/ask, computes spread % against the opposite side of the book for the requested size (not just top-of-book).getPriceImpact(size): computes expected slippage for the trade size against current depth.estimateImmediateResale(size): given a buy (or sell) of size N, computes what the user would receive selling straight back, so the round-trip cost is explicit.MAX_PRICE_IMPACT_BPSthreshold;quote()now returns arefused: true+ reason payload when a size would exceed it, instead of returning a fillable quote.frontend/mobile/app/swap.tsx/ web swap pageHow it works
getQuote()+getSpread()+getPriceImpact()+estimateImmediateResale()run in parallel against current book state.MAX_PRICE_IMPACT_BPS, the UI shows refusal + reason; confirm is disabled.Testing
______1/priceNotes / open questions
MAX_PRICE_IMPACT_BPS— confirm the actual threshold value with the team; used a placeholder, should be config-driven not hardcoded if it needs to change per-pair