on master help sign in
bridge/src/views/Point/IssueChild.tsx
349 lines 11 kB history raw
1import React, { useCallback, useEffect, useMemo, useState } from 'react';
2import cn from 'classnames';
3import { Button, Grid } from 'indigo-react';
4import Window from 'components/L2/Window/Window';
5import HeaderPane from 'components/L2/Window/HeaderPane';
6import BodyPane from 'components/L2/Window/BodyPane';
8import * as azimuth from 'azimuth-js';
9import ob from 'urbit-ob';
10import { Box, Icon, Row, Text } from '@tlon/indigo-react';
12import { useNetwork } from 'store/network';
13import { usePointCache } from 'store/pointCache';
14import { usePointCursor } from 'store/pointCursor';
15import { useRollerStore } from 'store/rollerStore';
17import * as need from 'lib/need';
18import useEthereumTransaction from 'lib/useEthereumTransaction';
19import { GAS_LIMITS } from 'lib/constants';
20import { patp2dec } from 'lib/patp2dec';
21import { getSpawnCandidate } from 'lib/child';
22import { useLocalRouter } from 'lib/LocalRouter';
23import { validateChild } from 'lib/validators';
24import { convertToInt } from 'lib/convertToInt';
25import { isGalaxy } from 'lib/utils/point';
26import useRoller from 'lib/useRoller';
27import { L1TxnType } from 'lib/types/PendingL1Transaction';
29import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
30import View from 'components/View';
31import CopiableAddress from 'components/copiable/CopiableAddress';
32import L2BackHeader from 'components/L2/Headers/L2BackHeader';
33import LoadingOverlay from 'components/L2/LoadingOverlay';
34import { PointInput, AddressInput } from 'form/Inputs';
35import {
36 composeValidator,
37 buildPointValidator,
38 buildAddressValidator,
39 hasErrors,
40} from 'form/validators';
41import BridgeForm from 'form/BridgeForm';
42import FormError from 'form/FormError';
44import { PatpBadge } from './PatpBadge';
45import { AddressButton } from '../../components/AddressButton';
46import './IssueChild.scss';
48export function useIssueChild() {
49 const { contracts }: any = useNetwork();
50 const { syncDates }: any = usePointCache();
52 const _contracts = need.contracts(contracts);
54 const [spawnedPoint, setSpawnedPoint] = useState<number | string | null>();
56 return useEthereumTransaction(
57 useCallback(
58 (spawnedPoint: string | number, owner: string) => {
59 setSpawnedPoint(spawnedPoint);
60 return azimuth.ecliptic.spawn(_contracts, spawnedPoint, owner);
61 },
62 [_contracts]
63 ),
64 useCallback(() => syncDates(spawnedPoint), [spawnedPoint, syncDates]),
65 GAS_LIMITS.DEFAULT
66 );
69export default function IssueChild() {
70 const { pop }: any = useLocalRouter();
71 const { api, checkForUpdates, spawnPoint } = useRoller();
72 const { pointCursor }: any = usePointCursor();
73 const { point } = useRollerStore();
75 const _point = convertToInt(need.point(pointCursor), 10);
76 const [availablePoints, setAvailablePoints] = useState<Set<number> | null>(
77 null
78 );
79 const [candidates, setCandidates] = useState<string[]>([]);
80 const [pointToSpawn, setPointToSpawn] = useState<number | null>();
81 const [destinationAddress, setDestinationAddress] = useState<string | null>();
82 const [l2SpawnSent, setL2SpawnSent] = useState(false);
83 const [loading, setLoading] = useState(false);
85 const fetchAvailablePoints = useCallback(async () => {
86 const available = await api.getUnspawned(_point);
87 setAvailablePoints(new Set(available));
88 }, [_point, api]);
90 const shuffle = useCallback(() => {
91 const newCandidates: string[] = [];
93 // 20 tries to randomize
94 for (let i = 0; i < 20; i++) {
95 const candidate = ob.patp(getSpawnCandidate(_point));
96 if (!newCandidates.includes(candidate)) {
97 newCandidates.push(candidate);
98 }
99 if (newCandidates.length === 3) {
100 break;
101 }
102 }
104 setCandidates(newCandidates);
105 }, [_point]);
107 const handleShuffleClick = useCallback(() => {
108 shuffle();
109 }, [shuffle]);
111 useEffect(() => {
112 // load the availablePoints for validation
113 fetchAvailablePoints();
114 // set the candidates
115 shuffle();
116 }, [fetchAvailablePoints, shuffle]);
118 const spawnNewPoint = useCallback(async () => {
119 if (pointToSpawn && destinationAddress) {
120 setLoading(true);
121 await spawnPoint(pointToSpawn, destinationAddress);
122 checkForUpdates({ point: pointToSpawn });
123 setPointToSpawn(null);
124 setDestinationAddress(null);
125 setL2SpawnSent(true);
126 setLoading(false);
127 }
128 }, [pointToSpawn, destinationAddress, spawnPoint, checkForUpdates]);
130 const {
131 isDefaultState,
132 construct,
133 unconstruct,
134 completed,
135 inputsLocked,
136 bind,
137 txHashes,
138 } = useIssueChild();
140 useEffect(() => {
141 if (completed && pointToSpawn) {
142 checkForUpdates({
143 point: pointToSpawn,
144 l1Txn: {
145 id: `${pointToSpawn}`,
146 point: point.value,
147 type: L1TxnType.spawn,
148 hash: txHashes[0],
149 time: new Date().getTime(),
150 },
151 });
152 }
153 }, [completed]); // eslint-disable-line react-hooks/exhaustive-deps
155 const validateFormAsync = useCallback(
156 async values => {
157 const point = patp2dec(values.point);
158 const hasPoint = availablePoints && availablePoints.has(point);
160 if (!hasPoint) {
161 return { point: 'This point cannot be spawned.' };
162 }
163 },
164 [availablePoints]
165 );
167 const validateForm = useCallback(
168 (values, errors) => {
169 if (hasErrors(errors)) {
170 return errors;
171 }
173 return validateFormAsync(values);
174 },
175 [validateFormAsync]
176 );
178 const validate = useMemo(
179 () =>
180 composeValidator(
181 {
182 point: buildPointValidator(4, [validateChild(ob.patp(_point))]),
183 owner: buildAddressValidator(),
184 },
185 validateForm
186 ),
187 [_point, validateForm]
188 );
190 const onValues = useCallback(
191 ({ valid, values }) => {
192 if (valid) {
193 construct(patp2dec(values.point), values.owner);
194 } else {
195 unconstruct();
196 }
198 try {
199 setPointToSpawn(patp2dec(values.point));
200 } catch (err) {
201 setPointToSpawn(null);
202 }
204 setDestinationAddress(values.owner);
205 },
206 [construct, unconstruct]
207 );
209 const validPoint =
210 pointToSpawn && availablePoints && availablePoints.has(pointToSpawn);
211 const validAddress =
212 destinationAddress && !buildAddressValidator()(destinationAddress);
214 const pointType = point.isStar ? 'planet' : 'star';
216 const getButtonText = () => {
217 if (!validPoint) {
218 return `Please enter a valid ${pointType}`;
219 } else if (!validAddress) {
220 return `Please enter a valid destination address`;
221 }
222 return `Spawn ${ob.patp(pointToSpawn)}`;
223 };
225 return (
226 <View
227 pop={pop}
228 hideBack
229 inset
230 className="issue-child"
231 header={<L2BackHeader hideBalance={point.isL2Spawn} back={pop} />}>
232 <Window className="id-issue-child">
233 <HeaderPane>
234 <Row>
235 <h5>Spawn {isGalaxy(_point) ? 'Star' : 'Planet'}</h5>
236 </Row>
237 </HeaderPane>
238 <BodyPane>
239 <Box className="inner-container">
240 <BridgeForm
241 className="w-full"
242 validate={validate}
243 onValues={onValues}>
244 {({ handleSubmit, values }: any) => (
245 <>
246 {(completed || l2SpawnSent) && (
247 <Grid.Item
248 full
249 as={'div'}
250 style={{ fontSize: 14 }}
251 className={cn('f5 wrap', {
252 green3: completed,
253 })}>
254 <span className="mono">{values.point}</span> has been
255 spawned and can be claimed by{' '}
256 <CopiableAddress>{values.owner}</CopiableAddress>.
257 <Grid.Item
258 as={Button}
259 full
260 className="mt6"
261 center
262 solid
263 onClick={() => pop()}>
264 Go back
265 </Grid.Item>
266 </Grid.Item>
267 )}
269 {!(completed || l2SpawnSent) && (
270 <>
271 <Grid.Item
272 full
273 as={PointInput}
274 name="point"
275 disabled={inputsLocked}
276 className="mt4"
277 />
279 {isDefaultState && candidates.length > 0 && (
280 <>
281 <Text className={'input-hint'}>Perhaps:</Text>
282 <Row>
283 <Box className={'patp-badge-container'}>
284 <Box className="badges">
285 {candidates.map(p => (
286 <PatpBadge className={'patp-badge'} key={p}>
287 {p}
288 </PatpBadge>
289 ))}
290 </Box>
291 <Box className="shuffle">
292 <Icon
293 size={16}
294 icon="ArrowRefresh"
295 onClick={handleShuffleClick}></Icon>
296 </Box>
297 </Box>
298 </Row>
299 </>
300 )}
302 <Grid.Item
303 full
304 as={AddressInput}
305 className="mb4"
306 name="owner"
307 label="Ethereum Address"
308 disabled={inputsLocked}
309 />
310 {isDefaultState && (
311 <Box>
312 <AddressButton inputName="owner">
313 Use my address
314 </AddressButton>
315 </Box>
316 )}
317 <Grid.Item full as={FormError} />
319 {point.isL2Spawn ? (
320 <Grid.Item
321 as={Button}
322 full
323 className="mt4"
324 center
325 solid
326 disabled={!validPoint || !validAddress}
327 onClick={spawnNewPoint}>
328 {getButtonText()}
329 </Grid.Item>
330 ) : (
331 <Grid.Item
332 full
333 as={InlineEthereumTransaction}
334 {...bind}
335 onReturn={() => pop()}
336 />
337 )}
338 </>
339 )}
340 </>
341 )}
342 </BridgeForm>
343 </Box>
344 </BodyPane>
345 </Window>
346 <LoadingOverlay loading={loading} />
347 </View>
348 );