@@ -1,28 +1,37 @@
1+import React, { useCallback, useMemo, useRef } from 'react';
12import { Just, Nothing } from 'folktale/maybe';
23import cn from 'classnames';
3−import React, { useCallback, useState, useEffect } from 'react';
44import * as azimuth from 'azimuth-js';
5−import * as ob from 'urbit-ob';
65import * as kg from 'urbit-key-generation/dist/index';
7−import { Input, Grid, CheckboxInput } from 'indigo-react';
6+import { Grid, CheckboxInput } from 'indigo-react';
7+import { FORM_ERROR } from 'final-form';
88
99import { useNetwork } from 'store/network';
1010import { useWallet } from 'store/wallet';
1111import { usePointCursor } from 'store/pointCursor';
1212
13−import {
14− usePointInput,
15− useTicketInput,
16− usePassphraseInput,
17− useCheckboxInput,
18−} from 'lib/useInputs';
1913import * as need from 'lib/need';
2014import { WALLET_TYPES, urbitWalletFromTicket } from 'lib/wallet';
2115import useImpliedPoint from 'lib/useImpliedPoint';
2216import useLoginView from 'lib/useLoginView';
2317import patp2dec from 'lib/patp2dec';
24−
25−export default function Ticket({ className }) {
18+import BridgeForm from 'form/BridgeForm';
19+import Condition from 'form/Condition';
20+import {
21+ TicketInput,
22+ PassphraseInput,
23+ PointInput,
24+ composeValidator,
25+ buildCheckboxValidator,
26+ buildTicketValidator,
27+ buildPassphraseValidator,
28+ buildPointValidator,
29+} from 'form/Inputs';
30+import timeout from 'lib/timeout';
31+import FormError from 'form/FormError';
32+import ContinueButton from './ContinueButton';
33+
34+export default function Ticket({ className, goHome }) {
2635 useLoginView(WALLET_TYPES.TICKET);
2736
2837 const { contracts } = useNetwork();
@@ -30,158 +39,171 @@ export default function Ticket({ className }) {
3039 const { setPointCursor } = usePointCursor();
3140 const impliedPoint = useImpliedPoint();
3241
33− // point
34− const [pointInput, { data: pointName }] = usePointInput({
35− name: 'point',
36− initialValue: impliedPoint || '',
37− autoFocus: true,
38− });
39−
40− // passphrase
41− const [passphraseInput, { data: passphrase }] = usePassphraseInput({
42− name: 'passphrase',
43− label: 'Wallet Passphrase',
44− });
45−
46− const [hasPassphraseInput, { data: hasPassphrase }] = useCheckboxInput({
47− name: 'has-passphrase',
48− label: 'Passphrase',
49− initialValue: false,
50− });
51−
52− // ticket
53− const [error, setError] = useState();
54− const [deriving, setDeriving] = useState(false);
55− const [ticketInput, { data: ticket, pass: validTicket }] = useTicketInput({
56− name: 'ticket',
57− label: 'Master Ticket',
58− error,
59− deriving,
60− });
61−
62− // shards
63− const [shardsInput, { data: isUsingShards }] = useCheckboxInput({
64− name: 'shards',
65− label: 'Shards',
66− initialValue: false,
67− });
68−
69− const [shard1Input, { data: shard1, pass: shard1Pass }] = useTicketInput({
70− name: 'shard1',
71− label: 'Shard 1',
72− });
73−
74− const [shard2Input, { data: shard2, pass: shard2Pass }] = useTicketInput({
75− name: 'shard2',
76− label: 'Shard 2',
77− });
78−
79− const [shard3Input, { data: shard3, pass: shard3Pass }] = useTicketInput({
80− name: 'shard3',
81− label: 'Shard 3',
82− });
83−
84− const shardsReady = shard1Pass && shard2Pass && shard3Pass;
85−
86− // TODO: maybe want to do this only on-go, because wallet derivation is slow...
87− const deriveWalletFromTicket = useCallback(async () => {
88− // clear states
89− setError();
90− setDeriving(true);
91− setUrbitWallet(Nothing());
92−
93− if (
94− !ticket ||
95− !pointName ||
96− !ob.isValidPatq(ticket) ||
97− !ob.isValidPatp(pointName)
98− ) {
99− setDeriving(false);
100− return;
101− }
102−
103− const _contracts = need.contracts(contracts);
104− const point = patp2dec(pointName);
105− const urbitWallet = await urbitWalletFromTicket(ticket, point, passphrase);
106− const [isOwner, isTransferProxy] = await Promise.all([
107− azimuth.azimuth.isOwner(
108− _contracts,
109− point,
110− urbitWallet.ownership.keys.address
111− ),
112− azimuth.azimuth.isTransferProxy(
113− _contracts,
114− point,
115− urbitWallet.ownership.keys.address
42+ const cachedUrbitWallet = useRef(Nothing());
43+
44+ const validateForm = useCallback(
45+ async (values, errors) => {
46+ if (errors.point) {
47+ return errors;
48+ }
49+
50+ if (values.useShards) {
51+ if (errors.shard1 || errors.shard2 || errors.shard3) {
52+ return errors;
53+ }
54+
55+ // shards
56+ try {
57+ const ticket = kg.combine([
58+ values.shard1,
59+ values.shard2,
60+ values.shard3,
61+ ]);
62+ const point = patp2dec(values.point);
63+ cachedUrbitWallet.current = await urbitWalletFromTicket(
64+ ticket,
65+ point,
66+ values.passphrase
67+ );
68+ } catch (error) {
69+ console.error(error);
70+ return { [FORM_ERROR]: 'Unable to derive wallet from shards.' };
71+ }
72+ } else {
73+ if (errors.ticket) {
74+ return errors;
75+ }
76+
77+ await timeout(100); // allow ui events to flush
78+ try {
79+ // ticket
80+ const _contracts = need.contracts(contracts);
81+ const point = patp2dec(values.point);
82+
83+ console.log('computing...');
84+ const urbitWallet = await urbitWalletFromTicket(
85+ values.ticket,
86+ point,
87+ values.passphrase
88+ );
89+
90+ cachedUrbitWallet.current = urbitWallet;
91+
92+ const [isOwner, isTransferProxy] = await Promise.all([
93+ azimuth.azimuth.isOwner(
94+ _contracts,
95+ point,
96+ urbitWallet.ownership.keys.address
97+ ),
98+ azimuth.azimuth.isTransferProxy(
99+ _contracts,
100+ point,
101+ urbitWallet.ownership.keys.address
102+ ),
103+ ]);
104+
105+ if (!isOwner && !isTransferProxy) {
106+ // notify the user, but allow login regardless
107+ // TODO: warnings
108+ // 'This ticket is not the owner of or transfer proxy for this point.'
109+ }
110+ } catch (error) {
111+ console.error(error);
112+ return { ticket: 'Unable to derive wallet from ticket.' };
113+ }
114+ }
115+ },
116+ [contracts]
117+ );
118+
119+ const validate = useMemo(
120+ () =>
121+ composeValidator(
122+ {
123+ usePassphrase: buildCheckboxValidator(),
124+ useShards: buildCheckboxValidator(),
125+ point: buildPointValidator(4),
126+ ticket: buildTicketValidator(),
127+ shard1: buildTicketValidator(),
128+ shard2: buildTicketValidator(),
129+ shard3: buildTicketValidator(),
130+ passphrase: buildPassphraseValidator(),
131+ },
132+ validateForm
116133 ),
117− ]);
118−
119− if (!isOwner && !isTransferProxy) {
120− // notify the user, but allow login regardless
121− setError(
122− 'This ticket is not the owner of or transfer proxy for this point.'
123− );
124− }
125−
126− setUrbitWallet(Just(urbitWallet));
127− setPointCursor(Just(point));
128− setDeriving(false);
129− }, [
130− pointName,
131− ticket,
132− passphrase,
133− contracts,
134− setUrbitWallet,
135− setPointCursor,
136− setDeriving,
137− ]);
138−
139− const deriveWalletFromShards = useCallback(async () => {
140− const s1 = shard1 || undefined;
141− const s2 = shard2 || undefined;
142− const s3 = shard3 || undefined;
143−
144− try {
145− const ticket = kg.combine([s1, s2, s3]);
146− const point = patp2dec(pointName);
147− const uhdw = await urbitWalletFromTicket(ticket, point, passphrase);
148− setUrbitWallet(Just(uhdw));
149− } catch (_) {
150− // do nothing
151− }
152− }, [passphrase, pointName, setUrbitWallet, shard1, shard2, shard3]);
153−
154− // derive wallet on change
155− useEffect(() => {
156− if (isUsingShards && shardsReady) {
157− deriveWalletFromShards();
158− } else if (validTicket) {
159− deriveWalletFromTicket();
160− }
161− }, [
162− isUsingShards,
163− validTicket,
164− shardsReady,
165− deriveWalletFromShards,
166− deriveWalletFromTicket,
167− ]);
134+ [validateForm]
135+ );
136+
137+ const onValues = useCallback(
138+ ({ valid, values }) => {
139+ if (valid) {
140+ setUrbitWallet(Just(cachedUrbitWallet.current));
141+ setPointCursor(Just(patp2dec(values.point)));
142+ } else {
143+ setUrbitWallet(Nothing());
144+ }
145+ },
146+ [setPointCursor, setUrbitWallet]
147+ );
168148
169149 return (
170150 <Grid className={cn('mt4', className)}>
171− <Grid.Item full as={Input} {...pointInput} />
172−
173− {!isUsingShards && <Grid.Item full as={Input} {...ticketInput} />}
174− {isUsingShards && (
175− <>
176− <Grid.Item full as={Input} {...shard1Input} />
177− <Grid.Item full as={Input} {...shard2Input} />
178− <Grid.Item full as={Input} {...shard3Input} />
179− </>
180− )}
181− {hasPassphrase && <Grid.Item full as={Input} {...passphraseInput} />}
182−
183− <Grid.Item full as={CheckboxInput} {...hasPassphraseInput} />
184− <Grid.Item full as={CheckboxInput} {...shardsInput} />
151+ <BridgeForm
152+ validate={validate}
153+ onValues={onValues}
154+ onSubmit={goHome}
155+ initialValues={{
156+ point: impliedPoint || '',
157+ usePasshrase: false,
158+ useShards: false,
159+ }}>
160+ {({ handleSubmit }) => (
161+ <>
162+ <Grid.Item full as={PointInput} name="point" />
163+
164+ <Condition when="useShards" is={false}>
165+ <Grid.Item
166+ full
167+ as={TicketInput}
168+ name="ticket"
169+ label="Master Ticket"
170+ />
171+ </Condition>
172+
173+ <Condition when="useShards" is={true}>
174+ <Grid.Item full as={TicketInput} name="shard1" label="Shard 1" />
175+ <Grid.Item full as={TicketInput} name="shard2" label="Shard 2" />
176+ <Grid.Item full as={TicketInput} name="shard3" label="Shard 3" />
177+ </Condition>
178+
179+ <Condition when="usePassphrase" is={true}>
180+ <Grid.Item
181+ full
182+ as={PassphraseInput}
183+ name="passphrase"
184+ label="Wallet Passphrase"
185+ />
186+ </Condition>
187+
188+ <Grid.Item
189+ full
190+ as={CheckboxInput}
191+ name="usePassphrase"
192+ label="Passphrase"
193+ />
194+ <Grid.Item
195+ full
196+ as={CheckboxInput}
197+ name="useShards"
198+ label="Shards"
199+ />
200+
201+ <Grid.Item full as={FormError} />
202+
203+ <Grid.Item full as={ContinueButton} handleSubmit={handleSubmit} />
204+ </>
205+ )}
206+ </BridgeForm>
185207 </Grid>
186208 );
187209}