on master help sign in
feat: Create Galaxy view with new InlineEthereumTransaction
commit2b8b988fc441836608a8635a009c7e69dc267b75 authorMatt Condon (shrugs) <mattgcondon@gmail.com> dateWed 17 Jul 2019, 17:48 (7 years ago) parente8c909b
8 files changed+213−208
src/indigo-react/components/CheckboxInput.js+1 src/indigo-react/components/Input.js+1 src/indigo-react/components/SelectInput.js+1 src/indigo-react/components/ToggleInput.js+1 src/indigo-react/lib/useForm.js+6 src/lib/useInputs.js+23−10 src/views/CreateGalaxy.js+166−186 src/views/Points.js+14−12
src/indigo-react/components/CheckboxInput.js+1
@@ -15,6 +15,7 @@ export default function CheckboxInput({
1515 // state from hook
1616 focused,
1717 pass,
18+ syncPass,
1819 visiblyPassed,
1920 error,
2021 hintError,
src/indigo-react/components/Input.js+1
@@ -18,6 +18,7 @@ export default function Input({
1818 // state from hook
1919 focused,
2020 pass,
21+ syncPass,
2122 visiblyPassed,
2223 error,
2324 hintError,
src/indigo-react/components/SelectInput.js+1
@@ -22,6 +22,7 @@ export default function SelectInput({
2222 // state from hook
2323 focused,
2424 pass,
25+ syncPass,
2526 visiblyPassed,
2627 error,
2728 hintError,
src/indigo-react/components/ToggleInput.js+1
@@ -17,6 +17,7 @@ export default function ToggleInput({
1717 // state from hook
1818 focused,
1919 pass,
20+ syncPass,
2021 visiblyPassed,
2122 error,
2223 hintError,
src/indigo-react/lib/useForm.js+6
@@ -141,6 +141,10 @@ export default function useForm(inputConfigs = []) {
141141
142142 // memo-compute the set of (perhaps changed by validation) data
143143 const datas = useMemo(() => validations.map(v => v.data), [validations]);
144+ const syncPasses = useMemo(
145+ () => validations.map(v => v.pass, [validations]),
146+ [validations]
147+ );
144148
145149 // the input has errored if it
146150 // 1) did not pass validation and has an error text
@@ -214,6 +218,7 @@ export default function useForm(inputConfigs = []) {
214218 value: value,
215219 data: passes[i] ? datas[i] : undefined,
216220 pass: passes[i],
221+ syncPass: syncPasses[i],
217222 visiblyPassed: visiblePasses[i],
218223 error: errors[i],
219224 hintError: hintErrors[i],
@@ -237,6 +242,7 @@ export default function useForm(inputConfigs = []) {
237242 configs,
238243 datas,
239244 passes,
245+ syncPasses,
240246 visiblePasses,
241247 errors,
242248 hintErrors,
src/lib/useInputs.js+23−10
@@ -145,29 +145,33 @@ export function useTicketInput({ validators = [], deriving = false, ...rest }) {
145145 );
146146}
147147
148−const kPointValidators = [
149− validatePoint,
150− validateMaximumPatpByteLength(4),
151− validateNotEmpty,
152−];
153148const kPointTransformers = [prependSig];
154−export function usePointInput(rest) {
149+export function usePointInput({ size = 4, ...rest }) {
150+ const validators = useMemo(
151+ () => [
152+ validatePoint,
153+ validateMaximumPatpByteLength(size),
154+ validateNotEmpty,
155+ ],
156+ [size]
157+ );
158+
155159 return useFirstOf(
156160 useForm([
157161 {
158162 type: 'text',
159163 label: 'Point',
160164 placeholder: 'e.g. ~zod',
161− validators: kPointValidators,
165+ validators,
162166 transformers: kPointTransformers,
163167 mono: true,
164168 ...rest,
165169 },
166170 ]),
167− ({ error, pass, focused, data }) => ({
168− accessory: data ? (
171+ ({ error, pass, focused, value }) => ({
172+ accessory: value ? (
169173 <InputSigil
170− patp={data}
174+ patp={value}
171175 size={44}
172176 margin={8}
173177 pass={pass}
@@ -179,6 +183,15 @@ export function usePointInput(rest) {
179183 );
180184}
181185
186+export function useGalaxyInput(props) {
187+ return usePointInput({
188+ name: 'galaxy',
189+ label: 'Galaxy Name',
190+ size: 1,
191+ ...props,
192+ });
193+}
194+
182195export function useCheckboxInput({ initialValue, ...rest }) {
183196 return useFirstOf(
184197 useForm([
src/views/CreateGalaxy.js+166−186
@@ -1,198 +1,178 @@
1−import React from 'react';
2−import { Just, Nothing } from 'folktale/maybe';
1+import React, { useCallback, useEffect, useState } from 'react';
2+import { Nothing, Just } from 'folktale/maybe';
3+import cn from 'classnames';
4+import { Grid, Text, Input } from 'indigo-react';
35import * as azimuth from 'azimuth-js';
4−import * as ob from 'urbit-ob';
5−import * as need from '../lib/need';
6−
7−import { Button } from '../components/old/Base';
8−import { H1, P, Anchor } from '../components/old/Base';
9−import {
10− InnerLabel,
11− GalaxyInput,
12− AddressInput,
13− ValidatedSigil,
14− ShowBlockie,
15−} from '../components/old/Base';
16−import StatelessTransaction from '../components/old/StatelessTransaction';
17−
18−import { NETWORK_TYPES } from '../lib/network';
19−import { canDecodePatp } from '../lib/txn';
20−
21−import { isValidAddress, isZeroAddress } from '../lib/wallet';
22−
23−import { isValidGalaxy, compose } from '../lib/lib';
24−import { withNetwork } from '../store/network';
25−import { withWallet } from '../store/wallet';
26−import View from 'components/View';
27−
28−const buttonTriState = status => {
29− if (status === null) return 'blue';
30− if (status === false) return 'yellow';
31− if (status === true) return 'green';
32−};
33−
34−const buttonTriStateText = status => {
35− if (status === null) return 'Confirm Galaxy Availablility';
36− if (status === false) return 'Galaxy is Not Available';
37− if (status === true) return 'Galaxy is Available';
38−};
39−
40−class CreateGalaxy extends React.Component {
41− constructor(props) {
42− super(props);
43−
44− const galaxyOwner = need.addressFromWallet(props.wallet);
45−
46− this.state = {
47− galaxyOwner: galaxyOwner,
48− galaxyName: '',
49− isAvailable: null,
50− };
51−
52− this.handleAddressInput = this.handleAddressInput.bind(this);
53− this.handleGalaxyNameInput = this.handleGalaxyNameInput.bind(this);
54− this.confirmAvailability = this.confirmAvailability.bind(this);
55− this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
56− this.statelessRef = React.createRef();
57− }
58−
59− handleGalaxyNameInput = galaxyName => {
60− this.setState({ galaxyName, isAvailable: null });
61− this.statelessRef.current.clearTxn();
62− };
63−
64− handleAddressInput = galaxyOwner => {
65− this.setState({ galaxyOwner });
66− this.statelessRef.current.clearTxn();
67− };
686
69− createUnsignedTxn = () => {
70− const { state, props } = this;
71− if (isValidAddress(state.galaxyOwner) === false) return Nothing();
72− if (state.isAvailable === false) return Nothing();
73− if (canDecodePatp(state.galaxyName) === false) return Nothing();
74− if (isValidGalaxy(state.galaxyName) === false) return Nothing();
7+import { useNetwork } from 'store/network';
8+import { useHistory } from 'store/history';
9+import { usePointCache } from 'store/pointCache';
7510
76− const validContracts = need.contracts(props.contracts);
11+import * as need from 'lib/need';
12+import { useAddressInput, useGalaxyInput } from 'lib/useInputs';
13+import useEthereumTransaction from 'lib/useEthereumTransaction';
14+import { GAS_LIMITS } from 'lib/constants';
15+import patp2dec from 'lib/patp2dec';
7716
78− const galaxyDec = ob.patp2dec(state.galaxyName);
79−
80− const txn = azimuth.ecliptic.createGalaxy(
81− validContracts,
82− galaxyDec,
83− state.galaxyOwner
84− );
85−
86− return Just(txn);
17+import ViewHeader from 'components/ViewHeader';
18+import MiniBackButton from 'components/MiniBackButton';
19+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
20+import View from 'components/View';
21+import { isZeroAddress } from 'lib/wallet';
22+
23+function useCreateGalaxy() {
24+ const { contracts } = useNetwork();
25+ const { syncKnownPoint } = usePointCache();
26+
27+ const _contracts = need.contracts(contracts);
28+
29+ const [galaxy, setGalaxy] = useState();
30+
31+ const {
32+ construct: _construct,
33+ unconstruct,
34+ confirmed,
35+ inputsLocked,
36+ bind,
37+ } = useEthereumTransaction(GAS_LIMITS.DEFAULT);
38+
39+ const construct = useCallback(
40+ (galaxy, owner) => {
41+ setGalaxy(galaxy);
42+ _construct(azimuth.ecliptic.createGalaxy(_contracts, galaxy, owner));
43+ },
44+ [_construct, _contracts]
45+ );
46+
47+ useEffect(() => {
48+ if (confirmed) {
49+ syncKnownPoint(galaxy);
50+ }
51+ }, [confirmed, galaxy, syncKnownPoint]);
52+
53+ return {
54+ construct,
55+ unconstruct,
56+ confirmed,
57+ inputsLocked,
58+ bind,
8759 };
60+}
8861
89− confirmAvailability = async () => {
90− const { state, props } = this;
62+export default function CreateGalaxy() {
63+ const { pop } = useHistory();
64+ const { contracts } = useNetwork();
65+ const _contracts = need.contracts(contracts);
66+
67+ const [error, setError] = useState();
68+ const [isAvailable, setIsAvailable] = useState(Nothing());
69+
70+ const {
71+ construct,
72+ unconstruct,
73+ confirmed,
74+ inputsLocked,
75+ bind,
76+ } = useCreateGalaxy();
77+
78+ const [
79+ galaxyNameInput,
80+ { pass: validGalaxyName, syncPass: syncValidGalaxyName, value: galaxyName },
81+ // ^ we use value: here so our effect runs onChange
82+ ] = useGalaxyInput({
83+ disabled: inputsLocked,
84+ autoFocus: true,
85+ error:
86+ error ||
87+ isAvailable.matchWith({
88+ Nothing: () => 'Loading availabiliy...', // TODO: make async loading?
89+ Just: p => (p.value ? undefined : 'This galaxy is already owned'),
90+ }),
91+ });
92+
93+ const [ownerInput, { pass: validOwner, data: owner }] = useAddressInput({
94+ name: 'owner',
95+ label: `Ethereum Address`,
96+ disabled: inputsLocked,
97+ });
98+
99+ useEffect(() => {
100+ if (validGalaxyName && validOwner) {
101+ construct(patp2dec(galaxyName), owner);
102+ } else {
103+ unconstruct();
104+ }
105+ }, [owner, construct, unconstruct, validGalaxyName, validOwner, galaxyName]);
91106
92− if (canDecodePatp(state.galaxyName) === false) {
93− this.setState({ isAvailable: false });
107+ useEffect(() => {
108+ if (!syncValidGalaxyName || inputsLocked) {
94109 return;
95110 }
96111
97− const validContracts = need.contracts(props.contracts);
98−
99− const galaxyDec = ob.patp2dec(state.galaxyName);
100−
101− const currentOwner = await azimuth.azimuth.getOwner(
102− validContracts,
103− galaxyDec
104− );
105−
106− const available = isZeroAddress(currentOwner);
107−
108− this.setState({ isAvailable: available });
109− };
110−
111− render() {
112− const { props, state } = this;
113−
114− const validAddress = isValidAddress(state.galaxyOwner);
115− const validGalaxy = isValidGalaxy(state.galaxyName);
116−
117− const canGenerate =
118− validAddress === true &&
119− validGalaxy === true &&
120− state.isAvailable === true;
121−
122− const esvisible =
123− props.networkType === NETWORK_TYPES.ROPSTEN ||
124− props.networkType === NETWORK_TYPES.MAINNET;
125−
126− const esdomain =
127− props.networkType === NETWORK_TYPES.ROPSTEN
128− ? 'ropsten.etherscan.io'
129− : 'etherscan.io';
130−
131− return (
132− <View>
133− <H1> {'Create a Galaxy'} </H1>
134−
135− <P>
136− {'Enter the galaxy to create and the address that will own ' +
137− 'it (defaulting to this account, if not provided).'}
138− </P>
139−
140− <GalaxyInput
141− className="mono"
142− prop-size="lg"
143− prop-format="innerLabel"
144− autoFocus
145− placeholder="e.g. ~zod"
146− value={state.galaxyName}
147− onChange={v => this.handleGalaxyNameInput(v)}>
148− <InnerLabel>{'Galaxy Name'}</InnerLabel>
149− <ValidatedSigil
150− className="tr-0 mt-05 mr-0 abs"
151− patp={state.galaxyName}
152− size={68}
153− margin={8}
154− />
155− </GalaxyInput>
156−
157− <AddressInput
158− className="mono mt-8"
159− prop-size="lg"
160− prop-format="innerLabel"
161− value={state.galaxyOwner}
162− onChange={v => this.handleAddressInput(v)}>
163− <InnerLabel>{'Address that will own this galaxy'}</InnerLabel>
164− <ShowBlockie className={'mt-1'} address={state.galaxyOwner} />
165− </AddressInput>
166−
167− <Anchor
168− className={'mt-1'}
169− prop-size={'sm'}
170− prop-disabled={!validAddress || !esvisible}
171− target={'_blank'}
172− href={`https://${esdomain}/address/${state.galaxyOwner}`}>
173− {'View on Etherscan ↗'}
174− </Anchor>
175−
176− <Button
177− prop-size="lg wide"
178− className="mt-8"
179− prop-color={buttonTriState(state.isAvailable)}
180− disabled={!validGalaxy}
181− onClick={() => this.confirmAvailability()}>
182− {buttonTriStateText(state.isAvailable)}
183− </Button>
184−
185− <StatelessTransaction
186− ref={this.statelessRef}
187− canGenerate={canGenerate}
188− createUnsignedTxn={this.createUnsignedTxn}
112+ setIsAvailable(Nothing());
113+
114+ let cancelled = false;
115+
116+ (async () => {
117+ try {
118+ const currentOwner = await azimuth.azimuth.getOwner(
119+ _contracts,
120+ patp2dec(galaxyName)
121+ );
122+
123+ const isAvailable = isZeroAddress(currentOwner);
124+
125+ if (cancelled) {
126+ return;
127+ }
128+
129+ setIsAvailable(Just(isAvailable));
130+ } catch (error) {
131+ console.error(error);
132+ setError(error.message);
133+ setIsAvailable(Just(false));
134+ }
135+ })();
136+
137+ return () => (cancelled = true);
138+ }, [
139+ _contracts,
140+ galaxyName,
141+ inputsLocked,
142+ setIsAvailable,
143+ syncValidGalaxyName,
144+ ]);
145+
146+ return (
147+ <View inset>
148+ <Grid>
149+ <Grid.Item full as={MiniBackButton} onClick={() => pop()} />
150+
151+ <Grid.Item full as={ViewHeader}>
152+ Create a Galaxy
153+ </Grid.Item>
154+
155+ {confirmed && (
156+ <Grid.Item
157+ full
158+ as={Text}
159+ className={cn('f5', {
160+ green3: confirmed,
161+ })}>
162+ {galaxyName} has been created and is owned by {owner}.
163+ </Grid.Item>
164+ )}
165+
166+ <Grid.Item full as={Input} {...galaxyNameInput} className="mt4" />
167+ <Grid.Item full as={Input} {...ownerInput} className="mb4" />
168+
169+ <Grid.Item
170+ full
171+ as={InlineEthereumTransaction}
172+ {...bind}
173+ onReturn={() => pop()}
189174 />
190− </View>
191− );
192− }
175+ </Grid>
176+ </View>
177+ );
193178}
194−
195−export default compose(
196− withNetwork,
197− withWallet
198−)(CreateGalaxy);
src/views/Points.js+14−12
@@ -78,6 +78,7 @@ export default function Points() {
7878 </P>
7979
8080 <Grid>
81+ <Grid.Divider />
8182 <Grid.Item full>
8283 <ForwardButton
8384 detail="View a point on Azimuth"
@@ -85,20 +86,21 @@ export default function Points() {
8586 View a point
8687 </ForwardButton>
8788 </Grid.Item>
89+ <Grid.Divider />
90+ {isEclipticOwner && (
91+ <>
92+ <Grid.Item full>
93+ <ForwardButton
94+ detail="You have the authority to create a new Galaxy."
95+ onClick={() => history.push(ROUTE_NAMES.CREATE_GALAXY)}>
96+ Create a galaxy
97+ </ForwardButton>
98+ </Grid.Item>
99+ <Grid.Divider />
100+ </>
101+ )}
88102 </Grid>
89103
90− {isEclipticOwner && (
91− <Grid>
92− <Grid.Item full>
93− <ForwardButton
94− detail="You have the authority to create a new Galaxy."
95− onClick={() => history.push(ROUTE_NAMES.CREATE_GALAXY)}>
96− Create a galaxy
97− </ForwardButton>
98− </Grid.Item>
99− </Grid>
100− )}
101−
102104 {incoming.length > 0 && (
103105 <>
104106 <H2>Incoming Transfers</H2>