Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pilot-app): select token to transfer #699

Merged
merged 35 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
9026d69
also include nested routes in test renderer
frontendphil Jan 29, 2025
f81748f
extract token balance logic into module
frontendphil Jan 29, 2025
f2d5bc7
add test utility to create mock token balances
frontendphil Jan 29, 2025
aa5acd2
wip mock wagmi
frontendphil Jan 29, 2025
979f86c
make it possible to mock a wallet connection in tests
frontendphil Jan 30, 2025
ed2e1fe
add a connectWallet method that warns if the preconditions do not loo…
frontendphil Jan 30, 2025
6e43834
failing test plus new useTokenBalances hook
frontendphil Jan 30, 2025
ce2b5e0
do not mix server and client code
frontendphil Jan 30, 2025
9536d4e
use a balance type that can be serialsed
frontendphil Jan 30, 2025
ba7789a
update spellchecker
frontendphil Jan 30, 2025
63adfa2
TokenValueInput writes value as string representation of bigint
frontendphil Jan 30, 2025
5260bbb
connectWallet helper returns more stuff
frontendphil Jan 30, 2025
8e93c29
add failing test for transaction
frontendphil Jan 30, 2025
e66820c
refactor select so that option rendering can be changed easier
frontendphil Jan 30, 2025
c74939c
further inline select style improvements
frontendphil Jan 30, 2025
6e8c7d0
render tokens nicely
frontendphil Jan 30, 2025
eeb7434
add error boundary for send route
frontendphil Jan 30, 2025
a61fd9e
update spec
frontendphil Feb 3, 2025
54be63c
show max balance
frontendphil Feb 3, 2025
9274e5c
make it possible to send a token
frontendphil Feb 3, 2025
45aa5f4
adjust send specs
frontendphil Feb 3, 2025
7d96e99
adjust edit-route specs
frontendphil Feb 3, 2025
444f0f0
adjust edit-route specs more
frontendphil Feb 3, 2025
0b76858
maybe make send spec work on ci
frontendphil Feb 3, 2025
9d25859
maybe solve another timing issue
frontendphil Feb 3, 2025
33f56fb
fix lockfile
frontendphil Feb 3, 2025
cc6bbac
trying stuff
frontendphil Feb 3, 2025
7c26797
debug logging
frontendphil Feb 3, 2025
b24d028
more logging and fixed connector id
frontendphil Feb 3, 2025
4b19b7a
more logging
frontendphil Feb 3, 2025
7c5d851
try another transport
frontendphil Feb 3, 2025
cee2d8c
try another thing before bailing
frontendphil Feb 3, 2025
49fc29f
add waitFor
frontendphil Feb 3, 2025
0b20523
remove logging
frontendphil Feb 3, 2025
c2fac2e
skip send test on ci
frontendphil Feb 3, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"Sorhus",
"Sourcify",
"speculationrules",
"spinbutton",
"staderlabs",
"Stakewise",
"Sushiswap",
Expand Down
2 changes: 2 additions & 0 deletions deployables/app/app/balances/client/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type { BalanceResult, TokenBalance } from '../types'
export { useTokenBalances } from './useTokenBalances'
19 changes: 19 additions & 0 deletions deployables/app/app/balances/client/useTokenBalances.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useEffect } from 'react'
import { useFetcher } from 'react-router'
import { useAccount } from 'wagmi'
import type { BalanceResult } from '../types'

export const useTokenBalances = () => {
const { address, chainId } = useAccount()
const { load, data = [], state } = useFetcher<BalanceResult>()

useEffect(() => {
if (address == null || chainId == null) {
return
}

load(`/${address}/${chainId}/balances`)
}, [address, chainId, load])

return [data, state] as const
}
30 changes: 30 additions & 0 deletions deployables/app/app/balances/server/getTokenBalances.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { ChainId } from '@zodiac/chains'
import { getMoralisApiKey } from '@zodiac/env'
import type { HexAddress } from '@zodiac/schema'
import Moralis from 'moralis'
import type { Ref } from 'react'
import type { BalanceResult } from '../types'

const startedRef: Ref<boolean> = { current: false }

export const getTokenBalances = async (
chainId: ChainId,
address: HexAddress,
): Promise<BalanceResult> => {
if (startedRef.current === false) {
startedRef.current = true

await Moralis.start({
apiKey: getMoralisApiKey(),
})
}

const response = await Moralis.EvmApi.wallets.getWalletTokenBalancesPrice({
chain: chainId.toString(),
address,
})

return response.result
.filter((result) => !result.possibleSpam)
.map((result) => result.toJSON())
}
2 changes: 2 additions & 0 deletions deployables/app/app/balances/server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type { BalanceResult, TokenBalance } from '../types'
export { getTokenBalances } from './getTokenBalances'
9 changes: 9 additions & 0 deletions deployables/app/app/balances/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type Moralis from 'moralis'

export type BalanceResult = ReturnType<
Awaited<
ReturnType<(typeof Moralis.EvmApi.wallets)['getWalletTokenBalancesPrice']>
>['result'][number]['toJSON']
>[]

export type TokenBalance = BalanceResult[number]
27 changes: 11 additions & 16 deletions deployables/app/app/components/AvatarInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ export const AvatarInput = ({ value, waypoints, onChange }: Props) => {
label="Piloted Safe"
clearLabel="Clear piloted Safe"
dropdownLabel="View all available Safes"
formatOptionLabel={SafeOptionLabel}
placeholder="Paste an address or select from the list"
classNames={selectStyles<Option>()}
value={
Expand Down Expand Up @@ -89,7 +88,17 @@ export const AvatarInput = ({ value, waypoints, onChange }: Props) => {
isValidNewOption={(option) => {
return !!validateAddress(option)
}}
/>
>
{({ data: { value } }) => (
<div className="flex items-center gap-4">
<Blockie address={value} className="size-5 shrink-0" />

<code className="overflow-hidden text-ellipsis whitespace-nowrap font-mono">
{getAddress(value).toLowerCase()}
</code>
</div>
)}
</Select>
)
}

Expand All @@ -112,17 +121,3 @@ export const AvatarInput = ({ value, waypoints, onChange }: Props) => {
/>
)
}

const SafeOptionLabel = (option: Option) => {
const checksumAddress = getAddress(option.value).toLowerCase()

return (
<div className="flex items-center gap-4 py-2">
<Blockie address={option.value} className="size-5 shrink-0" />

<code className="overflow-hidden text-ellipsis whitespace-nowrap font-mono">
{checksumAddress}
</code>
</div>
)
}
20 changes: 7 additions & 13 deletions deployables/app/app/components/ChainSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@ export interface Props {
onChange(chainId: ChainId): void
}

interface Option {
value: ChainId
label: string
}

const options = Object.entries(CHAIN_NAME).map(([chainId, name]) => ({
value: parseInt(chainId) as ChainId,
label: name,
Expand All @@ -30,12 +25,11 @@ export const ChainSelect = ({ value, onChange }: Props) => (

onChange(option.value)
}}
formatOptionLabel={ChainOptionLabel as any}
/>
)

const ChainOptionLabel = ({ value, label }: Option) => (
<div className="flex items-center gap-4 py-2">
<div className="pl-1">{label || `#${value}`}</div>
</div>
>
{({ data: { label, value } }) => (
<div className="flex items-center gap-4">
<div className="pl-1">{label || `#${value}`}</div>
</div>
)}
</Select>
)
39 changes: 19 additions & 20 deletions deployables/app/app/components/ModSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,36 +12,35 @@ export interface Option {
}

interface Props<Option = unknown, Multi extends boolean = boolean>
extends SelectProps<false, Option, Multi> {
extends SelectProps<Option, false, Multi> {
avatarAddress: HexAddress
}

export function ModSelect<Multi extends boolean = boolean>({
avatarAddress,
...props
}: Props<Option, Multi>) {
const ModuleOptionLabel = (option: Option) => {
if (!option.value)
return (
<Value label="No Mod — Direct execution" address={avatarAddress}>
Transactions submitted directly to the Safe
</Value>
)

const checksumAddress = getAddress(option.value)
return (
<Value address={option.value} label={option.label}>
{checksumAddress}
</Value>
)
}

return (
<Select
{...props}
formatOptionLabel={ModuleOptionLabel}
noOptionsMessage={() => 'No modules are enabled on this Safe'}
/>
>
{({ data: { value, label } }) => {
if (!value)
return (
<Value label="No Mod — Direct execution" address={avatarAddress}>
Transactions submitted directly to the Safe
</Value>
)

const checksumAddress = getAddress(value)
return (
<Value address={value} label={label}>
{checksumAddress}
</Value>
)
}}
</Select>
)
}

Expand All @@ -51,7 +50,7 @@ type ValueProps = PropsWithChildren<{
}>

const Value = ({ label, address, children }: ValueProps) => (
<div className="flex items-center gap-4 py-2">
<div className="flex items-center gap-4">
<Blockie address={address} className="size-5 shrink-0" />

<div className="flex items-center gap-2 overflow-hidden">
Expand Down
46 changes: 2 additions & 44 deletions deployables/app/app/components/wallet/WalletProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,10 @@
import { getWagmiConfig } from '@/wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { getDefaultConfig } from 'connectkit'
import { useMemo, type PropsWithChildren } from 'react'
import { createConfig, injected, WagmiProvider } from 'wagmi'
import {
arbitrum,
avalanche,
base,
gnosis,
mainnet,
optimism,
polygon,
sepolia,
} from 'wagmi/chains'
import { metaMask, walletConnect } from 'wagmi/connectors'
import { WagmiProvider } from 'wagmi'

const queryClient = new QueryClient()

const WALLETCONNECT_PROJECT_ID = '0f8a5e2cf60430a26274b421418e8a27'

export type WalletProviderProps = PropsWithChildren<{
injectedOnly?: boolean
}>
Expand All @@ -34,32 +21,3 @@ export const WalletProvider = ({
</QueryClientProvider>
)
}

export const getWagmiConfig = (injectedOnly: boolean) =>
createConfig(
getDefaultConfig({
appName: 'Zodiac Pilot',
ssr: true,
walletConnectProjectId: WALLETCONNECT_PROJECT_ID,
chains: [
mainnet,
optimism,
gnosis,
polygon,
sepolia,
base,
arbitrum,
avalanche,
],
connectors: injectedOnly
? [injected()]
: [
injected(),
metaMask(),
walletConnect({
projectId: WALLETCONNECT_PROJECT_ID,
showQrModal: false,
}),
],
}),
)
2 changes: 1 addition & 1 deletion deployables/app/app/components/wallet/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { ConnectWallet } from './ConnectWallet'
export { ConnectWalletFallback } from './ConnectWalletFallback'
export { WalletProvider, getWagmiConfig } from './WalletProvider'
export { WalletProvider } from './WalletProvider'
4 changes: 3 additions & 1 deletion deployables/app/app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export default [
layout('routes/tokens/balances/layout.tsx', [
route('balances', 'routes/tokens/balances/balances.tsx'),
]),
route('send', 'routes/tokens/send.tsx'),
layout('routes/tokens/send/layout.tsx', [
route('send', 'routes/tokens/send/send.tsx'),
]),
]),
route('/new-route', 'routes/new-route.ts'),
route('/edit-route/:data', 'routes/edit-route.$data.tsx'),
Expand Down
27 changes: 7 additions & 20 deletions deployables/app/app/routes/$address.$chainId/balances.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,15 @@
import { getMoralisApiKey } from '@zodiac/env'
import Moralis from 'moralis'
import type { Ref } from 'react'
import type { BalanceResult } from '../types.server'
import { getTokenBalances, type BalanceResult } from '@/balances-server'
import { verifyChainId } from '@zodiac/chains'
import { verifyHexAddress } from '@zodiac/schema'
import type { Route } from './+types/balances'

const startedRef: Ref<boolean> = { current: false }

export const loader = async ({
params,
}: Route.LoaderArgs): Promise<BalanceResult> => {
const { chainId, address } = params

if (startedRef.current === false) {
startedRef.current = true

await Moralis.start({
apiKey: getMoralisApiKey(),
})
}

const response = await Moralis.EvmApi.wallets.getWalletTokenBalancesPrice({
chain: chainId,
address,
})

return response.result.filter((result) => !result.possibleSpam)
return getTokenBalances(
verifyChainId(parseInt(chainId)),
verifyHexAddress(address),
)
}
20 changes: 15 additions & 5 deletions deployables/app/app/routes/edit-route.$data.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,9 @@ describe('Edit route', () => {
)
await userEvent.click(screen.getByRole('option', { name: 'Roles v2' }))

expect(await screen.findByText('Roles v2')).toBeInTheDocument()
await waitFor(async () => {
expect(await screen.findByText('Roles v2')).toBeInTheDocument()
})
})

it('reloads the modules when the chain changes', async () => {
Expand Down Expand Up @@ -357,7 +359,9 @@ describe('Edit route', () => {

await render(`/edit-route/${encode(route)}`)

expect(await screen.findByText('Roles v1')).toBeInTheDocument()
await waitFor(async () => {
expect(await screen.findByText('Roles v1')).toBeInTheDocument()
})
})

it('is possible to select the v1 roles mod', async () => {
Expand All @@ -382,7 +386,9 @@ describe('Edit route', () => {
)
await userEvent.click(screen.getByRole('option', { name: 'Roles v1' }))

expect(await screen.findByText('Roles v1')).toBeInTheDocument()
await waitFor(async () => {
expect(await screen.findByText('Roles v1')).toBeInTheDocument()
})
})

describe('Config', () => {
Expand Down Expand Up @@ -482,7 +488,9 @@ describe('Edit route', () => {

await render(`/edit-route/${encode(route)}`)

expect(await screen.findByText('Roles v2')).toBeInTheDocument()
await waitFor(async () => {
expect(await screen.findByText('Roles v2')).toBeInTheDocument()
})
})

it('is possible to select the v2 roles mod', async () => {
Expand All @@ -507,7 +515,9 @@ describe('Edit route', () => {
)
await userEvent.click(screen.getByRole('option', { name: 'Roles v2' }))

expect(await screen.findByText('Roles v2')).toBeInTheDocument()
await waitFor(async () => {
expect(await screen.findByText('Roles v2')).toBeInTheDocument()
})
})

describe('Config', () => {
Expand Down
Loading
Loading