on master help sign in
feat: Create Galaxy view with new InlineEthereumTransaction
2b8b988Matt Condon (shrugs)
src/indigo-react/components/CheckboxInput.js+1
@@ -15,6 +15,7 @@ export default function CheckboxInput({
15 // state from hook
15 // state from hook
16 focused,
16 focused,
17 pass,
17 pass,
18+ syncPass,
18 visiblyPassed,
19 visiblyPassed,
19 error,
20 error,
20 hintError,
21 hintError,
src/indigo-react/components/Input.js+1
@@ -18,6 +18,7 @@ export default function Input({
18 // state from hook
18 // state from hook
19 focused,
19 focused,
20 pass,
20 pass,
21+ syncPass,
21 visiblyPassed,
22 visiblyPassed,
22 error,
23 error,
23 hintError,
24 hintError,
src/indigo-react/components/SelectInput.js+1
@@ -22,6 +22,7 @@ export default function SelectInput({
22 // state from hook
22 // state from hook
23 focused,
23 focused,
24 pass,
24 pass,
25+ syncPass,
25 visiblyPassed,
26 visiblyPassed,
26 error,
27 error,
27 hintError,
28 hintError,
src/indigo-react/components/ToggleInput.js+1
@@ -17,6 +17,7 @@ export default function ToggleInput({
17 // state from hook
17 // state from hook
18 focused,
18 focused,
19 pass,
19 pass,
20+ syncPass,
20 visiblyPassed,
21 visiblyPassed,
21 error,
22 error,
22 hintError,
23 hintError,
src/indigo-react/lib/useForm.js+6
@@ -141,6 +141,10 @@ export default function useForm(inputConfigs = []) {
141
141
142 // memo-compute the set of (perhaps changed by validation) data
142 // memo-compute the set of (perhaps changed by validation) data
143 const datas = useMemo(() => validations.map(v => v.data), [validations]);
143 const datas = useMemo(() => validations.map(v => v.data), [validations]);
144+ const syncPasses = useMemo(
145+ () => validations.map(v => v.pass, [validations]),
146+ [validations]
147+ );
144
148
145 // the input has errored if it
149 // the input has errored if it
146 // 1) did not pass validation and has an error text
150 // 1) did not pass validation and has an error text
@@ -214,6 +218,7 @@ export default function useForm(inputConfigs = []) {
214 value: value,
218 value: value,
215 data: passes[i] ? datas[i] : undefined,
219 data: passes[i] ? datas[i] : undefined,
216 pass: passes[i],
220 pass: passes[i],
221+ syncPass: syncPasses[i],
217 visiblyPassed: visiblePasses[i],
222 visiblyPassed: visiblePasses[i],
218 error: errors[i],
223 error: errors[i],
219 hintError: hintErrors[i],
224 hintError: hintErrors[i],
@@ -237,6 +242,7 @@ export default function useForm(inputConfigs = []) {
237 configs,
242 configs,
238 datas,
243 datas,
239 passes,
244 passes,
245+ syncPasses,
240 visiblePasses,
246 visiblePasses,
241 errors,
247 errors,
242 hintErrors,
248 hintErrors,
src/lib/useInputs.js+23−10
@@ -145,29 +145,33 @@ export function useTicketInput({ validators = [], deriving = false, ...rest }) {
145 );
145 );
146}
146}
147
147
148−const kPointValidators = [
149− validatePoint,
150− validateMaximumPatpByteLength(4),
151− validateNotEmpty,
152−];
153const kPointTransformers = [prependSig];
148const 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+
155 return useFirstOf(
159 return useFirstOf(
156 useForm([
160 useForm([
157 {
161 {
158 type: 'text',
162 type: 'text',
159 label: 'Point',
163 label: 'Point',
160 placeholder: 'e.g. ~zod',
164 placeholder: 'e.g. ~zod',
161− validators: kPointValidators,
165+ validators,
162 transformers: kPointTransformers,
166 transformers: kPointTransformers,
163 mono: true,
167 mono: true,
164 ...rest,
168 ...rest,
165 },
169 },
166 ]),
170 ]),
167− ({ error, pass, focused, data }) => ({
171+ ({ error, pass, focused, value }) => ({
168− accessory: data ? (
172+ accessory: value ? (
169 <InputSigil
173 <InputSigil
170− patp={data}
174+ patp={value}
171 size={44}
175 size={44}
172 margin={8}
176 margin={8}
173 pass={pass}
177 pass={pass}
@@ -179,6 +183,15 @@ export function usePointInput(rest) {
179 );
183 );
180}
184}
181
185
186+export function useGalaxyInput(props) {
187+ return usePointInput({
188+ name: 'galaxy',
189+ label: 'Galaxy Name',
190+ size: 1,
191+ ...props,
192+ });
193+}
194+
182export function useCheckboxInput({ initialValue, ...rest }) {
195export function useCheckboxInput({ initialValue, ...rest }) {
183 return useFirstOf(
196 return useFirstOf(
184 useForm([
197 useForm([
src/views/CreateGalaxy.js+166−186
@@ -1,198 +1,178 @@
1−import React from 'react';
1+import React, { useCallback, useEffect, useState } from 'react';
2−import { Just, Nothing } from 'folktale/maybe';
2+import { Nothing, Just } from 'folktale/maybe';
3+import cn from 'classnames';
4+import { Grid, Text, Input } from 'indigo-react';
3import * as azimuth from 'azimuth-js';
5import * 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− };
68
6
69− createUnsignedTxn = () => {
7+import { useNetwork } from 'store/network';
70− const { state, props } = this;
8+import { useHistory } from 'store/history';
71− if (isValidAddress(state.galaxyOwner) === false) return Nothing();
9+import { usePointCache } from 'store/pointCache';
72− if (state.isAvailable === false) return Nothing();
73− if (canDecodePatp(state.galaxyName) === false) return Nothing();
74− if (isValidGalaxy(state.galaxyName) === false) return Nothing();
75
10
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';
77
16
78− const galaxyDec = ob.patp2dec(state.galaxyName);
17+import ViewHeader from 'components/ViewHeader';
79−
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);
80− const txn = azimuth.ecliptic.createGalaxy(
42+ _construct(azimuth.ecliptic.createGalaxy(_contracts, galaxy, owner));
81− validContracts,
43+ },
82− galaxyDec,
44+ [_construct, _contracts]
83− state.galaxyOwner
45+ );
84− );
46+
85−
47+ useEffect(() => {
48+ if (confirmed) {
49+ syncKnownPoint(galaxy);
50+ }
51+ }, [confirmed, galaxy, syncKnownPoint]);
52+
86− return Just(txn);
53+ return {
54+ construct,
55+ unconstruct,
56+ confirmed,
57+ inputsLocked,
58+ bind,
87 };
59 };
60+}
88
61
89− confirmAvailability = async () => {
62+export default function CreateGalaxy() {
90− const { state, props } = this;
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]);
91
106
92− if (canDecodePatp(state.galaxyName) === false) {
107+ useEffect(() => {
93− this.setState({ isAvailable: false });
108+ if (!syncValidGalaxyName || inputsLocked) {
94 return;
109 return;
95 }
110 }
96
111
97− const validContracts = need.contracts(props.contracts);
112+ setIsAvailable(Nothing());
98−
113+
99− const galaxyDec = ob.patp2dec(state.galaxyName);
114+ let cancelled = false;
100−
115+
116+ (async () => {
117+ try {
101− const currentOwner = await azimuth.azimuth.getOwner(
118+ const currentOwner = await azimuth.azimuth.getOwner(
102− validContracts,
119+ _contracts,
103− galaxyDec
120+ patp2dec(galaxyName)
104− );
121+ );
105−
122+
106− const available = isZeroAddress(currentOwner);
123+ const isAvailable = isZeroAddress(currentOwner);
107−
124+
108− this.setState({ isAvailable: available });
125+ if (cancelled) {
126+ return;
109− };
127+ }
110−
128+
111− render() {
129+ setIsAvailable(Just(isAvailable));
112− const { props, state } = this;
130+ } catch (error) {
113−
131+ console.error(error);
114− const validAddress = isValidAddress(state.galaxyOwner);
132+ setError(error.message);
115− const validGalaxy = isValidGalaxy(state.galaxyName);
133+ setIsAvailable(Just(false));
116−
134+ }
117− const canGenerate =
135+ })();
118− validAddress === true &&
136+
119− validGalaxy === true &&
137+ return () => (cancelled = true);
120− state.isAvailable === true;
138+ }, [
121−
139+ _contracts,
122− const esvisible =
140+ galaxyName,
123− props.networkType === NETWORK_TYPES.ROPSTEN ||
141+ inputsLocked,
124− props.networkType === NETWORK_TYPES.MAINNET;
142+ setIsAvailable,
125−
143+ syncValidGalaxyName,
126− const esdomain =
144+ ]);
127− props.networkType === NETWORK_TYPES.ROPSTEN
145+
128− ? 'ropsten.etherscan.io'
146+ return (
129− : 'etherscan.io';
147+ <View inset>
130−
148+ <Grid>
131− return (
149+ <Grid.Item full as={MiniBackButton} onClick={() => pop()} />
132− <View>
150+
151+ <Grid.Item full as={ViewHeader}>
133− <H1> {'Create a Galaxy'} </H1>
152+ Create a Galaxy
134−
153+ </Grid.Item>
135− <P>
154+
136− {'Enter the galaxy to create and the address that will own ' +
155+ {confirmed && (
137− 'it (defaulting to this account, if not provided).'}
156+ <Grid.Item
138− </P>
157+ full
139−
158+ as={Text}
140− <GalaxyInput
159+ className={cn('f5', {
141− className="mono"
160+ green3: confirmed,
142− prop-size="lg"
161+ })}>
143− prop-format="innerLabel"
162+ {galaxyName} has been created and is owned by {owner}.
144− autoFocus
163+ </Grid.Item>
145− placeholder="e.g. ~zod"
164+ )}
146− value={state.galaxyName}
165+
147− onChange={v => this.handleGalaxyNameInput(v)}>
166+ <Grid.Item full as={Input} {...galaxyNameInput} className="mt4" />
148− <InnerLabel>{'Galaxy Name'}</InnerLabel>
167+ <Grid.Item full as={Input} {...ownerInput} className="mb4" />
149− <ValidatedSigil
168+
150− className="tr-0 mt-05 mr-0 abs"
169+ <Grid.Item
151− patp={state.galaxyName}
170+ full
152− size={68}
171+ as={InlineEthereumTransaction}
153− margin={8}
172+ {...bind}
154− />
173+ onReturn={() => pop()}
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}
189 />
174 />
175+ </Grid>
190− </View>
176+ </View>
191− );
177+ );
192− }
193}
178}
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() {
78 </P>
78 </P>
79
79
80 <Grid>
80 <Grid>
81+ <Grid.Divider />
81 <Grid.Item full>
82 <Grid.Item full>
82 <ForwardButton
83 <ForwardButton
83 detail="View a point on Azimuth"
84 detail="View a point on Azimuth"
@@ -85,20 +86,21 @@ export default function Points() {
85 View a point
86 View a point
86 </ForwardButton>
87 </ForwardButton>
87 </Grid.Item>
88 </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+ )}
88 </Grid>
102 </Grid>
89
103
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−
102 {incoming.length > 0 && (
104 {incoming.length > 0 && (
103 <>
105 <>
104 <H2>Incoming Transfers</H2>
106 <H2>Incoming Transfers</H2>