chore: promote to main for 1.118.8 release - #295
Open
TuCopFi wants to merge 48 commits into
Open
Conversation
…xposure) (#265) * feat(earn/neeru): remove contract surface from tracked source (zero-exposure) Backend flagged on 2026-07-12 that the wallet repo published the full earn-vault contract layout in src/earn/neeru/abi.ts (positions struct return with 8 named fields, Deposit event args, previewAccruedInterest signature, three named custom errors) and types.ts mirrored the struct 1:1. Since the contract is intentionally unverified on Celoscan, that made every field/event/error name recoverable from a public grep, defeating the point of leaving the bytecode nameless. This closes the exposure surface end to end. - abi.ts: the named ABI object is replaced by DEPOSIT_EVENT_DATA_SCHEMA, a bare list of ABI parameter type descriptors (uint8, uint256, uint256) for decoding the Deposit event data slot. No function names, no event args, no error names in source. - eventParsing.ts: parseDepositEvent now filters by topic0 (env-driven) and decodes non-indexed args with decodeAbiParameters against the type-only schema; the indexed positionId is read from topics[2] via hexToBigInt. - constants.ts: contract address, Deposit topic0 and the three custom error selectors are read from react-native-config with hex defaults so local builds keep working; a repo-wide grep no longer surfaces the address as text. The tranche language is renamed to category end-to-end (NeeruCategoryId, NEERU_CATEGORY_IDS, NEERU_CATEGORY_LABEL_KEYS, categoryIdFromPositionId). The positionId regex still accepts both :tranche-<N> and :category-<N> so persisted state parses cleanly. - saga.ts: matching the low-interest-pool revert now runs through NEERU_ERR_INTEREST_POOL_LOW_SELECTOR (env-driven) instead of a hardcoded 'InterestPoolLow' string. The exported helper is renamed isLowPoolError and the wallet-internal signal constants NEERU_LOW_POOL_ACTION and NEERU_LOW_POOL_ERROR replace the contract-error-name mirrors. - api.ts: adapter collapses to identity now that wire and internal names align on category / categoryLabel / amount / endTs / rateValue. The legacy fallback (amount ?? principal, etc.) is removed since backend confirmed 2026-07-12 that the pre-cutover names are no longer emitted. - types.ts, rateConversion.ts, selectors.ts and all five UI components cascade the rename: principal -> amount, tranche -> category, trancheLabel -> categoryLabel, maturityTs -> endTs, dailyRateRay -> rateValue. - Tests updated to match: new-wire assertions, selector-based low-pool detection, opaque wallet error signals. Verification (backend's grep list): grep -rE 'FondoCOPmMVP|0xD05CDF2|0x988Af5977201|dailyRateRay| capitalizedInterest|lockSeconds|maturityTs|InterestPoolLow| principal_only|renewed_from_position_id' src/earn/neeru/ returns zero matches (was 252 hits before this change). Full test suite (80 tests) passes, TypeScript and lint clean. * chore(persist): bump v252 clearing stale neeru positions on upgrade The zero-exposure refactor renames the persisted NeeruIndividualPosition shape (principal -> amount, tranche -> category, trancheLabel -> categoryLabel, maturityTs -> endTs, dailyRateRay -> rateValue). Old persisted state from 1.118.7 uses the pre-rename field names; loading it into the new reducer would render the earn cards as undefined until the next backend fetch. Migration 252 clears state.neeru.positions and state.neeru.optimisticPositions on upgrade. The earn screen refetches on mount so the visible impact is a single fetch cycle and no data loss (no client-computed state to preserve). test/RootStateSchema.json regenerated to reflect the renamed fields. * test: extend schemas to v252 and bump inline snapshot for the persist upgrade
…odes + smoke playbook (#266) Wallet-consumer-spec 2026-07-12 review found three gaps against tasks/specs/wallet-consumer-spec.md; this PR closes all three. 1. src/positions/saga.ts now surfaces the hooks-api meta envelope on the earn positions fetch. When getEarnPositions returns { data: [...], meta: { partialFailure: { neeru: true } } }, the saga preserves any prior positions from the affected app (neeru-vaults / allbridge) and merges them with the successful slice. Without this, the earn tab silently rendered empty cards for the failing app - the exact scenario that ran for 27h during the 2026-07-11 selector incident and only stayed invisible because the Statsig gate had suppressed exposure. 2. src/earn/neeru/errorMapping.ts drops INVALID_TRANCHE and TRANCHE_CAP_EXCEEDED. Backend confirmed the pre-cutover codes are no longer emitted anywhere; the spec (section 8) enumerates the eleven TRIGGER_USER_ERROR_CODES the backend still surfaces, and this file now matches that list one-to-one. errorMapping.test.ts is extended to assert the legacy codes fall through to the unknown i18n key. 3. scripts/pre-release-backend-smoke.sh is the executable form of the verification playbook (spec section 9). Six curl checks against BASE=tucop-backend-production.up.railway.app: health probes, neeru catalogue count + partialFailure absence, shortcut list contains the new id and omits the legacy one, asset URLs 200/404, neeru detail shape, feed indexer lag under 100 blocks. Halts on any regression. Dry-run against live backend passes 6/6.
…umer spec path (#267) The wallet-consumer-spec.md doc lives in the backend clone at /Users/.../TuCOPWallet-Backend/tasks/specs/, not in this repo. Two comments pointed to a wallet-side path that never existed in tracked source. Rephrase to name the spec by title without the file path so future readers do not chase a broken breadcrumb.
…acked source (#268) * chore(earn/neeru): drop remaining partner-contract vocabulary from tracked source Backend sweep of HEAD c56e796 found residual tranche/principal/maturity vocabulary that survived the #265 refactor across i18n keys, i18n values, CSS class names, prop names, testIDs and test descriptions. This PR closes the remaining surface end-to-end. Backend's two grep assertions (tasks/specs/wallet-consumer-spec.md verification block) both return zero on this HEAD. Renames: i18n keys (locales/base/translation.json + locales/es-419/translation.json): - neeruVaults.tranches.* -> neeruVaults.categories.* - neeruVaults.detail.descriptionByTranche.* -> descriptionByCategory.* - neeruVaults.positionRow.principal -> positionRow.amount - neeruVaults.positionRow.maturity -> positionRow.availableAt - neeruVaults.closeSheet.principalLabel -> closeSheet.amountLabel - neeruVaults.closeSheet.principalOnlyCta -> closeSheet.amountOnlyCta - neeruVaults.closeSheet.principalOnlyHelp -> closeSheet.amountOnlyHelp - neeruVaults.errors.invalidTranche -> errors.invalidCategory - neeruVaults.errors.trancheCapExceeded -> errors.categoryCapExceeded i18n interpolation vars (rename in values + all t() callsites): - {{trancheLabel}} -> {{categoryLabel}} - {{principal}} -> {{amount}} i18n values (user-facing copy updated to avoid mirroring contract terms): - 'Principal: {{amount}} Pesos' -> 'Deposited: {{amount}} Pesos' (es-419: 'Depositado') - 'Principal' label -> 'Deposited' (es-419: 'Depositado') - 'Exit with principal only' -> 'Withdraw deposit only' (es-419: 'Retirar solo el deposito') - 'principal without interest' -> 'deposit without interest' (es-419: 'deposito sin intereses') - 'never to your principal' -> 'never to your deposit' (es-419: 'nunca al deposito') - Explainer bodies: 'principal + earned interest' -> 'deposit + earned interest' - Emergency explanation: 'recover your principal of X Pesos' -> 'recover your deposit of X Pesos' - 'Withdraw principal only' -> 'Withdraw deposit only' - 'Invalid tranche selected.' -> 'Invalid category.' (es-419: 'Categoria invalida.') Code: - src/earn/neeru/constants.ts: categoryIdFromPositionId regex drops the legacy :tranche- fallback branch; only :category- matches now. Backend no longer emits the pre-cutover suffix, and the earn slice refetches on mount so persisted positionIds from before 1.118.7 get replaced on the next successful backend read. - src/earn/neeru/errorMapping.ts: values point to invalidCategory / categoryCapExceeded (was still pointing at the old tranche i18n keys). - src/earn/neeru/NeeruCloseSheet.tsx: PrincipalOnly prop / testID / CSS class renamed to AmountOnly. onPrincipalOnlyRequested -> onAmountOnlyRequested. - src/earn/neeru/NeeruEmergencyCloseSheet.tsx: t() call passes 'amount:' instead of 'principal:' for the interpolation slot. - src/earn/neeru/NeeruPositionRow.tsx: two t() key strings updated. - src/earn/neeru/NeeruVaultDetailScreen.tsx: DESCRIPTION_KEY_BY_CATEGORY points at descriptionByCategory; local var trancheLabel renamed to categoryLabel; onPrincipalOnlyRequested callback renamed. - Tests: describe/it strings and testID references updated; the constants test that used a :tranche- positionId is replaced with a generic :legacy- suffix (still validates the 'unknown suffix returns null' branch). Release automation: - .github/workflows/check.yml gains a backend-smoke job that runs scripts/pre-release-backend-smoke.sh. Guarded by the same 'PR to main OR manual OR schedule' condition as vulnerability and knip-regression, so day-to-day Development PRs are not gated on backend uptime. * chore(earn): drop residual tranche vocabulary from PoolCard + EarnHome test Two comments in src/earn/PoolCard.tsx and one positionId literal in src/earn/EarnHome.test.tsx still spelled 'tranche' after the sweep in the parent commit. The wider grep across src/earn/ (not just neeru/) now returns zero for tranche|principal|maturity.
…ub (#269) The two comments in src/redux/migrations.ts and test/schemas.ts spelled out the pre-cutover field names (trancheLabel, maturityTs, dailyRateRay) as documentation of what the migration replaces. That block survived the zero-exposure sweep because grep scoped to src/earn/neeru/. Rewriting it to 'five fields renamed to opaque wire names' before running the history scrub keeps HEAD identical after --replace-text and avoids a garbled comment ('categoryLabel -> categoryLabel') as collateral.
… shortcut gas (#270) * fix(viem): buffer non-native shortcut gas by 25% to avoid CIP-64 OOG When a shortcut (hooks-api trigger) pre-supplies rawTx.gas and the user pays in a non-native fee currency, wallet was only adding STATIC_GAS_PADDING (50k) on top. That was demonstrably too tight for complex shortcut calls: on 2026-07-14 a Neeru deposit reverted OOG at 306,385 / 310,000 (98.8%) because the 260k hint + 50k padding left no room for real-world variance. Add SHORTCUT_NON_NATIVE_BUFFER_BPS = 2500, applied ONLY when both conditions hold (shortcut-provided gas AND CIP-64 fee currency). Wallet-estimated txs already benefit from Forno's binary-search LIMIT and are unaffected. Buffer scales with the shortcut's own gas so small pre-set gas barely moves (+3.75k on a 15k tx) while realistic shortcut calls get a real safety margin (+65k on a 260k tx). * fix(feed): render failed state on EarnFeedItem so reverts stop reading as successful When an Earn deposit / withdraw / claim reverts on-chain, TransactionFeedItemImage swapped its avatar to the attention icon but EarnFeedItem never read transaction.status for subtitle or amount styling. Result: a failed 20,000 COPm Neeru deposit rendered "en la pool Neeru Vaults" with a full-color -COP$20,000 amount, indistinguishable from a successful move (trust-critical UX bug). Mirror the TransferFeedItem pattern via transferFeedUtils.ts:151: - If status === Failed, override the type-specific subtitle with feedItemFailedTransaction. - Style the amount in Colors.gray3 instead of the accent color so it visually reads as inactive without hiding the number (retry decisions still need it). * fix(feed): render failed state on SwapFeedItem to close the same gap in swap history Same class of bug as EarnFeedItem: SwapFeedItem only passed status to the avatar image and never overrode subtitle or amount styling. Any failed swap (Squid, cross-chain, multi-leg 7702 Dolares batch) rendered with its full route text ("Dolares > Pesos") and full-color incoming amount, reading as successful. Confirmed live in 1.118.7. Apply the same pattern as EarnFeedItem so a reverted swap: - Replaces the route-based subtitle with feedItemFailedTransaction regardless of cross-chain / multi-dollar / multi-leg / single-leg branch. - Grays the incoming amount (outgoing amount is already gray). Left the icon path unchanged (TransactionFeedItemImage already handles Failed). * test(swap): refresh SwapScreen gas math for the CIP-64 shortcut buffer The SwapScreen fixture and analytics assertion baked in the pre-buffer gas numbers. After a21e209 added SHORTCUT_NON_NATIVE_BUFFER_BPS = 2500 to prepareTransactions, the swap tx now consumes: base 1_800_000 + STATIC_GAS_PADDING 50_000 + 25% * 1_800_000 = 2_300_000 Update the fixture (1_850_000 -> 2_300_000) and the swap_review_submit analytics assertion (gas 1_871_000 -> 2_321_000, maxGasFee/estimatedGasFee recomputed accordingly) so the four broken SwapScreen tests match the shipped behavior. Comment expanded to spell out the buffer contribution.
…ncy fallback + UI polish (#271) * feat(earn/neeru): consume backend simulation-revert envelope + emergency fallback stash Backend now pre-simulates the Neeru withdraw and, when it would revert, returns `transactions: []` plus `dataProps.simulationRevert.{selector, reason}`. When the revert is INTEREST_POOL_LOW, the response also carries a pre-built `dataProps.fallback.transactions` (the amount-only calldata, ready to sign), so the wallet can present the emergency sheet without a second hooks-api round-trip and without paying gas on a doomed tx. Wallet-side changes: - `slice.ts`: new transient `pendingEmergencyFallback` keyed by positionId, plus `setEmergencyFallback` / `clearEmergencyFallback` reducers. Cleared on rehydrate so stale calldata never survives an app restart. - `selectors.ts`: `neeruEmergencyFallbackByIdSelector(state, positionId)`. - `saga.ts`: - `TriggerShortcutResponseData` type for the enriched envelope. - `classifySimulationRevert(...)` returns an opaque tag (`low_pool` | `already_closed` | `not_owner` | `unknown` | null) matched against the raw 4-byte selectors in `constants.ts`. No contract error names in tracked source. - `closeNeeruPositionSaga`: when `response.transactions.length === 0`, routes on the classified tag. LOW_POOL stashes the pre-built fallback (if present) and dispatches the wallet-side action so the screen swaps to the emergency sheet. ALREADY_CLOSED / NOT_OWNER / UNKNOWN each map to their own opaque failure tag. - `emergencyCloseNeeruPositionSaga`: reads the stash first and consumes it in place, skipping triggerShortcut entirely. Falls back to a fresh trigger when reached via the wallet-side receipt-check safety net, which does not carry pre-built calldata. - `test/schemas.ts`: mirror the new persisted field. - Tests: 7 new cases (LOW_POOL with + without fallback, ALREADY_CLOSED, NOT_OWNER, UNKNOWN selector, empty response with no dataProps, emergency saga skip-trigger, emergency saga fall-through-to-trigger). Existing tests now bind `.withState({ neeru: initialState })` so selector calls resolve cleanly under expectSaga. Receipt-check + on-chain replay path stays as safety net for races between the backend simulation and the actual send. * refactor(earn/neeru): sheets to BottomSheetModal + formatted amounts + proactive amount-only path Neeru sheets migrate off the wrapper `BottomSheet` component to `@gorhom/bottom-sheet`'s `BottomSheetModal` directly, so they can carry a `BottomSheetBackdrop` (dim+dismiss on tap) and dynamic sizing. Amount strings across the sheets, PositionRow, and PoolCard now go through `formatValueToDisplay`, which renders thousands separators and two decimals ("10,000.00" instead of the raw "10000"). Root testIDs (`NeeruCloseSheet`, `NeeruEmergencyCloseSheet`) landed on the inner `View` so the screen-level tests can assert sheet visibility around `Manage` / dismiss transitions. Two additions on top of the refactor: - `NeeruCloseSheet.AmountOnly`: proactive "withdraw only my deposit" button below the primary Confirm CTA. When tapped, the detail screen dismisses the close sheet and presents the emergency sheet for the same position, so a user who already knows the interest pool is low can skip the doomed full-withdraw attempt and its gas cost. New `amountOnlyCta` translation key in en / es-419. - `PoolCard` COPm balance fix: when the deposit token is COPm (mainnet) and its `priceUsd` is 0 (Mento stablecoins hydrate with a placeholder price), the USD -> local conversion collapses to 0. Skip the conversion in that case and render the token balance directly with the local currency symbol. Sheet tests refreshed to match the formatted output (`/10,000/`, `/82\.50/`) and the new proactive path. * chore(test/schema): regenerate RootStateSchema.json for pendingEmergencyFallback The new transient neeru state field lands in the generated Redux state schema. No migration needed because the field is reset on rehydrate (see the REHYDRATE reducer in earn/neeru/slice.ts). * test(redux/store): refresh rehydration snapshot for pendingEmergencyFallback The `validates against the RootState schema after rehydration` inline snapshot mirrors the runtime state tree, so it needs the new transient neeru field added under `state.neeru`. Same order as the initialState in the slice reducer.
…hain layout (#272) * fix(earn/neeru): correct Deposit topic0 default hex The zero-exposure sweep hardcoded a topic0 default that did not match the event actually emitted on-chain, so parseDepositEvent never found a matching log and the wallet fell back to backend polling for every deposit. Replaces the default with the value the backend meta endpoint exposes (also verified by matching a real deposit receipt on Celo mainnet). Env override via NEERU_DEPOSIT_TOPIC0 unchanged. * fix(earn/neeru): align Deposit event data schema with 4-slot layout Backend meta endpoint declares the Deposit event's non-indexed data as four ABI slots (uint8 + three uint256), but the local schema only had three. viem's decodeAbiParameters is strict about slot count, so the decode threw and parseDepositEvent silently returned null on every real receipt. Extends the schema by the trailing uint256 slot (currently ignored by the wallet) and refreshes the destructure with an explicit tuple cast. Updates the fixture in eventParsing.test.ts to encode all four slots so the happy-path assertion exercises the new layout.
…gue (#273) * feat(earn/neeru): add /meta + /catalogue fetchers and shared types Introduces NeeruMeta and NeeruCatalogue payload types plus their fetch helpers. Both endpoints share a 5s timeout, shorter than the position fetch because falling back to hardcoded defaults or a skeleton is quick and always safe. * feat(earn/neeru): neeruConfig slice + selectors + boot saga Adds a Redux slice that tracks the cached backend meta payload and the current-session catalogue payload separately. Meta persists across sessions with a source tag (backend|cache|fallback) and a fetchedAt timestamp; the resolver enforces a 24h TTL and falls back to a hardcoded last-resort payload only in cold-boot-offline. Catalogue never persists and has no fallback because on-chain rates fluctuate operationally, so callers must render a skeleton while it retries. The boot saga fires a meta fetch once at neeruSaga start; catalogue is left for consumers to trigger on-demand so wallets that never open Earn do not burn a request. * feat(earn/neeru): wire neeruConfig reducer + spawn boot saga Registers the new slice in reducersList and spawns neeruConfigSaga at the top of neeruSaga so the meta fetch runs as soon as the app boots. Order matters: config runs before positions fetch so downstream consumers can start reading from neeruConfig instead of hardcoded defaults in follow-up PRs. * test(earn/neeru): cover neeruConfig slice, selectors and boot saga Slice tests cover meta success/error, cache preservation on failure, catalogue no-fallback behavior, and REHYDRATE downgrading source from backend to cache. Selector tests exercise the four resolver branches (hardcoded fallback, backend fresh, cache fresh within TTL, cache stale past TTL) plus the exact TTL boundary. Saga tests dispatch success and failure for both endpoints. * chore(test/schema): register neeruConfig slice shape Adds v252SchemaWithNeeruConfig sibling of v252Schema (no persist version bump, autoMergeLevel2 folds the initialState for existing users at rehydrate). Regenerates test/RootStateSchema.json and refreshes the rehydration snapshot in src/redux/store.test.ts.
New test suite src/earn/neeru/__tests__/hardcoded-vs-live.test.ts that fetches /api/meta/contracts/neeru and /api/earn/neeru/catalogue live from the production backend and asserts the hardcoded NEERU_META_HARDCODED_FALLBACK matches byte for byte (proxyAddress, Deposit topic0, dataSchema structural shape, three error selectors, depositToken.address) plus structural category IDs and lock-period seconds on the catalogue side. Rates are excluded from the assertion because the operator retunes them without a contract upgrade; retunes must not fail CI. Suite is gated by NEERU_LIVE_META_CHECK=1 so local yarn test runs offline stay green. A new blocking workflow job (Neeru meta drift check) sets the env var and runs on every PR. Drift means merge blocked, closing the loop on the PR #272 class of bug.
…dcoded constants (#276) * refactor(earn/neeru): thread meta into eventParsing + saga helpers parseDepositEvent now receives contractAddress, topic0 and dataSchema as arguments instead of reading from static constants. Same for classifySimulationRevert (errorSelectors) and isLowPoolError (lowPoolSelector). Sagas that call them read the resolved meta from neeruMetaSelector at the top and pass the fields down. Runtime behavior: backend meta > cache (< 24h) > cache (> 24h, degraded) > hardcoded fallback. Same fallback the CI drift check enforces byte-for-byte. * chore(earn/neeru): drop hardcoded contract state from constants.ts + abi.ts Removes the five hex constants (NEERU_CONTRACT_ADDRESS, NEERU_DEPOSIT_TOPIC0, three NEERU_ERR_*_SELECTOR values) and the deposit-token pair that were sitting in constants.ts with build-time env overrides. Also deletes the now-unused src/earn/neeru/abi.ts. All of these flow through neeruMetaSelector at runtime and the last-resort fallback lives in configSelectors.NEERU_META_HARDCODED_FALLBACK. DEPOSIT_TOKEN_ADDRESS + DEPOSIT_TOKEN_ID had no external callers so they are dropped without replacement; consumers use meta.depositToken.address when needed. * test(earn/neeru): update fixtures + provides to the meta-injected helpers parseDepositEvent tests import topic0 and dataSchema from NEERU_META_HARDCODED_FALLBACK and pass them explicitly on every call. Saga tests provide neeruMetaSelector -> fallback so the wallet-side flow exercises the same hex constants the CI drift check enforces. isLowPoolError tests pass the INTEREST_POOL_LOW selector explicitly.
…logue at runtime (#280) * feat(earn/neeru): expose per-category catalogue selector New helper neeruCatalogueCategoryByIdSelector returns the {secs, monthlyRatePercentage, annualEffectivePercentage} row for a given category id, or null when the catalogue is missing the id. Consumers must handle the null branch (skeleton / retry) instead of silently using stale hardcoded rates. Backend-first policy for catalogue values. * refactor(earn/neeru): optimistic UI consumes catalogue for lock + monthly rate handleNeeruDepositOptimistic now reads the target category from the catalogue and passes secs + monthlyRatePercentage to buildOptimisticPosition. When the catalogue does not have the category (cold boot before the fetch lands), the saga triggers a background catalogue fetch and falls back to the normal backend-driven position fetch instead of using hardcoded values. Removes CATEGORY_DURATION_SECONDS and the monthlyPercentFromRateValue import; buildOptimisticPosition now takes secs and monthlyRatePercentage as explicit arguments. Test provides feed CAT_ROWS through a helper. * feat(earn): PoolCard reads Neeru rates from catalogue + refresh on EarnHome mount PoolCard now consumes annualEffectivePercentage + monthlyRatePercentage straight from neeruCatalogueCategoryByIdSelector when available, so operational retunes on the backend show up on the card without the wallet re-releasing. Falls back to the local monthly-to-annual conversion when the catalogue is still loading (cold boot). EarnHome dispatches fetchCatalogueStart on mount so the catalogue is refreshed every time the user visits Earn instead of only at boot. * chore(earn/neeru): drop monthlyPercentFromRateValue (backend catalogue covers it) The wallet no longer computes the monthly rate from the on-chain RAY value; the backend catalogue exposes monthlyRatePercentage directly. Test suite renamed to exercise effectiveAnnualPercentFromMonthly (kept as a pre-catalogue PoolCard fallback) plus the existing computePayout coverage.
#281) Adds fetchNeeruTxStatus API helper and refactors enforceReceiptsOrThrow to cross-check its wallet-side eth_call replay against the backend's replay at N-1. Confidence tag on the thrown error follows the 4-cell matrix backend specified on 2026-07-25: wallet=X + backend=X -> confirmed (persistent revert) wallet=null + backend=X -> transient (already resolved on-chain) wallet=X + backend=null -> live-only (new reason after mining) wallet=null + backend=null -> unknown (surface "estado incierto") Disagreement between the two selectors is logged and treated as live-only so the user sees the current state, not a stale one. Backend outage degrades to wallet-only (fail-soft, still throws so callers do not treat the revert as success). cause.data still carries the raw selector so isLowPoolError and classifySimulationRevert keep working.
…ta shape (#283) * chore(earn/neeru): opaque event + error names in NeeruMeta, lowercase addresses The 5-PR wire refactor exposed the earn-vault surface in tracked source by using semantic property names (events.Deposit, errorSelectors. INTEREST_POOL_LOW, etc) and a checksummed proxy address. The zero-exposure policy from PR #265 forbids exactly this: a repo grep must not surface the contract's event or error names. Rewires the internal projection so: - events.Deposit -> events.primary - errorSelectors.INTEREST_POOL_LOW/ALREADY_CLOSED/NOT_OWNER -> e1/e2/e3 - proxyAddress and depositToken.address stored lowercase - backend response adapted at the api.ts boundary (adaptNeeruMeta reads raw keys positionally, never references them as property paths) The hardcoded fallback now agrees with the adapter output byte for byte, and every downstream consumer sees only the opaque projection. * refactor(earn/neeru): saga consumers use opaque errorSelectors + events.primary classifySimulationRevert's third argument now takes {e1, e2, e3} instead of the semantic names; the mapping to internal tags (low_pool, etc) is stable via the enumeration order backend guarantees. handleNeeruDepositOptimistic reads meta.events.primary.{topic0,dataSchema} and closeNeeruPositionSaga passes meta.errorSelectors.e1 to isLowPoolError. * test(earn/neeru): consume opaque projection, route live drift check through adapter - eventParsing fixture: events.primary.{topic0,dataSchema} - hardcoded-vs-live: routes the live /meta response through adaptNeeruMeta and asserts the opaque projection matches the local fallback byte for byte, so semantic backend names never appear in this file - saga tests: errorSelectors.e1 and generic reason strings (E1/E2/E3) in the simulationRevert fixtures instead of the contract error names * chore(test/schema): regen RootStateSchema for opaque NeeruMeta shape Schema now uses events.primary and errorSelectors.{e1,e2,e3} matching the zero-exposure projection landed in this PR.
…ine (#282) Backend PR #152 shipped priceUsd for COPm on /hooks-api/getPositions, so the wallet can now render the standard USD-to-local conversion for COPm pools like any other token. The special-case that used the raw balance directly is kept only as a defensive fallback for when the token's priceUsd degrades to 0 (backend outage or fail-soft), so pool cards never show a blank headline for COPm specifically.
…ror schema (#284) * feat(sentry): PII scrub + opaque account id helpers Every Sentry payload the wallet ships passes through scrubSensitiveStrings before it leaves the device. EVM addresses (0x + 40 hex), transaction / storage / block hashes (0x + 64 hex) and wei-scale amounts (>= 15 digit runs) get replaced with opaque placeholders so an issue thread never contains the identity or holdings of the reporting user. opaqueAccountId maps a wallet address to a stable 16-hex id via FNV-1a so Sentry can still count sessions per user without receiving the raw address. Case-insensitive, deterministic, non-cryptographic (does not need to be). * feat(sentry): business-error helper with cross-team schema captureBusinessError is the only entrypoint sagas use so the event taxonomy (feature / provider / action / errorCode) is enforced by shape, not by convention. Fingerprint is set explicitly so the same failure mode groups into one Sentry issue across users regardless of the underlying exception message, which is what we want for dashboards (count / rate) rather than one issue per user. BusinessFeature + BusinessProvider unions cover the current wallet integrations (neeru, squid, allbridge, jumpstart, buckspay, marranitos, internal). Extend the unions as new integrations land so the schema review with backend stays sharp. * feat(sentry): wire beforeSend + opaque user context in Sentry.init Sentry.init now runs every event and every breadcrumb through scrubSensitiveStrings before they leave the device, sends sendDefaultPii=false so the SDK does not attach any identifying data of its own, and reports the user as an opaque hashed id derived from the wallet address instead of the raw address. * feat(sentry): wire captureBusinessError in earn, positions, swap paths Every catch block that already logs a business failure now also reports it to Sentry with the shared taxonomy: - earn/neeru/saga: fetch_positions, close_position, emergency_close - earn/neeru/configSaga: fetch_meta (also sets neeru_meta_source tag, primed for backend's fallback-usage dashboard) - positions/saga: trigger_shortcut, execute_shortcut - swap/saga: execute (with the existing classifyError code as errorCode) Existing Logger and AppAnalytics calls are preserved; Sentry is additive so nothing about the local UX or Segment (still disabled) paths changes. * chore(env): enable Sentry on mainnet Turns SENTRY_ENABLED on now that the PII scrub, business-error helper and saga wires are in place. The DSN is sourced from secrets.json (gitignored) at build time; the runtime SDK skips init when the DSN is missing so dev builds without the secret are unaffected. * chore(env): enable Sentry on mainnetdev too Dev builds should exercise the same PII scrub + capture path as production so smoke tests in the simulator surface any breakage before it ships. * feat(sentry): apply backend 2026-07-26 schema review Backend approved #284 with 3 requested changes; applying them plus the 2 relevant observations before merge. Requested changes: - Add 'wri' to the BusinessProvider union so future delegate-relay and fee-adapter-bootstrap failures do not fall through to 'internal'. - Introduce classifyHttpError (network_error | timeout | http_4xx | http_5xx | parse_error) and pass it as errorCode on every backend fetch path: fetch_meta, fetch_catalogue, fetch_positions, trigger_shortcut, execute_shortcut. - Introduce classifyRevertConfidence (revert_confirmed | transient | live_only | unknown) and pass it as errorCode on close_position and emergency_close catches. When the error came from enforceReceiptsOrThrow the confidence tag wins; falls back to classifyHttpError for network failures on the same path. Observations applied: - neeru_meta_source gains a 'cache' value emitted at neeruConfigSaga boot when the persisted meta already exists. Dashboard now differentiates fresh backend / TTL cache hit / fallback_pending. - beforeSend forces user.ip_address off (delete via destructure) so Sentry ingest never stores a connection IP even if the sendDefaultPii setting drifts on a future SDK upgrade. Observation 3 (fingerprint recipe) is documented in the helper comment: [feature, provider, action, errorCode ?? 'unclassified']. Favours count/rate dashboards; per-user detail lives on individual events.
Adds scripts/populate_secrets.sh which reads the Sentry auth token (SENTRY_ORG_TOKEN) and DSN (SENTRY_CLIENT_URL) from the developer's macOS Keychain, or from SENTRY_AUTH_TOKEN / SENTRY_CLIENT_URL env vars in CI, and writes ios/sentry.properties, android/sentry.properties, and secrets.json. All three targets are gitignored. Rewires postinstall to run the new script. Removes the yarn keys:decrypt / keys:encrypt entry points; the encrypted-file mechanism they wrap is deleted in the follow-up commit. Templates now carry the real org (tucop-finance) and project (tucopwallet) slugs so a dev without any token still gets a sensible scaffold. Missing token or DSN emits a warning but never fails postinstall; runtime error reporting simply stays disabled until credentials are provided.
Deletes the four encrypted files that depended on Valora's GCP project celo-mobile-testnet (keyring=github, key=wallet) and the scripts/key_placer.sh helper that encrypted/decrypted them: ios/sentry.properties.enc android/sentry.properties.enc secrets.json.enc e2e/.env.enc scripts/key_placer.sh The Sentry properties and secrets.json are now populated by scripts/populate_secrets.sh from the TuCOP-owned Keychain entries or CI env vars. e2e/.env.enc had no on-disk template, no local decryptable copy, and no TuCOP dev has access to Valora's KMS to open it. It is removed here; when TuCOP defines its own e2e credentials, they will land through the same Keychain/env mechanism. README scripts+layout tree updated to reflect the new setup.
Uncomments the sentry.gradle apply in android/app/build.gradle. Without it, Android release stacktraces reaching Sentry are minified and practically unreadable (line 12345 of a giant bundle), while iOS already had the equivalent Xcode phase active. Now both platforms upload sourcemaps and dSYMs during release builds. The plugin reads auth credentials from android/sentry.properties, which populate_secrets.sh already generates from the Keychain / env vars. Debug builds respect SENTRY_DISABLE_AUTO_UPLOAD=true and skip the upload step.
@sentry/types was on 7.x while the active SDK @sentry/react-native is on 6.22 which ships its own types. Grep across src/ confirms no imports; it only lingered in the auto-generated LicenseDisclaimer. Removing avoids the SDK-vs-types version drift and shrinks node_modules.
The Gold buy/sell confirm buttons only checked `!preparedTransactions`, but the screen initialises that state as an empty array (`[]`) while the on-screen quote is still being fetched and gas simulated. `![]` is `false`, so the button stayed enabled during the "Tarifa de Red: Estimando..." window. Tapping it navigated with `preparedTransactions: []` and `estimatedGasFee: "0"`, which the saga then blew up on with "Mismatch in number of prepared transactions and standby transaction creators" (0 txs vs 1 standby handler always pushed). Gate now requires: preparedTransactions is an array with length > 0 AND estimatedGasFee is set and != "0" AND no quoteError AND toTokenId resolved. Show the loading spinner while the quote hook is still running so the user understands why the button is disabled. Defence-in-depth in buyGoldSaga / sellGoldSaga: if the button is somehow bypassed and the saga is entered with an empty tx list, fail fast with a Spanish user-facing message and a Sentry business error tagged `empty_prepared_txs` instead of surfacing the generic mismatch. Also wire `captureBusinessError` in the buy + sell catch blocks so gold-flow failures land in Sentry with the shared business schema (feature=earn provider=squid action=buy_gold_execute / sell_gold_execute). Previously they only hit the Logger and never reached the dashboard.
…ee currency Previously useGoldQuote treated `prepareTransactions` returning `not-enough-balance-for-gas` (or `need-decrease-spend-amount-for-gas`) as success and returned a fake quote with `estimatedGasFee: '0'` and `preparedTransactions: []`. With the confirm button now correctly gated on those being populated, the user was left staring at "Estimando..." forever with no way to know what went wrong. Throw an actionable error instead. The confirm screen already catches and displays it via the inline error notification and the button gate stays disabled. Message deliberately does not blame the user's balance: in practice the same status code lands here for Forno rejecting eth_estimateGas on every CIP-64 fee currency (a real Celo RPC flakiness pattern we just observed live for 0x8427...ece which has 12,802 COPm plus 0.43 CELO of balance and yet hits "not-enough-balance-for-gas" because maxFee/baseFee margin got tight for the currency being tried). The copy invites a retry without misdiagnosing the state.
…en RPC rejects Celo mainnet switched from op-geth to op-reth on 2026-07-22 (https://forum.celo.org/t/13594). Since then, Forno intermittently rejects `eth_estimateGas` for CIP-64 non-native fee currency transactions with "Missing or invalid parameters", even for simple ERC20 approves that op-geth used to price cleanly. The existing rescue path already substitutes a 65k-gas fallback for `transfer(address,uint256)` (selector 0xa9059cbb) on the same class of failure. Extend the same fallback to `approve(address,uint256)` (selector 0x095ea7b3). Standard ERC20 approves have a well-known cost in the same range (45-55k), so the fallback is safe and unblocks the whole downstream batch — a swap flow whose approve fails estimation would return null and cascade to "not-enough-balance-for-gas" even when the user has ample balance in both fee currencies. Observed live 2026-07-26 with wallet 0x8427...ece (12,802 COPm plus 0.43 CELO) attempting COPm -> XAUt0 via Squid: the Squid-returned approve tx failed estimation for both COPm and CELO fee currencies because of the op-reth regression, and useGoldQuote correctly propagated "no fee currency worked" to the UI. With this fallback the approve is priced, the swap that follows uses the existing post-approve fallback, and the flow completes. Also fix GoldBuyEnterAmount to surface the actual error message from useGoldQuote instead of always showing the generic "Ruta de intercambio no disponible" translation. That message was actively misleading when the failure was a Forno flake and not a routing problem.
…w screens
`getInputDecimalsForToken` returns 6 across the board so users can TYPE
tiny Oro fractions without them rounding to zero. It was being called
in DISPLAY paths as well, producing "8,992.134348 Pesos" on the sell
confirmation review card when it should read "8,992.13 Pesos" per the
peso convention.
Splits the two concerns:
- `getInputDecimalsForToken` — unchanged, keeps returning 6 for input
fields.
- `getDisplayDecimalsForToken` — new. Returns 2 for stablecoins
(`isStableCoin`, plus an explicit id fallback covering Mento's COPm,
USDm, EURm, BRLm and non-Mento USDT/USDC), 6 otherwise (Oro etc).
Applied to GoldSellConfirmation ("Recibes" amount and network fee)
and GoldBuyConfirmation (network fee). The "Vendes" and "Precio del
Oro" lines already used the right formatting.
…als for stablecoins Follow-up to the confirmation-screen fix. The same getInputDecimalsForToken misuse was rendering the "Recibes" preview on GoldSellEnterAmount as "9,003.089097 Pesos" and the virtual-Dolares aggregated output on SwapScreen with 6 trailing zeros. Route both through getDisplayDecimalsForToken so stablecoin fields cap at 2. Sweep verified: GoldBuyEnterAmount's output field renders Oro (XAUT0_DECIMALS = 6, correct); other .toFormat(6) uses in GoldHome / BalanceCard / TabWallet are on gold balances (also correct).
…batches The mismatch check in sendPreparedTransactions is the last line of defence against dispatching a saga with mis-shaped tx state. Two callers were still vulnerable to the same class of race that broke gold buy last week: 1. `swap/saga.ts` (Squid swap): confirm screen gates on quote being populated, but stale-state races could still leave preparedTransactions as []. If it ever did, the saga always pushed one swap handler and blew up with "Mismatch in number of prepared transactions and standby transaction creators". Add the same early-return + Sentry business error we shipped for gold. 2. `jumpstart/saga.ts` (send-via-link): same shape. Add the empty guard before the handler pushes, wire captureBusinessError in the catch, and add a belt-and-suspenders check to the confirm button so it stays disabled if the route params are somehow empty. Also handle the reverse edge case in all three sagas (swap + gold buy + gold sell): if Squid returns MORE txs than we built handlers for (e.g. a multi-hop route with an intermediate step between approve and swap, or a 2-tx batch whose first tx is not a standard approve), pad the handlers array with null-returning entries before the trailing swap handler. The intermediate txs still go on-chain, they just don't generate standby entries the user sees.
…alora-kms chore(sentry): migrate off Valora KMS + enable Android sourcemaps + drop @sentry/types
Celo mainnet migrated from op-geth to op-reth on 2026-07-22 (https://forum.celo.org/t/13594). op-reth validates that `maxFeePerGas >= real baseFee + priority` at inclusion time more strictly than op-geth, and rejects the request as "Missing or invalid parameters" whenever the wallet's calculated max is too close to the current price. The previous shared Celo L2 path derived priority as `eth_gasPrice(feeCurrency) - block.baseFeePerGas`. Those two numbers are denominated in DIFFERENT units for CIP-64 non-native fee currency transactions (block base is CELO wei, gas price is fee-currency units), so the subtraction produced `priority ≈ gasPrice(fc)` and left `maxFee` only 2-3% above the current price. Under op-geth this usually worked; under op-reth it rejects the moment base fee moves up between estimation and inclusion. Split the CIP-64 case into `estimateCip64FeesPerGas` which treats `eth_gasPrice(feeCurrency)` as authoritative (units agree, no mixing) and builds a wide envelope: maxFeePerGas = gasPrice(fc) * 2 (100% headroom) maxPriorityFeePerGas = gasPrice(fc) / 4 (25% of current price) baseFeePerGas = gasPrice(fc) - priority (for UI display) The 100% headroom is deliberately generous: op-reth rejects on any borderline case, and the user is never actually charged above `realBaseFee + priority` regardless of maxFee. Trading a slightly overestimated ceiling for a lower rejection rate is the right call until Celo Foundation stabilises the op-reth CIP-64 validation tolerance. The native (non-CIP-64) path is unchanged. Test updated to reflect the new invariants — the block-header base fee must NOT leak into the CIP-64 result, and maxFee must exceed baseFee + priority in fee-currency units.
…and balance
Backend returns `dataProps.tvl` and computes `balance * priceUsd`
for a pool in whatever currency the deposit token represents. For
USD stablecoins (USDT, USDC, USDm, USAT) that value is in USD and
must be run through `useDollarsToLocalAmount` to display in the
user's local fiat. For local-currency Mento stablecoins (COPm,
EURm, BRLm, XOFm, GHSm, KESm), the value is ALREADY in the local
currency, so the same conversion multiplies by the USD->local rate
a second time - roughly 4000x for COP, producing "TVL COP$321,865,000"
where the real number is around COP$100,000 (321M / 4000 ≈ the 3218
COP/USD rate). Consumers had no way to distinguish the two, so every
COPm pool was inflated.
Approach:
- New helper `isLocalCurrencyStable(symbol)` in src/tokens/utils.ts
centralises the classification (Mento peso/euro/real/etc. family
plus legacy `cCOP`/`cEUR`/`cREAL` names still on historical
records). USD stablecoins deliberately excluded.
- `getEarnPositionBalanceValues` in src/earn/utils.ts now also
returns `isLocalCurrencyDenominated` derived from the deposit
token's symbol. Old return shape preserved so existing callers
keep compiling; new flag is optional to read.
- The 3 rendering call sites branch on the flag:
* PoolCard.tsx: pool balance card + TVL row
* poolInfoScreen/Cards.tsx: deposit balance card + TVL card
* EarnEntrypoint.tsx: total supplied summary (converts per pool
before summing so mixed portfolios stay honest)
- EarnConfirmationScreen already reads `poolBalanceInDepositToken`
(not the USD variant) so it was unaffected.
Not affected: XAUt0 (gold) which has real USD-denominated pricing;
CELO (native, always USD-priced); Aave / other USD-only integrations.
Tests: 13 new cases on the helper cover the whole Mento family and
their opposites; 280 existing earn tests still pass.
Verification plan on mainnet (backend prod, wallet
0x8427e4409b73a31b9d4e0d210677c88877472ece):
- Flexible: COP$100,000 (was COP$321,865,000)
- 30d: COP$0 (backend also fixed the empty aggregation)
- 60d: COP$80,000 (was COP$257,492,000)
- 90d: COP$20,000 (new tranche)
fix(tx): defense-in-depth for empty/oversized prepared tx batches (swap+gold+jumpstart)
…op-reth fix(viem): dedicated CIP-64 fee estimator with 100% headroom for op-reth
…-denominated fix(earn): stop double-converting TVL/balance for local-currency Mento pools (COPm et al)
… sentry.properties The postinstall script `populate_secrets.sh` reads SENTRY_AUTH_TOKEN and SENTRY_CLIENT_URL from env vars when there is no macOS Keychain (i.e. Linux/macOS CI runners). Without these the script warns and writes empty auth.token + empty DSN, which produces: - Android sentry.gradle upload step tries to POST sourcemaps with an empty auth token and Sentry rejects. - The shipped app has an empty SENTRY_CLIENT_URL baked in and reports nothing to Sentry at runtime. Repo secrets already added to https://github.com/TuCopFinance/TuCopWallet/settings/secrets/actions so the ${{ secrets.X }} references resolve at build time.
…ction
Backend team asked (2026-07-27) why their Railway logs show ZERO requests
during outage windows while wallet users see the hardcoded $3050 fallback.
The three server-side hypotheses (tight timeout, wrong URL, upstream CMC
outage) do not match wallet code: FETCH_TIMEOUT_DURATION = 15s,
GET_XAUT_PRICE_URL points at the correct proxy.
The missing hypothesis is client-side. The wallet has a per-host circuit
breaker (src/lib/circuitBreaker: 5 failures within 60s opens the breaker
for 30s). While open, fetchWithTimeout returns a synthetic Response with
statusText "Service Unavailable (circuit open)" WITHOUT ever leaving the
device. If unrelated Railway calls (positions, tx/status, feed) burn the
five-failure budget, subsequent /api/prices/xaut requests short-circuit
locally and backend never sees them.
Instrument the price fetch so this is observable in Sentry going forward:
- captureBusinessError in the primary (TuCop backend) and fallback (DIA)
catch blocks, with `feature: transactions, provider: internal,
action: fetch_gold_price_backend|_dia`. errorCode uses classifyHttpError
by default, but returns 'circuit_open' when the error message contains
the CIRCUIT_OPEN_MARKER literal. Backend can dashboard the
circuit_open bucket to know when the wallet aborted client-side vs
when they should look at their own logs.
- Sentry.setTag('gold_price_source', 'backend' | 'dia_data' |
'fallback_hardcoded') on the three success paths. Mirrors the existing
neeru_meta_source pattern. The fallback_hardcoded rate becomes a direct
operational health metric of the price feed.
No changes to timeout, URL, retry, or the circuit-breaker thresholds
themselves. The intent is observability first; if telemetry later shows
the breaker opening from unrelated calls, we can revisit whether the
gold-price fetch deserves its own bucket separate from the shared
host-level breaker.
Tests: 5 new cases cover each tag/errorCode branch, including the
circuit-open detection.
chore(ci): expose SENTRY_AUTH_TOKEN + SENTRY_CLIENT_URL to postinstall
feat(gold): Sentry telemetry + circuit-breaker detection on price fetch
…estimation error classification Two small pieces of tech debt from the recent Sentry / op-reth work: 1. README: the postinstall now runs scripts/populate_secrets.sh (replacing the deleted yarn keys:decrypt), but the README's Quick Start still told newcomers to `cp secrets.json.template secrets.json` and edit by hand. Documented the new mechanism (Keychain on dev laptops, env vars in CI), the exact `security add-generic-password` commands, and pointed CI to the repo secrets link. This is the missing dev-onboarding half of PR #285. 2. viem/prepareTransactions: `isInsufficientFundsError` was checking `InvalidInputRpcError` only when `.details` matched `/gas required exceeds allowance/` (op-geth wording). op-reth (Celo mainnet from 2026-07-22) surfaces the same RPC error code -32602 with `.details = "Missing or invalid parameters."`, so the old regex misses it and the fee-currency cascade throws instead of trying the next candidate. Widen to accept ANY InvalidInputRpcError as recoverable. Renamed the flag to `isRecoverableEstimationError` to reflect the actual semantics. Added a test case with the op-reth wording so future regressions fail loud. False-positive risk (a real unrecoverable error mis-classified as recoverable) is safe: it just retries with the next fee currency or uses the hardcoded ERC-20 fallback, and Logger.warn + the downstream captureBusinessError in the caller sagas keep the telemetry visible on Sentry.
Backend shipped (2026-07-27) a 24h stale-cache fallback on
GET /api/prices/xaut?vs=usd. Fresh path (TTL 60s) is unchanged. On
stale-serve the response body is identical but includes:
X-Stale: true
X-Stale-Age: <seconds>
Cache-Control: max-age=0, must-revalidate
Wire the wallet to surface that signal:
- src/gold/types.ts: GoldPriceData gains `isStale?` and
`staleAgeSeconds?`. Optional so DIA + hardcoded paths leave them
undefined.
- src/gold/api.ts fetchFromTucopBackend: read the two headers after
validating the body, propagate on the return.
- src/gold/api.ts fetchGoldPriceFromApi: on backend success branch on
`priceData.isStale`:
* fresh -> tag `gold_price_source = backend` (unchanged)
* stale -> tag `gold_price_source = backend_stale`
+ tag `gold_price_stale_age_bucket = <bucket>`
- Buckets: <5min, 5-15min, 15-60min, >1h. Prevents per-second
cardinality explosion on the Sentry side while still surfacing the
operational shape (fresh-ish reads vs hours-old).
DIA / hardcoded / circuit-breaker paths are unchanged: their tags stay
`dia_data` / `fallback_hardcoded` / `errorCode:circuit_open`
respectively. UI consumers (Task 3 in the follow-up chain) will read
`isStale + staleAgeSeconds` off the GoldPriceData to show a "cotización
desactualizada" badge when age > 5min.
Tests: 11 new cases (3 header-propagation, 8 bucketize edges) on top
of the existing 10, all pass. Bucketize edges specifically pin the
5-15min boundary (299s <5min, 300s 5-15min) so future refactors do not
silently drift the thresholds.
… not contaminate
The circuit breaker in src/lib/circuitBreaker keyed state by URL host.
That contaminated shared-host deployments: a burst of 502s on
`/hooks-api` on tucop-backend-production.up.railway.app would open the
breaker for the WHOLE host, causing subsequent calls to
`/api/prices/xaut`, `/api/positions`, `/api/tx/status` and
`/api/earn/neeru/catalogue` on the same host to short-circuit with a
synthetic 503 for 30s even though those endpoints were healthy.
Squid upstream returns legitimate 502s by design when the user runs
multiple quick quotes or hits low-liquidity paths, so it is easy to
burn the 5-failure/60s budget just by browsing swap. Backend
independently flagged this behaviour in the same thread that shipped
the price-proxy stale cache.
Switch the key to `${host}${pathname}`. Query string is intentionally
excluded so different params on the same route do not fragment the
failure counter.
- circuitBreaker.ts: rename param `host` -> `key` throughout with
updated docstrings. External API is unchanged (still
`shouldShortCircuit(k)`, `recordFailure(k)`, `recordSuccess(k)`).
- fetchWithTimeout.ts: replace `extractHost` with `extractCircuitKey`
that returns `${host}${pathname}`. Rename local `host` -> `circuitKey`
and update the retry breadcrumb wording. No caller signature
changes.
- 2 new tests pin the intended isolation: (a) `/hooks-api` opening does
not affect `/api/prices/xaut` on the same host, (b) `recordSuccess`
on one path does not clear the counter for another.
Existing 6 tests continue to pass unchanged because they used arbitrary
key strings, not literal hosts.
Note (not fixed here, on purpose): 4xx responses do not count against
the breaker. Backend's earlier note asking about 400/429 was based on
an outdated read of fetchWithTimeout; only response.status >= 500 and
caught network errors do. So this refactor is the only change needed
to fix the cross-endpoint contamination.
feat(gold): consume backend X-Stale header for stale-cache telemetry
Task 3 of the price-fix chain that started with #290 (Sentry telemetry) and #292 (X-Stale header consumption in GoldPriceData). When the TuCop backend serves the XAUt price from its 24h stale cache (upstream provider degraded), the previous PR now carries that signal through as `isStale + staleAgeSeconds` on GoldPriceData. This PR persists the two fields into the gold Redux slice and renders a "Cotización desactualizada" badge on the Gold buy amount screen so the user knows the price they are seeing is real but a few minutes old. Threshold: badge only shows when isStale && staleAgeSeconds > 300. Sub-5min stale is common at the fresh-cache eviction boundary and would flap the badge for essentially fresh data; the 5min bar matches the backend fresh TTL headroom. In practice this triggers only when the upstream provider is actually down long enough for the fresh cache to expire, not on normal refresh churn. Also fixes a small in-scope decimals bug: the "Disponible" balance line was hardcoded to `.toFormat(4)` producing "9,591.5614 Pesos". Route it through `getDisplayDecimalsForToken` so stablecoins render at 2 like the rest of the app. Slice additions (goldPriceIsStale, goldPriceStaleAgeSeconds): no persist version bump per the pre-release convention. autoMergeLevel2 fills the new keys from initialState on rehydrate of older persisted state, so existing users land on `false / 0` until the next successful price fetch. Screenshot approved by the user (2026-07-27, iOS iPhone 17 Pro sim) before merge; the temporary render-force was reverted in this commit. - src/gold/slice.ts: new state fields + reducer wiring - src/gold/selectors.ts: goldPriceIsStale + goldPriceStaleAgeSeconds - src/gold/GoldBuyEnterAmount.tsx: badge JSX + styles + `Disponible` decimals fix - locales/base + es-419: goldFlow.stalePriceBadge - test/RootStateSchema.json + test/schemas.ts + src/redux/store.test.ts: regen for the new fields
…fication chore(viem): document populate_secrets.sh and widen recoverable estimation error classification
…r-path refactor(circuit-breaker): key by host+path so unrelated endpoints do not contaminate
feat(gold): show stale-price badge when backend served from >5min cache
… baseline Release CI runs `yarn audit --level high` on PRs to main and compares the output to `yarn-audit-known-issues`. The file was empty, so the check enforced zero vulnerabilities on release. Two things needed to land the 1.118.8 promotion PR: 1. `ws@6.2.3` CVE-2026-48779 (HIGH, CVSS 7.5): memory-exhaustion DoS via tiny WebSocket fragments. Pulled transitively via `react-native>ws` and `react-native>@react-native/community-cli-plugin> @react-native/dev-middleware>ws`. Both paths are dev-server only (Metro debugging) and never shipped to production users, but the upgrade is trivial and cleaner than allowlisting. Add `"ws": "^6.2.4"` to resolutions to pull in the patched backport. 2. Regenerate `yarn-audit-known-issues` with the current 23 advisories from 5 modules (axios, brace-expansion, form-data, js-yaml, shell-quote). All are transitive from RN or twilio and require an attacker to already have code execution to matter. No user-facing runtime path. Verified locally with `bash scripts/ci_check_vulnerabilities.sh` -> "Ignoring known vulnerabilities". Each remaining advisory sits behind a dep TuCop does not own directly. Fixing them requires either upstream fixes in RN / twilio or forcing resolutions that risk breaking the parent (e.g. axios has a resolution already at ^1.15.2 but transitive paths through twilio still land on an older version). Cleanest to accept as baseline for this release and revisit when RN or twilio ship fixes.
# Conflicts: # yarn-audit-known-issues
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.
Summary
Cut Development → main para la release 1.118.8. 45 commits queued desde
1.118.7, agrupados por tema abajo.Version bump:
1.118.7→1.118.8(patch: todos los cambios son fixes o telemetría interna, cero features de usuario final ni breaking).package.json: 1.118.7 → 1.118.8android/gradle.properties: VERSION_CODE 1021081776 → 1021081777 (+1)android/app/build.gradle: versionName "1.118.7" → "1.118.8"ios/MobileStack.xcodeproj/project.pbxproj: CURRENT_PROJECT_VERSION 257 → 258 (×4 build configs), MARKETING_VERSION 1.118.7 → 1.118.8 (×2 build configs). NotificationService extension MARKETING_VERSION 1.0 sin tocar.Cambios incluidos (agrupados por tema)
Neeru vault integration (PRs #265-#283)
Cierre completo del roadmap de Neeru: zero-exposure runtime meta, tiered refactor de consumers, backend simulation-revert envelope, two-source revert cross-check, PoolCard con priceUsd real, catalogue en runtime, boot fetch, meta drift check, positions partialFailure honoring, fix Deposit topic0.
Sentry infra migration off Valora (PR #285, #289, #290, #292)
SENTRY_AUTH_TOKEN+SENTRY_CLIENT_URLexpuestos al postinstall.op-reth mitigations (PR #287, #291)
Celo mainnet migró de op-geth a op-reth 2026-07-22. Requiere manejo distinto de errores CIP-64:
estimateCip64FeesPerGascon 100% headroom en maxFee para no ser rechazado por op-reth por márgenes tight.isRecoverableEstimationErrorpara aceptar cualquierInvalidInputRpcError(op-reth surfaces "Missing or invalid parameters" en vez del "gas required exceeds allowance" de op-geth).Defense-in-depth transaction sagas (PR #286)
Empty guard + Sentry business error en swap / gold / jumpstart sagas: si
preparedTransactionsestá vacío al dispatchear, falla con mensaje amigable en vez del críptico "Mismatch in number of prepared transactions and standby transaction creators". Padding para casos multi-hop donde Squid devuelve 3+ txs.UX fixes
getDisplayDecimalsForToken.Neeru TVL fix (PR #288)
Los pools de Mento local-currency (COPm, EURm, BRLm, XOFm, GHSm, KESm) no deben pasar por
useDollarsToLocalAmount. Nuevo helperisLocalCurrencyStablecentraliza la classification. Bug fix visible: "Flexible TVL COP$321,865,000" (inflado 3218x) → "COP$110,000" (real).Circuit breaker per-endpoint (PR #293)
Rekeyed el circuit breaker de
${host}a${host}${pathname}. Bursts de 502s en/hooks-apiya no bloquean llamadas sanas a/api/prices/xautu otros paths del mismo host.Docs (PR #291)
README documenta el nuevo mecanismo de
populate_secrets.sh(Keychain en dev, env vars en CI). Reemplaza la referencia stale ayarn keys:decryptque dependía de la KMS de Valora.Pre-flight verificado
curl /hooks-api/getEarnPositions— los 4 pools devuelven TVLs sanos: Flexible 110k, 30d 0, 60d 80k, 90d 20k).Sin dependencies de release paralelas
Test plan post-merge
build.ymlverde en main.release=org.tucop@1.118.8+258.Follow-up conocido (NO en este release)
Target Android 16 / API 36 — deadline Google Play 2026-08-30. Roadmap interno: 2026-08-20. Descubierto durante audit de esta release, deliberadamente fuera de scope porque triggerea behaviour changes de Android 16 (edge-to-edge, predictive back, foreground services) que requieren regresión per-screen. Se abre branch dedicada
chore/target-sdk-36para 1.119.0 apenas 1.118.8 esté en producción.