Skip to content
This repository was archived by the owner on Feb 8, 2025. It is now read-only.

Commit 380ef43

Browse files
authored
Merge pull request #270 from zallo-labs/Z-260-incoming-transfers
Z 260 incoming transfers
2 parents ec3b4c1 + 4721ced commit 380ef43

21 files changed

+385
-85
lines changed
Binary file not shown.

api/schema.graphql

+1
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,7 @@ type Query {
604604
tokenMetadata(address: UAddress!): TokenMetadata
605605
tokens(input: TokensInput! = {}): [Token!]!
606606
transaction(id: ID!): Transaction
607+
transfer(id: ID!): Transferlike
607608
user: User!
608609
}
609610

api/src/feat/transfers/transfers.resolver.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { Context, Parent, Resolver, Subscription } from '@nestjs/graphql';
1+
import { Args, Context, Info, Parent, Query, Resolver, Subscription } from '@nestjs/graphql';
22
import {
33
TRANSFER_VALUE_FIELDS_SHAPE,
44
TransferValueSelectFields,
55
TransfersService,
66
} from './transfers.service';
77
import { TransferSubscriptionInput } from './transfers.input';
8-
import { Transfer, TransferDetails } from './transfers.model';
8+
import { Transfer, TransferDetails, Transferlike } from './transfers.model';
99
import { GraphQLResolveInfo } from 'graphql';
1010
import { getShape } from '~/core/database';
1111
import { Input, InputArgs } from '~/common/decorators/input.decorator';
@@ -16,6 +16,7 @@ import { TransferSubscriptionPayload, transferTrigger } from './transfers.events
1616
import { ComputedField } from '~/common/decorators/computed.decorator';
1717
import { DecimalScalar } from '~/common/scalars/Decimal.scalar';
1818
import Decimal from 'decimal.js';
19+
import { NodeArgs } from '../nodes/nodes.input';
1920

2021
@Resolver(() => TransferDetails)
2122
export class TransfersResolver {
@@ -24,6 +25,11 @@ export class TransfersResolver {
2425
private pubsub: PubsubService,
2526
) {}
2627

28+
@Query(() => Transferlike, { nullable: true })
29+
async transfer(@Args() { id }: NodeArgs, @Info() info: GraphQLResolveInfo) {
30+
return this.service.selectUnique(id, getShape(info));
31+
}
32+
2733
@ComputedField(() => DecimalScalar, TRANSFER_VALUE_FIELDS_SHAPE, { nullable: true })
2834
async value(@Parent() parent: TransferValueSelectFields): Promise<Decimal | null> {
2935
return this.service.value(parent);

api/src/feat/transfers/transfers.service.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ export class TransfersService {
2424
private prices: PricesService,
2525
) {}
2626

27-
async selectUnique(id: uuid, shape?: ShapeFunc<typeof e.Transfer>) {
27+
async selectUnique(id: uuid, shape?: ShapeFunc<typeof e.Transferlike>) {
2828
return this.db.queryWith(
2929
{ id: e.uuid },
3030
({ id }) =>
31-
e.select(e.Transfer, (transfer) => ({
31+
e.select(e.Transferlike, (transfer) => ({
3232
filter_single: { id },
3333
...shape?.(transfer),
3434
})),

app/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
"jotai": "^2.8.4",
9595
"jotai-effect": "^1.0.0",
9696
"jotai-immer": "^0.4.1",
97+
"jotai-scope": "^0.7.0",
9798
"jwt-decode": "^4.0.0",
9899
"lib": "workspace:*",
99100
"luxon": "^3.4.4",

app/src/api/field-handlers.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { MissingFieldHandler, ROOT_TYPE } from 'relay-runtime';
22

3-
const NODE_RESOLVERS = new Set(['node', 'proposal', 'transaction', 'message']);
3+
const NODE_RESOLVERS = new Set(['node', 'proposal', 'transaction', 'message', 'transfer']);
44

55
export const missingFieldHandlers: MissingFieldHandler[] = [
66
{

app/src/app/(nav)/[account]/(home)/activity/_layout.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { ActivityPane } from './index';
44

55
export default function ActivityLayout() {
66
return (
7-
<Panes>
7+
<>
88
<ActivityPane />
99
<Slot />
10-
</Panes>
10+
</>
1111
);
1212
}

app/src/app/(nav)/[account]/(home)/activity/index.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Searchbar } from '#/Appbar/Searchbar';
2-
import { FirstPane } from '#/layout/FirstPane';
32
import { PaneSkeleton } from '#/skeleton/PaneSkeleton';
43
import { withSuspense } from '#/skeleton/withSuspense';
54
import { AccountParams } from '../../_layout';
@@ -26,6 +25,7 @@ import {
2625
activity_ActivityPaneQuery,
2726
activity_ActivityPaneQuery$data,
2827
} from '~/api/__generated__/activity_ActivityPaneQuery.graphql';
28+
import { Pane } from '#/layout/Pane';
2929

3030
const Query = graphql`
3131
query activity_ActivityPaneQuery($account: UAddress!) {
@@ -78,7 +78,7 @@ function ActivityPane_() {
7878
});
7979

8080
return (
81-
<FirstPane flex>
81+
<Pane flex>
8282
<FlashList
8383
ListHeaderComponent={
8484
<>
@@ -130,7 +130,7 @@ function ActivityPane_() {
130130
keyExtractor={(item) => (typeof item === 'string' ? item : item.id)}
131131
getItemType={(item) => (typeof item === 'string' ? 'section' : item.__typename)}
132132
/>
133-
</FirstPane>
133+
</Pane>
134134
);
135135
}
136136

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import { withSuspense } from '#/skeleton/withSuspense';
2+
import { zUuid } from '~/lib/zod';
3+
import { AccountParams } from '../../../_layout';
4+
import { useLocalParams } from '~/hooks/useLocalParams';
5+
import { Pane } from '#/layout/Pane';
6+
import { graphql } from 'relay-runtime';
7+
import { useLazyQuery } from '~/api';
8+
import { Id_TransferScreenQuery } from '~/api/__generated__/Id_TransferScreenQuery.graphql';
9+
import { Scrollable } from '#/Scrollable';
10+
import { TokenIcon } from '#/token/TokenIcon';
11+
import { OverlayIcon } from '#/layout/OverlayIcon';
12+
import { ContactsOutlineIcon, ReceiveIcon, ShareIcon, WebIcon } from '@theme/icons';
13+
import { View } from 'react-native';
14+
import { ICON_SIZE } from '@theme/paper';
15+
import { Text } from 'react-native-paper';
16+
import { createStyles, useStyles } from '@theme/styles';
17+
import { Timestamp } from '#/format/Timestamp';
18+
import { Appbar } from '#/Appbar/Appbar';
19+
import { TokenAmount } from '#/token/TokenAmount';
20+
import { FiatValue } from '#/FiatValue';
21+
import { ItemList } from '#/layout/ItemList';
22+
import { ListItem } from '#/list/ListItem';
23+
import { AddressIcon } from '#/Identicon/AddressIcon';
24+
import { Chip } from '#/Chip';
25+
import { SelectableAddress } from '#/address/SelectableAddress';
26+
import { asChain, asUAddress } from 'lib';
27+
import { Link, useRouter } from 'expo-router';
28+
import Decimal from 'decimal.js';
29+
import { Actions } from '#/layout/Actions';
30+
import { Button } from '#/Button';
31+
import { share } from '~/lib/share';
32+
import { CHAINS } from 'chains';
33+
import { PaneSkeleton } from '#/skeleton/PaneSkeleton';
34+
35+
const Query = graphql`
36+
query Id_TransferScreenQuery($id: ID!, $account: UAddress!) {
37+
transfer(id: $id) @required(action: THROW) {
38+
id
39+
tokenAddress
40+
timestamp
41+
from
42+
amount
43+
value
44+
systxHash
45+
token {
46+
...TokenIcon_token
47+
...TokenAmount_token
48+
balance(input: { account: $account })
49+
price {
50+
id
51+
usd
52+
}
53+
}
54+
account {
55+
id
56+
address
57+
}
58+
}
59+
}
60+
`;
61+
62+
const TransferScreenParams = AccountParams.extend({ id: zUuid() });
63+
64+
function TransferScreen() {
65+
const { id, account } = useLocalParams(TransferScreenParams);
66+
const { styles } = useStyles(stylesheet);
67+
const router = useRouter();
68+
69+
const { transfer: t } = useLazyQuery<Id_TransferScreenQuery>(Query, { id, account });
70+
const chain = asChain(t.tokenAddress);
71+
const from = asUAddress(t.from, chain);
72+
73+
const blockExplorer = CHAINS[chain].blockExplorers?.default;
74+
const explorerUrl = blockExplorer && `${blockExplorer.url}/tx/${t.systxHash}`;
75+
76+
return (
77+
<Pane flex>
78+
<Scrollable>
79+
<Appbar mode="small" />
80+
<View style={styles.centered}>
81+
<View style={styles.icon}>
82+
<TokenIcon token={t.token} size={ICON_SIZE.extraLarge} />
83+
<OverlayIcon icon={ReceiveIcon} parentSize={ICON_SIZE.extraLarge} />
84+
</View>
85+
86+
<Text variant="titleLarge" style={styles.received}>
87+
Received <Timestamp timestamp={t.timestamp} />
88+
</Text>
89+
90+
<Text variant="headlineMedium">
91+
{/* TODO: generic token resolution */}
92+
{t.token && <TokenAmount token={t.token} amount={t.amount} />}
93+
94+
<Text style={styles.value}>
95+
{' ('}
96+
<FiatValue value={t.amount} />
97+
{')'}
98+
</Text>
99+
</Text>
100+
</View>
101+
102+
<ItemList>
103+
<ListItem
104+
leading={<AddressIcon address={from} />}
105+
overline="From"
106+
headline={<SelectableAddress address={from} />}
107+
trailing={
108+
<Chip
109+
mode="outlined"
110+
icon={(props) => (
111+
<ContactsOutlineIcon {...props} color={styles.contactChip.color} />
112+
)}
113+
onPress={() =>
114+
router.push({ pathname: `/(nav)/contacts/[address]`, params: { address: from } })
115+
}
116+
>
117+
Contact
118+
</Chip>
119+
}
120+
containerStyle={styles.item}
121+
/>
122+
123+
<ListItem
124+
leading={<AddressIcon address={t.account.address} />}
125+
overline="Account"
126+
headline={<SelectableAddress address={t.account.address} />}
127+
containerStyle={styles.item}
128+
trailing={({ Text }) =>
129+
t.token && (
130+
<View style={styles.balanceContainer}>
131+
<Text>
132+
<TokenAmount token={t.token} amount={t.token.balance} />
133+
</Text>
134+
135+
{t.token.price && (
136+
<Text>
137+
<FiatValue value={new Decimal(t.token.balance).mul(t.token.price.usd)} />
138+
</Text>
139+
)}
140+
</View>
141+
)
142+
}
143+
/>
144+
</ItemList>
145+
146+
{explorerUrl && (
147+
<Actions horizontal style={styles.actions}>
148+
<Link href={explorerUrl} asChild>
149+
<Button mode="contained-tonal" icon={WebIcon}>
150+
Explorer
151+
</Button>
152+
</Link>
153+
154+
<Button
155+
mode="contained-tonal"
156+
icon={ShareIcon}
157+
onPress={() => share({ url: explorerUrl })}
158+
>
159+
Share
160+
</Button>
161+
</Actions>
162+
)}
163+
</Scrollable>
164+
</Pane>
165+
);
166+
}
167+
168+
const stylesheet = createStyles(({ colors }) => ({
169+
centered: {
170+
alignItems: 'center',
171+
marginBottom: 16,
172+
},
173+
icon: {
174+
marginBottom: 16,
175+
},
176+
received: {
177+
color: colors.onSurfaceVariant,
178+
},
179+
value: {
180+
color: colors.tertiary,
181+
},
182+
item: {
183+
backgroundColor: colors.surface,
184+
},
185+
balanceContainer: {
186+
flexDirection: 'column',
187+
alignItems: 'flex-end',
188+
},
189+
contactChip: {
190+
color: colors.onSurface,
191+
},
192+
actions: {
193+
marginVertical: 8,
194+
},
195+
}));
196+
197+
export default withSuspense(TransferScreen, <PaneSkeleton />);
198+
199+
export { ErrorBoundary } from '#/ErrorBoundary';

app/src/app/(nav)/[account]/(home)/index.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { FirstPane } from '#/layout/FirstPane';
21
import { PaneSkeleton } from '#/skeleton/PaneSkeleton';
32
import { withSuspense } from '#/skeleton/withSuspense';
43
import { useLocalParams } from '~/hooks/useLocalParams';
@@ -18,6 +17,7 @@ import { ITEM_LIST_GAP } from '#/layout/ItemList';
1817
import { graphql } from 'relay-runtime';
1918
import { HomePaneQuery } from '~/api/__generated__/HomePaneQuery.graphql';
2019
import { useLazyQuery } from '~/api/useLazyQuery';
20+
import { Pane } from '#/layout/Pane';
2121

2222
const Query = graphql`
2323
query HomePaneQuery($account: UAddress!, $chain: Chain!) {
@@ -62,7 +62,7 @@ function HomePane_() {
6262
.sort((a, b) => b.value.comparedTo(a.value));
6363

6464
return (
65-
<FirstPane flex padding={false}>
65+
<Pane flex padding={false}>
6666
<FlatList
6767
contentContainerStyle={styles.container}
6868
ListHeaderComponent={
@@ -95,7 +95,7 @@ function HomePane_() {
9595
keyExtractor={(item) => item.id}
9696
showsVerticalScrollIndicator={false}
9797
/>
98-
</FirstPane>
98+
</Pane>
9999
);
100100
}
101101

app/src/app/(nav)/[account]/settings/index.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { ListItem } from '#/list/ListItem';
1111
import { AddCircleIcon } from '#/AddCircleIcon';
1212
import { PolicyItem } from '#/policy/PolicyItem';
1313
import { usePath } from '#/usePath';
14-
import { FirstPane } from '#/layout/FirstPane';
1514
import { useRouteInfo, useRouter } from 'expo-router/build/hooks';
1615
import { ItemList } from '#/layout/ItemList';
1716
import { PolicySuggestions } from '#/account/PolicySuggestions';
@@ -24,6 +23,7 @@ import { graphql } from 'relay-runtime';
2423
import { useLazyQuery } from '~/api';
2524
import { settings_AccountSettingsQuery } from '~/api/__generated__/settings_AccountSettingsQuery.graphql';
2625
import { UpgradePolicyItem } from '#/account/UpgradePolicyItem';
26+
import { Pane } from '#/layout/Pane';
2727

2828
const Query = graphql`
2929
query settings_AccountSettingsQuery($account: UAddress!) {
@@ -79,7 +79,7 @@ function AccountSettingsPane_() {
7979
const upgradePolicy = a.policies.find((p) => p.key === PolicyPresetKey.upgrade);
8080

8181
return (
82-
<FirstPane fixed>
82+
<Pane fixed>
8383
<ScrollView contentContainerStyle={styles.pane} showsVerticalScrollIndicator={false}>
8484
<Searchbar
8585
leading={MenuOrSearchIcon}
@@ -186,7 +186,7 @@ function AccountSettingsPane_() {
186186
</Link>
187187
</ItemList>
188188
</ScrollView>
189-
</FirstPane>
189+
</Pane>
190190
);
191191
}
192192

0 commit comments

Comments
 (0)