diff --git a/app/src/app/(drawer)/accounts/create.tsx b/app/src/app/(drawer)/accounts/create.tsx index e0946c983..9da299b53 100644 --- a/app/src/app/(drawer)/accounts/create.tsx +++ b/app/src/app/(drawer)/accounts/create.tsx @@ -1,6 +1,5 @@ import { useRouter } from 'expo-router'; import { gql } from '@api/generated'; -import { useApproverAddress } from '@network/useApprover'; import { useForm } from 'react-hook-form'; import { View } from 'react-native'; import { useMutation } from 'urql'; @@ -15,6 +14,8 @@ import { Address } from 'lib'; import { Text } from 'react-native-paper'; import { AccountNameFormField } from '~/components/fields/AccountNameFormField'; import { createStyles } from '@theme/styles'; +import { usePolicyPresets } from '~/lib/policy/presets'; +import { asPolicyInput } from '~/lib/policy/draft'; const Create = gql(/* GraphQL */ ` mutation CreateAccountScreen_Create($input: CreateAccountInput!) { @@ -35,8 +36,8 @@ export interface CreateAccountScreenProps { function CreateAccountScreen({ onCreate }: CreateAccountScreenProps) { const router = useRouter(); - const approver = useApproverAddress(); const create = useMutation(Create)[1]; + const presets = usePolicyPresets(undefined); const { control, handleSubmit } = useForm({ defaultValues: { label: '' }, @@ -60,26 +61,16 @@ function CreateAccountScreen({ onCreate }: CreateAccountScreenProps) { style={styles.button} control={control} onPress={handleSubmit(async ({ label }) => { - try { - const account = ( - await create({ - input: { - label, - policies: [{ name: 'High risk', approvers: [approver] }], - }, - }) - ).data?.createAccount; + const r = await create({ input: { label, policies: [asPolicyInput(presets.high)] } }); - if (onCreate) { - onCreate(account!.address); - } else { - router.push({ - pathname: `/(drawer)/[account]/(home)/`, - params: { account: account!.address }, - }); - } - } catch (error) { - showError('Failed to create account', { event: { error } }); + const account = r.data?.createAccount.address; + if (!account) + return showError('Failed to create account', { event: { error: r.error } }); + + if (onCreate) { + onCreate(account); + } else { + router.push({ pathname: `/(drawer)/[account]/(home)/`, params: { account } }); } })} > diff --git a/app/src/components/policy/ActionsSettings.tsx b/app/src/components/policy/ActionsSettings.tsx index 0c5cf91eb..7f01ccdb0 100644 --- a/app/src/components/policy/ActionsSettings.tsx +++ b/app/src/components/policy/ActionsSettings.tsx @@ -1,4 +1,4 @@ -import { AddIcon, CustomActionIcon, materialIcon } from '@theme/icons'; +import { CustomActionIcon, materialIcon } from '@theme/icons'; import { createStyles } from '@theme/styles'; import { Divider, Switch } from 'react-native-paper'; import Collapsible from 'react-native-collapsible'; @@ -8,7 +8,7 @@ import { ListItemHorizontalTrailing } from '~/components/list/ListItemHorizontal import { ListItemTrailingText } from '~/components/list/ListItemTrailingText'; import { useToggle } from '~/hooks/useToggle'; import { PolicyDraftAction, usePolicyDraftState } from '~/lib/policy/draft'; -import { getActionPresets } from '~/lib/policy/presets'; +import { ACTION_PRESETS } from '~/lib/policy/presets'; export const INTERNAL_ACTION_LABEL_PREFIX = '__app__: '; @@ -21,10 +21,10 @@ export interface ActionsSettingsProps { } export function ActionsSettings(props: ActionsSettingsProps) { - const [{ account, actions }, update] = usePolicyDraftState(); + const [{ actions }, update] = usePolicyDraftState(); const [expanded, toggleExpanded] = useToggle(props.initiallyExpanded); - const actionPresets = Object.values(getActionPresets(account)); + const actionPresets = Object.values(ACTION_PRESETS); const defaultAllow = actions.find(isDefaultAllowAction)?.allow; const allowedExplicitActions = actions.filter((a) => a.allow && !isDefaultAllowAction(a)).length; diff --git a/app/src/components/policy/PolicySuggestions.tsx b/app/src/components/policy/PolicySuggestions.tsx index 219c4ea21..f01fd6867 100644 --- a/app/src/components/policy/PolicySuggestions.tsx +++ b/app/src/components/policy/PolicySuggestions.tsx @@ -5,12 +5,12 @@ import { useState } from 'react'; import { View } from 'react-native'; import { Chip, Text } from 'react-native-paper'; import { POLICY_DRAFT_ATOM } from '~/lib/policy/draft'; -import { getPolicyPresets } from '~/lib/policy/presets'; +import { usePolicyPresets } from '~/lib/policy/presets'; const Account = gql(/* GraphQL */ ` fragment PolicySuggestions_Account on Account { id - ...getPolicyTemplate_Account + ...getPolicyPresets_Account } `); @@ -20,7 +20,7 @@ export interface PolicySuggestionsProps { export function PolicySuggestions(props: PolicySuggestionsProps) { const account = useFragment(Account, props.account); - const presets = getPolicyPresets(account); + const presets = usePolicyPresets(account); const policy = useAtomValue(POLICY_DRAFT_ATOM); const setDraft = useSetAtom(POLICY_DRAFT_ATOM); diff --git a/app/src/hooks/useHydratePolicyDraft.ts b/app/src/hooks/useHydratePolicyDraft.ts index 6dc5068f3..29bcaa9d8 100644 --- a/app/src/hooks/useHydratePolicyDraft.ts +++ b/app/src/hooks/useHydratePolicyDraft.ts @@ -4,13 +4,13 @@ import { useHydrateAtoms } from 'jotai/utils'; import { ZERO_ADDR } from 'lib'; import { useEffect, useMemo } from 'react'; import { POLICY_DRAFT_ATOM, PolicyDraft, policyAsDraft } from '~/lib/policy/draft'; -import { getPolicyPresets } from '~/lib/policy/presets'; +import { usePolicyPresets } from '~/lib/policy/presets'; const Account = gql(/* GraphQL */ ` fragment useHydratePolicyDraft_Account on Account { id address - ...getPolicyTemplate_Account + ...getPolicyPresets_Account } `); @@ -39,6 +39,7 @@ export interface UseHydratePolicyDraftParams { export function useHydratePolicyDraft(params: UseHydratePolicyDraftParams) { const account = useFragment(Account, params.account); const policy = useFragment(Policy, params.policy); + const presets = usePolicyPresets(account); const init = useMemo( (): PolicyDraft => ({ @@ -48,14 +49,7 @@ export function useHydratePolicyDraft(params: UseHydratePolicyDraftParams) { ...((params.view === 'state' && policy?.state && policyAsDraft(policy.state)) || (policy?.draft && policyAsDraft(policy.draft)) || (policy?.state && policyAsDraft(policy.state)) || - (account - ? getPolicyPresets(account).high - : { - approvers: new Set(), - threshold: 0, - actions: [], - transfers: { defaultAllow: false, limits: {} }, - })), + presets.low), }), [account, policy, params.view], ); diff --git a/app/src/lib/policy/draft.ts b/app/src/lib/policy/draft.ts index 6c3c617ec..0575ac69b 100644 --- a/app/src/lib/policy/draft.ts +++ b/app/src/lib/policy/draft.ts @@ -85,7 +85,7 @@ export function policyAsDraft( }; } -export function asPolicyInput(p: PolicyDraft): PolicyInput { +export function asPolicyInput(p: Omit): PolicyInput { return { key: p.key, name: p.name, diff --git a/app/src/lib/policy/presets.ts b/app/src/lib/policy/presets.ts index a228f0b64..f64b27510 100644 --- a/app/src/lib/policy/presets.ts +++ b/app/src/lib/policy/presets.ts @@ -13,29 +13,32 @@ import _ from 'lodash'; import { FC } from 'react'; import { getAbiItem, getFunctionSelector } from 'viem'; import { SYNCSWAP_ROUTER } from '~/util/swap/syncswap/contracts'; +import { useApproverAddress } from '@network/useApprover'; -export const getActionPresets = (account: Address) => - ({ - all: { - icon: materialIcon('circle'), - label: 'Anything else', - functions: [{}], - }, - transferNfts: { - icon: imageFromSource(require('assets/ENS.svg')), - label: 'Transfer NFTs', - functions: [ - getAbiItem({ abi: ERC721_ABI, name: 'safeTransferFrom', args: ['0x', '0x', 0n] }), - getAbiItem({ abi: ERC721_ABI, name: 'safeTransferFrom', args: ['0x', '0x', 0n, '0x'] }), - getAbiItem({ abi: ERC721_ABI, name: 'transferFrom' }), - getAbiItem({ abi: ERC721_ABI, name: 'approve' }), - getAbiItem({ abi: ERC721_ABI, name: 'setApprovalForAll' }), - ].map((f) => ({ selector: asSelector(getFunctionSelector(f)) })), - }, - manageAccount: { - icon: AccountIcon, - label: 'Manage account', - functions: [ +type ActionDefinition = Omit & { icon?: FC }; + +export const ACTION_PRESETS = { + all: { + icon: materialIcon('circle'), + label: 'Anything else', + functions: [{}], + }, + transferNfts: { + icon: imageFromSource(require('assets/ENS.svg')), + label: 'Transfer NFTs', + functions: [ + getAbiItem({ abi: ERC721_ABI, name: 'safeTransferFrom', args: ['0x', '0x', 0n] }), + getAbiItem({ abi: ERC721_ABI, name: 'safeTransferFrom', args: ['0x', '0x', 0n, '0x'] }), + getAbiItem({ abi: ERC721_ABI, name: 'transferFrom' }), + getAbiItem({ abi: ERC721_ABI, name: 'approve' }), + getAbiItem({ abi: ERC721_ABI, name: 'setApprovalForAll' }), + ].map((f) => ({ selector: asSelector(getFunctionSelector(f)) })), + }, + manageAccount: { + icon: AccountIcon, + label: 'Manage account', + functions: (account: Address) => + [ getAbiItem({ abi: ACCOUNT_ABI, name: 'addPolicy' }), getAbiItem({ abi: ACCOUNT_ABI, name: 'removePolicy' }), getAbiItem({ abi: ACCOUNT_ABI, name: 'upgradeTo' }), @@ -44,39 +47,45 @@ export const getActionPresets = (account: Address) => contract: account, selector: asSelector(getFunctionSelector(f)), })), - }, - syncswapSwap: { - icon: SwapIcon, - label: 'Swap (SyncSwap)', - functions: [ - getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'swap' }), - getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'swapWithPermit' }), - ].map((f) => ({ - contract: SYNCSWAP_ROUTER.address, - selector: asSelector(getFunctionSelector(f)), - })), - }, - syncswapLiquidity: { - icon: materialCommunityIcon('water'), - label: 'Manage liquidity (SyncSwap)', - functions: [ - getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'addLiquidity' }), - getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'addLiquidity2' }), - getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'addLiquidityWithPermit' }), - getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'addLiquidityWithPermit2' }), - getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'burnLiquidity' }), - getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'burnLiquiditySingle' }), - getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'burnLiquiditySingleWithPermit' }), - getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'burnLiquidityWithPermit' }), - ].map((f) => ({ - contract: SYNCSWAP_ROUTER.address, - selector: asSelector(getFunctionSelector(f)), - })), - }, - }) satisfies Record & { icon?: FC }>; + }, + syncswapSwap: { + icon: SwapIcon, + label: 'Swap (SyncSwap)', + functions: [ + getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'swap' }), + getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'swapWithPermit' }), + ].map((f) => ({ + contract: SYNCSWAP_ROUTER.address, + selector: asSelector(getFunctionSelector(f)), + })), + }, + syncswapLiquidity: { + icon: materialCommunityIcon('water'), + label: 'Manage liquidity (SyncSwap)', + functions: [ + getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'addLiquidity' }), + getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'addLiquidity2' }), + getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'addLiquidityWithPermit' }), + getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'addLiquidityWithPermit2' }), + getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'burnLiquidity' }), + getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'burnLiquiditySingle' }), + getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'burnLiquiditySingleWithPermit' }), + getAbiItem({ abi: SYNCSWAP_ROUTER.abi, name: 'burnLiquidityWithPermit' }), + ].map((f) => ({ + contract: SYNCSWAP_ROUTER.address, + selector: asSelector(getFunctionSelector(f)), + })), + }, +} satisfies Record< + string, + | ActionDefinition + | (Omit & { + functions: (...params: any[]) => ActionDefinition['functions']; + }) +>; const Account = gql(/* GraphQL */ ` - fragment getPolicyTemplate_Account on Account { + fragment getPolicyPresets_Account on Account { id address approvers { @@ -86,11 +95,11 @@ const Account = gql(/* GraphQL */ ` } `); -export function getPolicyPresets(accountFragment: FragmentType) { +export function usePolicyPresets(accountFragment: FragmentType | null | undefined) { const account = getFragment(Account, accountFragment); - const actions = getActionPresets(account.address); + const approver = useApproverAddress(); - const approvers = new Set(account.approvers.map((a) => a.address)); + const approvers = new Set([approver, ...(account?.approvers.map((a) => a.address) ?? [])]); return { low: { @@ -98,9 +107,14 @@ export function getPolicyPresets(accountFragment: FragmentType) approvers, threshold: 1, actions: [ - { ...actions.syncswapSwap, allow: true }, - { ...actions.all, allow: false }, - ], + account && { + ...ACTION_PRESETS.manageAccount, + functions: ACTION_PRESETS.manageAccount.functions(account.address), + allow: false, + }, + { ...ACTION_PRESETS.syncswapSwap, allow: true }, + { ...ACTION_PRESETS.all, allow: false }, + ].filter(Boolean), transfers: { defaultAllow: false, limits: {} }, // TODO: allow transfers up to $x }, medium: { @@ -108,10 +122,15 @@ export function getPolicyPresets(accountFragment: FragmentType) approvers, threshold: Math.max(approvers.size > 3 ? 3 : 2, approvers.size), actions: [ - { ...actions.syncswapSwap, allow: true }, - { ...actions.syncswapLiquidity, allow: true }, - { ...actions.all, allow: true }, - ], + account && { + ...ACTION_PRESETS.manageAccount, + functions: ACTION_PRESETS.manageAccount.functions(account.address), + allow: false, + }, + { ...ACTION_PRESETS.syncswapSwap, allow: true }, + { ...ACTION_PRESETS.syncswapLiquidity, allow: true }, + { ...ACTION_PRESETS.all, allow: true }, + ].filter(Boolean), transfers: { defaultAllow: false, limits: {} }, // TODO: allow transfers up to $y }, high: { @@ -119,11 +138,15 @@ export function getPolicyPresets(accountFragment: FragmentType) approvers, threshold: _.clamp(approvers.size - 2, 1, 5), actions: [ - { ...actions.manageAccount, allow: true }, - { ...actions.syncswapSwap, allow: true }, - { ...actions.syncswapLiquidity, allow: true }, - { ...actions.all, allow: true }, - ], + account && { + ...ACTION_PRESETS.manageAccount, + functions: ACTION_PRESETS.manageAccount.functions(account.address), + allow: true, + }, + { ...ACTION_PRESETS.syncswapSwap, allow: true }, + { ...ACTION_PRESETS.syncswapLiquidity, allow: true }, + { ...ACTION_PRESETS.all, allow: true }, + ].filter(Boolean), transfers: { defaultAllow: true, limits: {} }, }, } satisfies Record>;