---
name: debug
description: Diagnose a bug in the Ecency mobile app when a broadcast or login fails, a wallet or transfer amount is wrong, a screen or deep link does not open, a bottom sheet misbehaves, dark mode colors are wrong, an SDK query returns stale data, or a Metro or native build breaks
argument-hint: [issue-description]
---

# Debug Guide

Triage procedure. Layout, commands, architecture and test setup are in `CLAUDE.md`; this file
adds the per-area entry points plus the traps. Verify against the code before acting.

## 1. Auth / broadcast

`authType` is a **string**, not a number (`src/constants/authType.ts`):
`steemConnect`, `hiveAuth`, `masterKey`, `activeKey`, `memoKey`, `postingKey`, `ownerKey`.
`mapAuthTypeToLoginType` (`src/utils/authMapper.ts`) maps them to the SDK login type:

| `currentAccount.local.authType` | login type |
|---|---|
| `'steemConnect'` | `'hivesigner'` |
| `'hiveAuth'` | `'hiveauth'` |
| the key types above | `'key'` |
| anything else | `'key'` plus an `[AuthMapper] Unknown authType` warning |

(CLAUDE.md still describes `AUTH_TYPE` as numbers; the code uses the strings above.)

Routing is `src/providers/sdk/mobilePlatformAdapter.ts`. `getLoginType(username, authority)` can
override the map: a key user with no `postingKey` but an `accessToken` goes to HiveSigner. A
HiveSigner user asking for `active` returns `null`, so the SDK falls through to
`showAuthUpgradeUI`.

Authority per operation: `resolveOperationAuthority` / `resolveTxRequiredAuthority` in
`src/utils/hiveOperationAuthority.ts`. `vote`, `comment`, `comment_options`, `delete_comment`,
`claim_reward_balance` are posting outright. The payload dependent ops are checked before that
set: `custom_json` is posting unless it declares a non-empty `required_auths`; `account_update2`
is posting unless it sets a non-empty `json_metadata` or any of
`owner`/`active`/`posting`/`memo_key`. Everything else is active.

`resolveTxRequiredAuthority` then collapses a whole transaction to a single authority, returning
active when any one operation needs it, so the signer decrypts one key for the batch. That is
fine for a uniform transaction. It is wrong for a mixed one, meaning a batch holding both a
posting-only operation and an active operation. Hive's `verify_authority` checks the required
posting authorities, then calls `clear_approved()` before checking the required active ones, so
the two sets are satisfied independently and one active signature does not cover both. HF28
lifted the old ban on mixing them in a single transaction, so such a batch can now arrive from a
deep link where it previously could not. Mobile signs with one key, so treat a mixed batch as
unsupported by this client, the same as a mixed `custom_json`.

- **Active key gone right after upgrade**: `setTempActiveKey` expires it on a timer while
  `getActiveKey` calls `clearTempActiveKey()` on read, so it is single use.
- **HiveSigner WebView not opening**: `broadcastWithHiveSigner` calls
  `RootNavigation.navigate({ name: ROUTES.MODALS.HIVE_SIGNER, ... })`
  (`src/navigation/rootNavigation.tsx`).
- **HiveAuth not responding**: `broadcastWithHiveAuth` delegates to `handleHiveAuthFallback` in
  `src/providers/hive/hive.ts`. CLAUDE.md still points at `src/providers/hive/dhive.ts`; that file
  is gone and nothing imports `hive/dhive`. The fallback dedupes by
  `` `${name}:${operationName}` ``, so a concurrent call reuses the in-flight promise.
- **Auth upgrade sheet not showing**: `showAuthUpgradeUI` loads `SheetManager` plus `SheetNames`
  via `getSheetDeps()`, a cached lazy `require()` deliberately used instead of `import()`
  (which Metro wraps in an async shim), to dodge a circular import. Check that first.
- **"@ecency.app doesn't have permission to broadcast"**:
  `isMissingEcencyPostingAuthorityError` lowercases the error text then matches the substring
  `permission to broadcast`, or `unauthorized_client` together with an `ecency.app` mention; an
  `ecency.app` mention on its own matches neither branch. A bare `unauthorized_client` is an
  expired token or wrong scope. `shouldPromptPostingAuthority` gates the grant sheet.

## 2. Wallet / transfer

Screens `src/screens/transfer/screen/`; hooks `src/providers/queries/walletQueries/`, which
composes SDK options (`getPortfolioQueryOptions`, `getPointsQueryOptions`,
`get{Hive,Hbd,HivePower}AssetTransactionsQueryOptions`, `getOpenOrdersQueryOptions`,
`getRecurrentTransfersQueryOptions`, `getSavingsWithdrawFromQueryOptions`,
`getConversionRequestsQueryOptions`, `getCollateralizedConversionRequestsQueryOptions`).

- Delegations are `getVestingDelegationsQueryOptions(username, limit)` (`delegateScreen.tsx`,
  `src/screens/assetDetails/children/delegationsModal.tsx`). The SDK also exports
  `getHivePowerDelegatingsQueryOptions`, which mobile does not appear to use, so do not reach for
  it by name.
- **Shows 0 HP**: `vestsToHp(vests, hivePerMVests)` (`src/utils/conversions.ts`) returns `0` when
  either argument is falsy and runs `parseFloat(String(vests))`, so a raw number and
  `"1000000.000000 VESTS"` both work. Zero almost always means `hivePerMVests` was missing.
- **Stale delegations**: invalidate the exact
  `getVestingDelegationsQueryOptions(name, limit).queryKey`; a different `limit` is another key.
- **`[object Object]`**: RPC rejections are often not `Error` instances, so `String(error)`
  collapses them. See `src/components/upvotePopover/container/upvotePopover.tsx`.

## 3. Navigation

`src/navigation/`: `stackNavigator.tsx`, `drawerNavigator.tsx`, `botomTabNavigator.tsx` (spelling
is intentional), `appNavigator.tsx`, plus `rootNavigation.tsx` for non-React navigation.

- **Screen not found**: the route must be in `src/constants/routeNames.ts` *and* registered in one
  of the navigators. `stackNavigator.tsx` holds both `<MainStack.Screen>` and `<RootStack.Screen>`
  entries: the root stack mounts the main stack (`STACK.MAIN`, which renders
  `MainStackNavigator`) and registers routes beside it (`SCREENS.REGISTER`, `LOGIN`, `WELCOME`,
  `SCREENS.ACCOUNT_LIST`, `WEB_BROWSER`, `PINCODE`, `MODALS.POLL_WIZARD`, `MODALS.HIVE_SIGNER`),
  so grepping only for `MainStack` wrongly declares login, pincode, web browser and the HiveSigner
  modal unregistered. Remaining routes are the `<Tab.Screen>` entries in `botomTabNavigator.tsx`
  and `<Drawer.Screen name={ROUTES.SCREENS.FEED}>` in `drawerNavigator.tsx`.
- **Deep link dead**: `src/hooks/useLinkProcessor.tsx` returns `handleLink`, which dispatches to
  `_handleEcencyAuthTransferDeeplink`, `_handleEcencyLoginDeeplink`,
  `_handleEcencyTransferDeeplink`, `_handleHiveUri` (which defers to `_handleHiveUriTransaction`)
  or else `_handleDeepLink`. That last one runs `deepLinkParser` then navigates, falling back to
  `ROUTES.SCREENS.WEB_BROWSER` when nothing parses, so an unrecognised link looks like the in-app
  browser opening for no reason. Parsing is `src/utils/deepLinkParser.ts`, which has a co-located
  test to reproduce against.

## 4. Bottom sheets

Registry `src/navigation/sheets.tsx`: the `SheetNames` enum and the `registerSheet` calls line up
one-to-one.

- **Not opening**: the component must be imported into `sheets.tsx` and registered. It need not
  come from the `src/components/index.tsx` barrel; some registered sheets are imported by direct
  path instead, for example `SignConfirmSheet` from `src/screens/dappBrowser/components/`.
- **Stale data**: sheets unmount on hide (CLAUDE.md), so sheet state resets between shows. What a
  sheet renders is the payload captured when `SheetManager.show` ran, so re-show with fresh data.
- **Falsy result**: a sheet resolves with what it passes to
  `SheetManager.hide(sheetId, { payload: value })` (`src/components/authUpgradeSheet/`), so a
  falsy result does not mean confirmed. It also may not say why: `SignConfirmSheet` routes both
  its Cancel button and its `onClose`, which fires on a backdrop or gesture dismiss, through the
  same `_close(false)`, so `!ok` lumps an explicit reject in with a dismissal. Bail out on falsy;
  resolve a named field when the caller has to tell the two apart:
  `const ok = await SheetManager.show(SheetNames.SIGN_CONFIRM, { payload }); if (!ok) return;`
- A throw from a sheet render or cleanup is fatal: sheets sit outside the ErrorBoundary.

## 5. Theme

`react-native-extended-stylesheet` is built by
`EStyleSheet.build(isDarkTheme ? darkTheme : lightTheme)` inside a `useMemo` keyed on
`[isDarkTheme]` (`src/screens/application/hook/useInitApplication.tsx`), so it reruns when the
theme toggles. Stylesheet values therefore re-resolve; a value read outside a stylesheet can stay
stale. For those reads use `EStyleSheet.value('$theme') === 'darkTheme'`.

| Variable | Light | Dark |
|---|---|---|
| `$primaryBackgroundColor` | `#FFFFFF` | `#1e2835` |
| `$primaryLightBackground` | `#f6f6f6` | `#2e3d51` |
| `$primaryBlack` | `#3c4449` | `#fcfcfc` |
| `$primaryDarkText` | `#788187` | `#fcfcfc` |
| `$iconColor` | `#c1c5c7` | `#788187` |

`$primaryGray`, `$primaryLightGray`, `$primaryRed`, `$primaryGreen` are identical in both themes,
so switching to them fixes nothing. Bad dark mode colors usually mean a literal hex.

## 6. SDK queries

Config `src/providers/queries/sdk-config.ts` (`initSdkConfig`), client
`src/providers/queries/index.ts`.

- **No fetch**: check `enabled`; an undefined username usually disables the query.
- **Stale after a mutation**: the adapter's `invalidateQueries` takes a raw key or `{ queryKey }`
  and warns instead of throwing on failure, so a wrong key looks like success.
- **RPC errors**: the node pool reaches the SDK through `ConfigManager.setHiveNodes(...)`, which
  runs from more than one call site, so confirm which list won before blaming failover. Denied
  nodes are dropped by `withoutBlockedServers` / `isBlockedServer`
  (`src/constants/options/api.ts`), so check the pool too.

## 7. Build

```bash
bash patch-gradle.sh                       # gradle patch, also runs on install
cd android && ./gradlew clean && cd .. && yarn android
cd ios && pod install && cd .. && yarn ios
yarn start --reset-cache                   # Metro cache only
```

`yarn clear` deletes `node_modules` and reinstalls, so never run it in a shared or worktree
checkout. `yarn typecheck` runs `scripts/typecheck.js`, not bare `tsc`.

## Triage order

1. Reproduce, name the screen or component, find it under `src/screens/` or `src/components/`.
2. Reads: SDK query options to query hook to component.
3. Writes: `useMutationAuth()` to `mobilePlatformAdapter` to HiveSigner / HiveAuth / key.
4. User state: `useAppSelector(selectCurrentAccount)`.
5. Prefer a co-located Jest test over a manual repro; `src/utils/` already has suites.
