diff --git a/.cspell.json b/.cspell.json index 11b59d4b0..5241daf99 100644 --- a/.cspell.json +++ b/.cspell.json @@ -87,6 +87,7 @@ "TTFB", "Uids", "UNWRAPPER", + "vitalik", "vercel", "viem", "vnet", diff --git a/deployables/app/app/components/wallet/Connect.tsx b/deployables/app/app/components/wallet/Connect.tsx index 803fddee5..cf6b15cd5 100644 --- a/deployables/app/app/components/wallet/Connect.tsx +++ b/deployables/app/app/components/wallet/Connect.tsx @@ -5,7 +5,7 @@ import { useAccountEffect } from 'wagmi' export type OnConnectArgs = { providerType: ProviderType - account: HexAddress + address: HexAddress } type ConnectProps = { @@ -24,7 +24,7 @@ export const Connect = ({ onConnect }: ConnectProps) => { } onConnect({ - account: address, + address, providerType: connector.type === 'injected' ? ProviderType.InjectedWallet diff --git a/deployables/app/app/components/wallet/ConnectWallet.tsx b/deployables/app/app/components/wallet/ConnectWallet.tsx index 4e12f5fc2..245e1947c 100644 --- a/deployables/app/app/components/wallet/ConnectWallet.tsx +++ b/deployables/app/app/components/wallet/ConnectWallet.tsx @@ -104,7 +104,7 @@ const useAutoReconnect = ({ } onConnectRef.current({ - account: address, + address, providerType: connector.type === 'injected' ? ProviderType.InjectedWallet diff --git a/deployables/app/app/routes/edit-route.$data.tsx b/deployables/app/app/routes/edit-route.$data.tsx index 701c2b34e..41d837a5f 100644 --- a/deployables/app/app/routes/edit-route.$data.tsx +++ b/deployables/app/app/routes/edit-route.$data.tsx @@ -18,6 +18,8 @@ import { } from '@zodiac/form-data' import { CompanionAppMessageType } from '@zodiac/messages' import { + createAccount, + createEoaAccount, getRolesVersion, queryRolesV1MultiSend, queryRolesV2MultiSend, @@ -26,10 +28,10 @@ import { updateAvatar, updateChainId, updateLabel, - updatePilotAddress, updateProviderType, updateRoleId, updateRolesWaypoint, + updateStartingPoint, zodiacModuleSchema, type ZodiacModule, } from '@zodiac/modules' @@ -154,17 +156,23 @@ export const clientAction = async ({ case Intent.ConnectWallet: { const route = parseRouteData(params.data) - const account = getHexString(data, 'account') + const address = getHexString(data, 'address') const providerType = verifyProviderType(getInt(data, 'providerType')) + const account = await createAccount( + jsonRpcProvider(getChainId(route.avatar)), + address, + ) return editRoute( - updatePilotAddress(updateProviderType(route, providerType), account), + updateStartingPoint(updateProviderType(route, providerType), account), ) } case Intent.DisconnectWallet: { const route = parseRouteData(params.data) - return editRoute(updatePilotAddress(route, ZERO_ADDRESS)) + return editRoute( + updateStartingPoint(route, createEoaAccount({ address: ZERO_ADDRESS })), + ) } default: @@ -194,11 +202,11 @@ const EditRoute = ({ { + onConnect={({ address, providerType }) => { submit( formData({ intent: Intent.ConnectWallet, - account, + address, providerType, }), { method: 'POST' }, @@ -341,7 +349,7 @@ const useOptimisticRoute = () => { case Intent.ConnectWallet: { setOptimisticConnection({ - pilotAddress: getHexString(formData, 'account'), + pilotAddress: getHexString(formData, 'address'), }) } } diff --git a/packages/modules/src/createAccount.spec.ts b/packages/modules/src/createAccount.spec.ts new file mode 100644 index 000000000..9bade3676 --- /dev/null +++ b/packages/modules/src/createAccount.spec.ts @@ -0,0 +1,34 @@ +import { Chain } from '@zodiac/chains' +import { getDefaultProvider } from 'ethers' +import { AccountType, formatPrefixedAddress } from 'ser-kit' +import { describe, expect, it } from 'vitest' +import { createAccount } from './createAccount' + +describe('createAccount', () => { + const ethProvider = getDefaultProvider(1) + + it('creates an EAO account if the passed address does not have code', async () => { + const vitalikEoaAddress = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045' + + const account = await createAccount(ethProvider, vitalikEoaAddress) + + expect(account).toEqual({ + type: AccountType.EOA, + address: vitalikEoaAddress, + prefixedAddress: formatPrefixedAddress(undefined, vitalikEoaAddress), + }) + }) + it('creates a Safe account if the passed address has code', async () => { + const gnosisDaoTreasury = '0x849d52316331967b6ff1198e5e32a0eb168d039d' + + const account = await createAccount(ethProvider, gnosisDaoTreasury) + + expect(account).toEqual({ + type: AccountType.SAFE, + address: gnosisDaoTreasury, + prefixedAddress: formatPrefixedAddress(Chain.ETH, gnosisDaoTreasury), + threshold: NaN, + chain: Chain.ETH, + }) + }) +}) diff --git a/packages/modules/src/createAccount.ts b/packages/modules/src/createAccount.ts new file mode 100644 index 000000000..3830a739a --- /dev/null +++ b/packages/modules/src/createAccount.ts @@ -0,0 +1,22 @@ +import { verifyChainId } from '@zodiac/chains' +import type { HexAddress } from '@zodiac/schema' +import type { Provider } from 'ethers' +import { createEoaAccount } from './createEoaAccount' +import { createSafeAccount } from './createSafeAccount' + +export const createAccount = async ( + provider: Provider, + address: HexAddress, +) => { + const isEoa = (await provider.getCode(address)) === '0x' + const network = await provider.getNetwork() + + if (isEoa) { + return createEoaAccount({ address }) + } + + return createSafeAccount({ + address, + chainId: verifyChainId(Number(network.chainId)), + }) +} diff --git a/packages/modules/src/index.ts b/packages/modules/src/index.ts index 13898acc0..1c9bc0d16 100644 --- a/packages/modules/src/index.ts +++ b/packages/modules/src/index.ts @@ -1,4 +1,6 @@ +export { createAccount } from './createAccount' export { createBlankRoute } from './createBlankRoute' +export { createEoaAccount } from './createEoaAccount' export { createEoaStartingPoint } from './createEoaStartingPoint' export { fetchZodiacModules } from './fetchZodiacModules' export { getPilotAddress } from './getPilotAddress' @@ -16,10 +18,10 @@ export { decodeRoleKey, encodeRoleKey } from './roleKey' export { updateAvatar } from './updateAvatar' export { updateChainId } from './updateChainId' export { updateLabel } from './updateLabel' -export { updatePilotAddress } from './updatePilotAddress' export { updateProviderType } from './updateProviderType' export { updateRoleId } from './updateRoleId' export { updateRolesWaypoint } from './updateRolesWaypoint' +export { updateStartingPoint } from './updateStartingPoint' export { SUPPORTED_ZODIAC_MODULES, SupportedZodiacModuleType, diff --git a/packages/modules/src/updatePilotAddress.spec.ts b/packages/modules/src/updatePilotAddress.spec.ts deleted file mode 100644 index 8b68763d7..000000000 --- a/packages/modules/src/updatePilotAddress.spec.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { Chain } from '@zodiac/chains' -import { - createMockDelayWaypoint, - createMockEnabledConnection, - createMockEndWaypoint, - createMockExecutionRoute, - createMockOwnsConnection, - createMockRoleWaypoint, - createMockSafeAccount, - createMockStartingWaypoint, - createMockWaypoints, - randomAddress, -} from '@zodiac/test-utils' -import { AccountType, formatPrefixedAddress } from 'ser-kit' -import { describe, expect, it } from 'vitest' -import { getStartingWaypoint } from './getStartingWaypoint' -import { getWaypoints } from './getWaypoints' -import { updatePilotAddress } from './updatePilotAddress' - -describe('updatePilotAddress', () => { - it('updates the starting waypoint', () => { - const route = createMockExecutionRoute() - - const newAddress = randomAddress() - - const updatedRoute = updatePilotAddress(route, newAddress) - const startingPoint = getStartingWaypoint(updatedRoute.waypoints) - - expect(startingPoint.account).toHaveProperty('address', newAddress) - expect(startingPoint.account).toHaveProperty( - 'prefixedAddress', - formatPrefixedAddress(undefined, newAddress), - ) - }) - - it('updates the initiator field', () => { - const route = createMockExecutionRoute() - - const newAddress = randomAddress() - - const updatedRoute = updatePilotAddress(route, newAddress) - - expect(updatedRoute).toHaveProperty( - 'initiator', - formatPrefixedAddress(undefined, newAddress), - ) - }) - - it.each([ - [AccountType.ROLES, createMockRoleWaypoint()], - [AccountType.DELAY, createMockDelayWaypoint()], - [ - AccountType.SAFE, - createMockEndWaypoint({ connection: createMockOwnsConnection() }), - ], - ])('it updates the connection for "%s" waypoints', (_, waypoint) => { - const route = createMockExecutionRoute({ - waypoints: createMockWaypoints({ - start: createMockStartingWaypoint(createMockSafeAccount()), - waypoints: [waypoint], - }), - }) - - const newAddress = randomAddress() - - const updatedRoute = updatePilotAddress(route, newAddress) - - const [updatedWaypoint] = getWaypoints(updatedRoute) - - expect(updatedWaypoint.connection).toHaveProperty( - 'from', - formatPrefixedAddress(Chain.ETH, newAddress), - ) - }) - - it('does not update Safe waypoints that have a IS_ENABLED relation', () => { - const roleWaypoint = createMockRoleWaypoint() - - const route = createMockExecutionRoute({ - waypoints: createMockWaypoints({ - waypoints: [roleWaypoint], - end: createMockEndWaypoint({ - connection: createMockEnabledConnection( - roleWaypoint.account.prefixedAddress, - ), - }), - }), - }) - - const updatedRoute = updatePilotAddress(route, randomAddress()) - - const [, updatedWaypoint] = getWaypoints(updatedRoute) - - expect(updatedWaypoint.connection).toHaveProperty( - 'from', - roleWaypoint.account.prefixedAddress, - ) - }) - - describe('EOA', () => { - it('handles EOA addresses in the safe endpoint', () => { - const route = createMockExecutionRoute({ - waypoints: createMockWaypoints({ - end: createMockEndWaypoint({ - connection: createMockOwnsConnection( - formatPrefixedAddress(undefined, randomAddress()), - ), - }), - }), - }) - - const address = randomAddress() - - const updatedRoute = updatePilotAddress(route, address) - const [endPoint] = getWaypoints(updatedRoute) - - expect(endPoint.connection).toHaveProperty( - 'from', - formatPrefixedAddress(undefined, address), - ) - }) - }) -}) diff --git a/packages/modules/src/updatePilotAddress.ts b/packages/modules/src/updatePilotAddress.ts deleted file mode 100644 index 3692db677..000000000 --- a/packages/modules/src/updatePilotAddress.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { ExecutionRoute, HexAddress, Waypoint } from '@zodiac/schema' -import { - AccountType, - ConnectionType, - formatPrefixedAddress, - splitPrefixedAddress, -} from 'ser-kit' -import { getStartingWaypoint } from './getStartingWaypoint' -import { getWaypoints } from './getWaypoints' -import { updateConnection } from './updateConnection' -import { updateStartingWaypoint } from './updateStartingWaypoint' - -export const updatePilotAddress = ( - route: ExecutionRoute, - address: HexAddress, -): ExecutionRoute => { - const startingPoint = getStartingWaypoint(route.waypoints) - const [chainId] = splitPrefixedAddress(startingPoint.account.prefixedAddress) - const waypoints = getWaypoints(route) - - return { - ...route, - - initiator: formatPrefixedAddress(chainId, address), - waypoints: [ - updateStartingWaypoint(startingPoint, { address }), - ...waypoints.map((waypoint) => updateWaypoint(waypoint, address)), - ], - } -} - -const updateWaypoint = ( - { account, connection }: Waypoint, - address: HexAddress, -): Waypoint => { - if (account.type === AccountType.SAFE) { - if (connection.type === ConnectionType.IS_ENABLED) { - return { account, connection } - } - } - - return { - account, - connection: updateConnection(connection, { from: address }), - } -} diff --git a/packages/modules/src/updateStartingPoint.spec.ts b/packages/modules/src/updateStartingPoint.spec.ts new file mode 100644 index 000000000..641608bc4 --- /dev/null +++ b/packages/modules/src/updateStartingPoint.spec.ts @@ -0,0 +1,75 @@ +import { + createMockDelayWaypoint, + createMockEndWaypoint, + createMockEoaAccount, + createMockExecutionRoute, + createMockRoleWaypoint, + createMockSafeAccount, + createMockStartingWaypoint, + createMockWaypoints, +} from '@zodiac/test-utils' +import { describe, expect, it } from 'vitest' +import { getStartingWaypoint } from './getStartingWaypoint' +import { getWaypoints } from './getWaypoints' +import { updateStartingPoint } from './updateStartingPoint' + +describe('updateStartingPoint', () => { + it('updates the starting waypoint', () => { + const route = createMockExecutionRoute() + + const newAccount = createMockEoaAccount() + + const updatedRoute = updateStartingPoint(route, newAccount) + const startingPoint = getStartingWaypoint(updatedRoute.waypoints) + + expect(startingPoint.account).toEqual(newAccount) + }) + + it('updates the initiator field', () => { + const route = createMockExecutionRoute() + + const newAccount = createMockEoaAccount() + + const updatedRoute = updateStartingPoint(route, newAccount) + + expect(updatedRoute).toHaveProperty('initiator', newAccount.prefixedAddress) + }) + + it('updates the connection of the waypoint connected to the starting point', () => { + const route = createMockExecutionRoute({ + waypoints: createMockWaypoints({ + start: createMockStartingWaypoint(createMockSafeAccount()), + waypoints: [createMockRoleWaypoint(), createMockEndWaypoint()], + }), + }) + const newAccount = createMockEoaAccount() + const updatedRoute = updateStartingPoint(route, newAccount) + + const [updatedWaypoint] = getWaypoints(updatedRoute) + + expect(updatedWaypoint.connection).toHaveProperty( + 'from', + newAccount.prefixedAddress, + ) + }) + + it('leaves other waypoints unchanged', () => { + const delayWaypoint = createMockDelayWaypoint() + const endWaypoint = createMockEndWaypoint() + const route = createMockExecutionRoute({ + waypoints: createMockWaypoints({ + start: createMockStartingWaypoint(createMockSafeAccount()), + waypoints: [createMockRoleWaypoint(), delayWaypoint, endWaypoint], + }), + }) + + const newAccount = createMockEoaAccount() + const updatedRoute = updateStartingPoint(route, newAccount) + + const [, updatedDelayWaypoint, updatedEndWaypoint] = + getWaypoints(updatedRoute) + + expect(updatedDelayWaypoint).toEqual(delayWaypoint) + expect(updatedEndWaypoint).toEqual(endWaypoint) + }) +}) diff --git a/packages/modules/src/updateStartingPoint.ts b/packages/modules/src/updateStartingPoint.ts new file mode 100644 index 000000000..da228c38e --- /dev/null +++ b/packages/modules/src/updateStartingPoint.ts @@ -0,0 +1,40 @@ +import type { ExecutionRoute, Waypoint } from '@zodiac/schema' +import { type PrefixedAddress, type StartingPoint } from 'ser-kit' +import { getWaypoints } from './getWaypoints' + +export const updateStartingPoint = ( + route: ExecutionRoute, + account: StartingPoint['account'], +): ExecutionRoute => { + const [nextWaypoint, ...otherWaypoints] = getWaypoints(route) + + if (nextWaypoint == null) { + return { + ...route, + + initiator: account.prefixedAddress, + waypoints: [{ account }], + } + } + + return { + ...route, + + initiator: account.prefixedAddress, + waypoints: [ + { account }, + updateWaypointConnection(nextWaypoint, account.prefixedAddress), + ...otherWaypoints, + ], + } +} + +const updateWaypointConnection = ( + waypoint: Waypoint, + connectedFrom: PrefixedAddress, +): Waypoint => { + return { + ...waypoint, + connection: { ...waypoint.connection, from: connectedFrom }, + } +}