--- name: add-mutation description: Use when adding, wrapping, or calling an @ecency/sdk mutation hook in the mobile app (Hive broadcasts like transfer, follow, vote, delegate, community, engine token, or points) argument-hint: [operation-name] --- # Add Mutation Wrap an `@ecency/sdk` mutation hook in `src/providers/sdk/mutations/`. CLAUDE.md ("SDK Migration") covers the adapter; this file is the procedure plus the Hive authority rules a wrapper must respect. ## 1. Create the wrapper `src/providers/sdk/mutations/useMutation.ts`. Most wrappers there are exactly this shape, so copy it verbatim: ```typescript import { useTransfer } from '@ecency/sdk'; import { useMutationAuth } from './common'; export function useTransferMutation() { const { username, authContext } = useMutationAuth(); return useTransfer(username, authContext, 'async'); } ``` - `'async'` is the broadcast mode, passed as the last positional arg after `authContext`. Its index varies by hook, so read the signature rather than assuming a position. Pass it unless the hook has no such parameter. `useBroadcastMutation` takes it as `{ broadcastMode: 'async' }` inside the options object instead. - `useMutationAuth()` from `./common.ts` (most wrappers import it) returns `{ username, authContext }`: `currentAccount?.name` off `selectCurrentAccount`, plus `useAuthContext()` (`src/providers/sdk/useAuthContext.ts`) building `{ adapter: createMobilePlatformAdapter({...}), enableFallback: true }`. There is no `mobilePlatformAdapter` object to import, use the factory. - Most wrappers take no arguments. `useSetCommunityRoleMutation(community)` plus `useUpdateCommunityMutation(community)` take the community because the SDK bakes it into the mutation key. `useAccountRelationsUpdateMutation(target, onSuccess, onError)` takes the target plus both callbacks because the SDK bakes them into the mutation options. - A few files are not broadcasts, so they skip `useMutationAuth`, despite CLAUDE.md still saying all mutation wrappers use it. `useGenerateImageMutation` plus the digest hooks in `useNewsletterDigestMutations.ts` (`useSubscribeDigestMutation`, `useLeaveDigestMutation`, `useUnsubscribeAllDigestsMutation`) bind the HiveSigner `code` from `useAuth()` (`src/hooks/useAuth.ts`): `useGenerateImage(username, code)`. `useClaimPointsMutation` instead derives the access token itself, decrypting `currentAccount.local.accessToken` with `getDigitPinCode(pin)` plus `decryptKey`. ## 2. Export from the barrel One line in `src/providers/sdk/mutations/index.ts`, under the matching domain comment: ```typescript export { useTransferMutation } from './useTransferMutation'; ``` ## 3. Call it (from the barrel) ```typescript import { useFollowMutation } from '../providers/sdk/mutations'; const followMutation = useFollowMutation(); await followMutation.mutateAsync({ following: data.following }); ``` ## No SDK hook for the operation? There is no `packages/sdk` here. `@ecency/sdk` is an npm dependency, so there is no local build step. Use the generic `useBroadcastMutation`, as `useIgnoreUserMutation.ts` does. The positional args: ```typescript return useBroadcastMutation( ['hive', 'ignore-user'], // 1 mutation key username, // 2 username ({ following }: { following: string }) => [ // 3 ops builder buildIgnoreOp(username!, following), ], undefined, // 4 onSuccess or undefined authContext, // 5 auth context 'posting', // 6 authority { broadcastMode: 'async' }, // 7 options ); ``` Pass authority as a plain lowercase string. The parameter is typed `AuthorityLevel` from `@ecency/sdk` (`'posting' | 'active' | 'owner' | 'memo'`); mobile wrappers use `'posting'` or `'active'`. Do not import the same-named type from `src/screens/dappBrowser/bridges/bridgeTypes.ts`, an unrelated dapp browser union. - `'posting'`: vote, comment, reblog, follow, ignore, community roles - `'active'`: transfer, delegate, power up/down, savings, limit orders, proposal vote, witness proxy The SDK's exported `OPERATION_AUTHORITY_MAP` is the reference list. It maps both `account_update` plus `account_update2` to `'active'` flatly, which is right for the common cases but wrong at both ends, so do not copy it for either one. Both account update operations have payload-dependent authority (`custom_json` is the third payload-dependent case, below): | Operation | Payload sets | Authority Hive requires | |---|---|---| | both | `owner` | `'owner'` | | `account_update2` | only `posting_json_metadata` (profile edit, pinned post) | `'posting'` | | both | anything else | `'active'` | The owner row covers BOTH versions. `src/utils/hiveOperationAuthority.ts` implements the posting row plus the active row for `account_update2` only. It does NOT implement the owner row for either version: the function is typed `(operation: Operation) => 'posting' | 'active'`, so its `owner` branch returns `'active'`, `account_update` has no branch at all, plus `hiveOperationAuthority.test.ts` has no owner case. Its doc comment calling v1 "correctly resolves to active" is wrong whenever the payload sets `owner`. That resolver serves the hive-uri path (`src/providers/hive/hive.ts` and `src/hooks/useLinkProcessor.tsx`), where the operations arrive from an external link. Treat an owner change as unsupported and reject it rather than routing it to `'active'`. `custom_json` is the third payload-dependent case. `required_auths` alone means active, `required_posting_auths` alone means posting. A payload populating BOTH is the case this app does not handle. `resolveTxRequiredAuthority` collapses a whole transaction to a single `'posting' | 'active'`, which `src/providers/hive/hive.ts` then turns into one decrypted key, posting or active. So treat a mixed payload as unsupported by this client and say so, rather than calling it malformed or picking one authority for it. Neither implementation detects the case today. `hiveOperationAuthority.ts` never reads `required_posting_auths` at all. The SDK's `getCustomJsonAuthority` returns `'active'` as soon as `required_auths` is non-empty, without checking the other list. Both therefore sign with one key. `useBroadcastMutation` takes `authority` as its sixth positional parameter, fixed when the hook is created, while `operations` is `(payload: T) => Operation[]` and only runs at mutate time. The mutation reads the closed-over value, so a wrapper cannot choose an authority from its payload. If both shapes are possible, write two hooks with fixed authorities and pick at the call site: ```typescript // posting: profile edit, pinned post, anything touching only posting_json_metadata export const useUpdateProfileMetadataMutation = () => { /* ..., 'posting' */ }; // active: json_metadata or a key or authority change other than owner (an owner // change is unsupported, reject it) export const useUpdateAccountKeysMutation = () => { /* ..., 'active' */ }; ``` `useBroadcastMutation`'s `authority` parameter defaults to `'posting'`, so pass the right value explicitly. Prefer an SDK `buildOp` helper (`buildTransferOp`, `buildVoteOp`) over a hand written op tuple. ## Gotchas 1. Auth is not your job: the adapter routes PIN key decryption, HiveSigner `hive-uri` WebView signing, HiveAuth signing, plus the active key upgrade sheet. 2. No toasts or navigation in the wrapper. Do that at the call site or in hook callbacks. 3. Check the hook exists in the installed `@ecency/sdk` first, then run `yarn lint` plus `yarn typecheck`; the baseline is empty, so any error fails CI.