Skip to content

Commit

Permalink
Show pending transfers from temporal in UI
Browse files Browse the repository at this point in the history
  • Loading branch information
youngkidwarrior committed Oct 17, 2024
1 parent d138fa1 commit 4336036
Show file tree
Hide file tree
Showing 17 changed files with 295 additions and 166 deletions.
6 changes: 5 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,13 @@ Here is a quick peek at the send stack. Quickly jump to any of the submodules by
│   ├── <a href="https://github.com/0xsend/sendapp/tree/main/packages/daimo-expo-passkeys">daimo-expo-passkeys</a>
│   ├── <a href="https://github.com/0xsend/sendapp/tree/main/packages/eslint-config-custom">eslint-config-customs</a>
│   ├── <a href="https://github.com/0xsend/sendapp/tree/main/packages/playwright">playwright</a>
| ├── <a href="https://github.com/0xsend/sendapp/tree/main/packages/shovel">shovel</a>
│   ├── <a href="https://github.com/0xsend/sendapp/tree/main/packages/snaplet">snaplet</a>
| ├── <a href="https://github.com/0xsend/sendapp/tree/main/packages/temporal">temporal</a>
│   ├── <a href="https://github.com/0xsend/sendapp/tree/main/packages/ui">ui</a>
│   ├── <a href="https://github.com/0xsend/sendapp/tree/main/packages/wagmi">wagmi</a>
│   └── <a href="https://github.com/0xsend/sendapp/tree/main/packages/webauthn-authenticator">webauthn-authenticator</a>
| ├── <a href="https://github.com/0xsend/sendapp/tree/main/packages/webauthn-authenticator">webauthn-authenticator</a>
│   └── <a href="https://github.com/0xsend/sendapp/tree/main/packages/workflows">workflows</a>
└── <a href="https://github.com/0xsend/sendapp/tree/main/supabase">supabase</a>
</code>
</pre>
Expand Down
4 changes: 2 additions & 2 deletions packages/api/src/routers/transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export const transferRouter = createTRPCRouter({
const states: transferState[] = []
const client = await getTemporalClient()
const workflows = client.workflow.list({
query: `ExecutionStatus = "Running" AND WorkflowId BETWEEN "transfer-workflow-${token}-${sender}-" AND "transfer-workflow-${token}-${sender}-~"`,
query: `ExecutionStatus = "Running" AND WorkflowId BETWEEN "send-transfer-workflow-${token}-${sender}-" AND "send-transfer-workflow-${token}-${sender}-~"`,
})
for await (const workflow of workflows) {
const handle = client.workflow.getHandle(workflow.workflowId)
Expand Down Expand Up @@ -88,7 +88,7 @@ export const transferRouter = createTRPCRouter({
const states: transferState[] = []
const client = await getTemporalClient()
const workflows = client.workflow.list({
query: `ExecutionStatus = "Failed" AND WorkflowId BETWEEN "transfer-workflow-${token}-${sender}-" AND "transfer-workflow-${token}-${sender}-~"`,
query: `ExecutionStatus = "Failed" AND WorkflowId BETWEEN "send-transfer-workflow-${token}-${sender}-" AND "send-transfer-workflow-${token}-${sender}-~"`,
})
for await (const workflow of workflows) {
const handle = client.workflow.getHandle(workflow.workflowId)
Expand Down
94 changes: 93 additions & 1 deletion packages/app/features/home/TokenActivityRow.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { Stack, Text, XStack, YStack } from '@my/ui'
import { Avatar, LinkableAvatar, Spinner, Stack, Text, XStack, YStack } from '@my/ui'
import { amountFromActivity, eventNameFromActivity, subtextFromActivity } from 'app/utils/activity'
import type { Activity } from 'app/utils/zod/activity'
import { ActivityAvatar } from '../activity/ActivityAvatar'
import type { transferState } from '@my/workflows'
import { sendAccountAbi, erc20Abi } from '@my/wagmi'
import { decodeFunctionData, formatUnits } from 'viem'
import type { coins } from 'app/data/coins'
import { useProfileLookup } from 'app/utils/useProfileLookup'
import formatAmount from 'app/utils/formatAmount'

export function TokenActivityRow({ activity }: { activity: Activity }) {
const { created_at } = activity
Expand Down Expand Up @@ -58,3 +64,89 @@ export function TokenActivityRow({ activity }: { activity: Activity }) {
</XStack>
)
}

export function PendingTransferActivityRow({
coin,
state,
}: { coin: coins[number]; state: transferState }) {
const { userOp } = state
const { args } = decodeFunctionData({ abi: sendAccountAbi, data: userOp.callData })

const decodedTokenTransfer =
args?.[0]?.[0].data !== '0x'
? decodeFunctionData({ abi: erc20Abi, data: args?.[0]?.[0].data })
: undefined

const amount = decodedTokenTransfer
? formatUnits(decodedTokenTransfer.args[1] as bigint, coin.decimals)
: formatAmount(formatUnits(args?.[0]?.[0].value, 18), 5, 5)

const to = decodedTokenTransfer ? decodedTokenTransfer.args[0] : args?.[0]?.[0].dest

const { data: profile } = useProfileLookup('address', to)

return (
<XStack
width={'100%'}
ai="center"
jc="space-between"
gap="$4"
borderBottomWidth={1}
pb="$5"
borderBottomColor={'$decay'}
opacity={0.75}
$gtMd={{ borderBottomWidth: 0, pb: '0' }}
>
<XStack gap="$4.5" width={'100%'} f={1}>
<LinkableAvatar size="$4.5" br="$4" gap="$2" href={`/profile/${profile?.sendid}`}>
<Avatar.Image src={profile?.avatar_url ?? undefined} />
<Avatar.Fallback jc="center" bc="$olive">
<Avatar size="$4.5" br="$4">
<Avatar.Image
src={`https://ui-avatars.com/api/?name=${
profile?.tag ?? profile?.sendid
}&size=256&format=png&background=86ad7f`}
/>
</Avatar>
</Avatar.Fallback>
</LinkableAvatar>
<YStack gap="$1.5" width={'100%'} f={1} overflow="hidden">
<XStack fd="row" jc="space-between" gap="$1.5" f={1} width={'100%'}>
<Text
color="$color12"
fontSize="$7"
$gtMd={{ fontSize: '$5' }}
textTransform="capitalize"
>
Sending...
</Text>
<Text color="$color12" fontSize="$7">
{`${amount} ${coin.symbol}`}
</Text>
</XStack>
<Stack
gap="$1.5"
fd="column"
$gtSm={{ fd: 'row' }}
alignItems="flex-start"
justifyContent="space-between"
width="100%"
overflow="hidden"
f={1}
>
<Text
theme="alt2"
color="$olive"
fontFamily={'$mono'}
maxWidth={'100%'}
overflow={'hidden'}
>
{profile?.name ?? profile?.tag ?? profile?.sendid}
</Text>
<Spinner size="small" color={'$color12'} />
</Stack>
</YStack>
</XStack>
</XStack>
)
}
3 changes: 1 addition & 2 deletions packages/app/features/home/TokenDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,7 @@ const TokenDetailsBalance = ({
export function RowLabel({ children }: PropsWithChildren) {
return (
<H4
// @TODO: Update with theme color variable
color="hsl(0, 0%, 42.5%)"
color="$color10"
fontFamily={'$mono'}
fontWeight={'500'}
size={'$5'}
Expand Down
60 changes: 47 additions & 13 deletions packages/app/features/home/TokenDetailsHistory.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,66 @@
import { Button, Paragraph, Spinner, YStack } from '@my/ui'
import type { coins } from 'app/data/coins'
import { hexToBytea } from 'app/utils/hexToBytea'
import { Fragment } from 'react'
import { Fragment, useEffect, useState } from 'react'
import { useTokenActivityFeed } from './utils/useTokenActivityFeed'
import { RowLabel, AnimateEnter } from './TokenDetails'
import { TokenActivityRow } from './TokenActivityRow'
import { PendingTransferActivityRow, TokenActivityRow } from './TokenActivityRow'
import { useSendAccount } from 'app/utils/send-accounts'
import { zeroAddress } from 'viem'

export const TokenDetailsHistory = ({ coin }: { coin: coins[number] }) => {
const result = useTokenActivityFeed({
const { data: sendAccount } = useSendAccount()
const [hasPendingTransfers, setHasPendingTransfers] = useState<boolean | undefined>(true)
const { pendingTransfers, activityFeed } = useTokenActivityFeed({
address: sendAccount?.address ?? zeroAddress,
token: coin.token,
pageSize: 10,
address: coin.token === 'eth' ? undefined : hexToBytea(coin.token),
refetchInterval: 1000,
enabled:
(hasPendingTransfers === undefined || hasPendingTransfers) &&
sendAccount?.address !== undefined,
})

const { data: pendingTransfersData, isError: pendingTransfersError } = pendingTransfers

const {
data,
data: activityFeedData,
isLoading: isLoadingActivities,
error: activitiesError,
isFetching: isFetchingActivities,
isFetchingNextPage: isFetchingNextPageActivities,
fetchNextPage,
hasNextPage,
} = result
const { pages } = data ?? {}
} = activityFeed

const { pages } = activityFeedData ?? {}

// Check if there are any pending transfers in the temporal db. If not set hasPendingTransfers to false to control refetches
useEffect(() => {
if (Array.isArray(pendingTransfersData)) {
setHasPendingTransfers(pendingTransfersData?.length > 0)
} else if (pendingTransfersError) {
setHasPendingTransfers(false)
} else {
setHasPendingTransfers(undefined)
}
}, [pendingTransfersData, pendingTransfersError])

return (
<YStack gap="$5" testID="TokenDetailsHistory">
{hasPendingTransfers && (
<>
<RowLabel>Pending Transfers</RowLabel>
<AnimateEnter>
{pendingTransfersData?.map((state) => (
<Fragment key={`${state.userOp.nonce}-pending`}>
<AnimateEnter>
<PendingTransferActivityRow coin={coin} state={state} />
</AnimateEnter>
</Fragment>
))}
</AnimateEnter>
</>
)}
{(() => {
switch (true) {
case isLoadingActivities:
Expand All @@ -34,11 +72,7 @@ export const TokenDetailsHistory = ({ coin }: { coin: coins[number] }) => {
</Paragraph>
)
case pages?.length === 0:
return (
<>
<RowLabel>No activities</RowLabel>
</>
)
return <RowLabel>No activities</RowLabel>
default: {
let lastDate: string | undefined
return pages?.map((activities) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ exports[`TokenDetails 1`] = `
accessibilityRole="header"
style={
{
"color": "hsl(0, 0%, 42.5%)",
"color": "#B2B2B2",
"display": "none",
"fontFamily": "System",
"fontSize": 19.2,
Expand Down
2 changes: 1 addition & 1 deletion packages/app/features/home/screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export function HomeScreen() {
<Paragraph theme="red_alt1">No send account found</Paragraph>
</Stack>
)
case search !== undefined:
case search !== undefined: //@todo remove this
return <SendSearchBody />
default:
return (
Expand Down
21 changes: 15 additions & 6 deletions packages/app/features/home/utils/__mocks__/useTokenActivityFeed.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
import { SendAccountTransfersEventSchema } from 'app/utils/zod/activity'
import { mockUsdcTransfers } from './mock-usdc-transfers'
import { hexToBytea } from 'app/utils/hexToBytea'

const tokenTransfersByLogAddr = {
'\\x833589fcd6edb6e08f4c7c32d4f71b54bda02913': mockUsdcTransfers.map((t) =>
SendAccountTransfersEventSchema.parse(t)
),
}

const mockUseTokenActivityFeed = jest.fn(({ address }) => {
const pages = tokenTransfersByLogAddr[address]
const mockUseTokenActivityFeed = jest.fn(({ token }) => {
const logAddress = hexToBytea(token)
const pages = tokenTransfersByLogAddr[logAddress]
if (!pages) throw new Error('No pages found')
return {
data: {
pages: [tokenTransfersByLogAddr[address]],
pendingTransfers: {
data: [], //@todo maybe writes some mock data for temporal?
isLoading: false,
error: null,
},
activityFeed: {
data: {
pages: [tokenTransfersByLogAddr[logAddress]],
},
isLoading: false,
error: null,
},
isLoading: false,
error: null,
}
})
export const useTokenActivityFeed = mockUseTokenActivityFeed
22 changes: 22 additions & 0 deletions packages/app/features/home/utils/usePendingTransfers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { Address } from 'viem'
import type { coins } from 'app/data/coins'
import { api } from 'app/utils/api'

/**
* Fetch Pending transfers by token and send account address
*/
export function usePendingTransfers(params: {
address: Address
token: coins[number]['token']
refetchInterval?: number
enabled?: boolean
}) {
const { address, token, refetchInterval, enabled } = params
return api.transfer.getPending.useQuery(
{ token, sender: address },
{
refetchInterval,
enabled,
}
)
}
Loading

0 comments on commit 4336036

Please sign in to comment.