on master help sign in
Merge branch 'mino' into new-sigil-js
c50a97dFang
src/components/old/StatelessTransaction.jsdeleted−598
@@ -1,598 +0,0 @@
1−import { Nothing, Just } from 'folktale/maybe';
2−import { Ok } from 'folktale/result';
3−import React from 'react';
4−import * as need from '../../lib/need';
5−
6−import { Code, H3 } from './Base';
7−import { Button } from './Base';
8−import { CheckboxButton, Input, InnerLabel, InnerLabelDropdown } from './Base';
9−import { Warning } from './Base';
10−
11−import { GAS_LIMITS } from '../../lib/constants';
12−import { BRIDGE_ERROR } from '../../lib/error';
13−import { ROUTE_NAMES } from '../../lib/routeNames';
14−import { compose } from '../../lib/lib';
15−import * as tank from '../../lib/tank';
16−import {
17− sendSignedTransaction,
18− hexify,
19− renderSignedTx,
20− signTransaction,
21−} from '../../lib/txn';
22−
23−import { withNetwork } from '../../store/network';
24−import { withHistory } from '../../store/history';
25−import { withWallet } from '../../store/wallet';
26−import { withTxnCursor } from '../../store/txnCursor';
27−import { fromWei, toWei } from 'web3-utils';
28−
29−const SUBMISSION_STATES = {
30− PROMPT: 'Send transaction',
31− PREPARING: 'Preparing...',
32− FUNDING: 'Finding transaction funding...',
33− SENDING: 'Sending transaction...',
34−};
35−
36−class StatelessTransaction extends React.Component {
37− constructor(props) {
38− super(props);
39−
40− this.state = {
41− gasPrice: '5',
42− gasLimit: GAS_LIMITS.DEFAULT.toString(),
43− showGasDetails: false,
44− userApproval: false,
45− chainId: '1',
46− customChain: false,
47− nonce: '0',
48− stx: Nothing(),
49− txn: Nothing(),
50− txStatus: SUBMISSION_STATES.PROMPT,
51− txError: Nothing(),
52− };
53−
54− this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
55− this.setUserApproval = this.setUserApproval.bind(this);
56− this.setTxn = this.setTxn.bind(this);
57− this.setStx = this.setStx.bind(this);
58− this.sendTxn = this.sendTxn.bind(this);
59− this.setNonce = this.setNonce.bind(this);
60− this.setChainId = this.setChainId.bind(this);
61− this.setGasPrice = this.setGasPrice.bind(this);
62− this.setGasLimit = this.setGasLimit.bind(this);
63− this.rangeChange = this.rangeChange.bind(this);
64− this.toggleGasDetails = this.toggleGasDetails.bind(this);
65− this.handleChainUpdate = this.handleChainUpdate.bind(this);
66− }
67−
68− componentDidMount() {
69− const { props } = this;
70−
71− const addr = need.addressFromWallet(props.wallet);
72−
73− props.web3.matchWith({
74− Nothing: () => {},
75− Just: w3 => {
76− const validWeb3 = w3.value;
77−
78− const getTxMetadata = [
79− validWeb3.eth.getTransactionCount(addr),
80− validWeb3.eth.net.getId(),
81− validWeb3.eth.getGasPrice(),
82− ];
83−
84− Promise.all(getTxMetadata).then(r => {
85− const txMetadata = {
86− nonce: r[0],
87− chainId: r[1],
88− gasPrice: fromWei(r[2], 'gwei'),
89− };
90−
91− this.setState({ ...txMetadata });
92− });
93− },
94− });
95− }
96−
97− handleChainUpdate(chainId) {
98− if (chainId === 'custom') {
99− this.setState({
100− customChain: true,
101− });
102− } else {
103− this.setState({
104− customChain: false,
105− chainId,
106− });
107− }
108−
109− this.clearStx();
110− }
111−
112− setUserApproval() {
113− const { state } = this;
114− this.setState({ userApproval: !state.userApproval });
115− }
116−
117− toggleGasDetails() {
118− this.setState({
119− showGasDetails: !this.state.showGasDetails,
120− });
121− }
122−
123− setStx(stx) {
124− this.setState({
125− stx,
126− userApproval: false,
127− });
128− }
129−
130− setTxn(txn) {
131− this.setState({ txn });
132− }
133−
134− createUnsignedTxn() {
135− const txn = this.props.createUnsignedTxn();
136−
137− this.setState({ txn });
138− }
139−
140− clearTxn() {
141− this.setState({
142− userApproval: false,
143− txn: Nothing(),
144− stx: Nothing(),
145− });
146− }
147−
148− setNonce(nonce) {
149− this.setState({ nonce });
150− this.clearStx();
151− }
152−
153− setChainId(chainId) {
154− this.setState({ chainId });
155− this.clearStx();
156− }
157−
158− setGasPrice(gasPrice) {
159− this.setState({ gasPrice });
160− this.clearStx();
161− }
162−
163− rangeChange(e) {
164− this.setGasPrice(e.target.value);
165− }
166−
167− setGasLimit(gasLimit) {
168− this.setState({ gasLimit });
169− this.clearStx();
170− }
171−
172− clearStx() {
173− this.setState({
174− userApproval: false,
175− stx: Nothing(),
176− });
177− }
178−
179− async sendTxn() {
180− const { props, state } = this;
181− const web3 = props.web3.value;
182−
183− const stx = state.stx.matchWith({
184− Just: tx => tx.value,
185− Nothing: () => {
186− throw new Error(BRIDGE_ERROR.MISSING_TXN);
187− },
188− });
189−
190− if (props.beforeSend) {
191− try {
192− this.setState({ txStatus: SUBMISSION_STATES.PREPARING });
193− const res = await props.beforeSend(stx);
194− if (res === false) throw new Error('beforeSend disallowed sending');
195− } catch (e) {
196− console.log('beforeSend error', e);
197− this.setState({
198− txStatus: SUBMISSION_STATES.PROMPT,
199− txError: Just('Something went wrong!'),
200− });
201− return;
202− }
203− }
204−
205− this.setState({ txStatus: SUBMISSION_STATES.FUNDING });
206−
207− const rawTx = hexify(stx.serialize());
208− const cost = state.gasLimit * toWei(state.gasPrice, 'gwei');
209−
210− const address = need.addressFromWallet(props.wallet);
211− let balance = await web3.eth.getBalance(address);
212− let hasBalance = balance >= cost;
213−
214− let usedTank = false;
215− // if we need to, try and fund the transaction
216− if (!hasBalance) {
217− usedTank = await this.ensureFundsFor(web3, address, cost, [rawTx]);
218− hasBalance = usedTank;
219− }
220−
221− // if we still don't have sufficient balance, fail and tell the user
222− if (!hasBalance) {
223− this.setState({
224− txStatus: SUBMISSION_STATES.PROMPT,
225− txError: Just(`Insufficient funds.
226− Address ${address} needs at least ${fromWei(cost.toString())} ETH,
227− currently has ${fromWei(balance.toString())} ETH.`),
228− });
229−
230− // if we have the balance, proceed with submission
231− } else {
232− this.setState({ txStatus: SUBMISSION_STATES.SENDING });
233−
234− sendSignedTransaction(web3, stx, usedTank)
235− .then(txHash => {
236− props.setTxnCursor(Just(Ok(txHash)));
237−
238− let routeData = {};
239− if (props.newNetworkSeed) {
240− props.setNetworkSeed(Just(props.newNetworkSeed));
241− routeData.promptKeyfile = true;
242− }
243−
244− if (props.newRevision) {
245− props.setNetworkRevision(Just(props.newRevision));
246− }
247−
248− props.history.popAndPush(ROUTE_NAMES.SENT_TRANSACTION, routeData);
249− })
250− .catch(err => {
251− this.setState({
252− txStatus: SUBMISSION_STATES.PROMPT,
253− txError: Just(err),
254− });
255− });
256− }
257− }
258−
259− // uses the gas tank to ensure funds, but if we have to ask the user
260− // for funding, just resolve instead of waiting
261− // returns true if the gas tank was used, false otherwise
262− ensureFundsFor(web3, address, cost, signedTxs) {
263− return new Promise((resolve, reject) => {
264− tank
265− .ensureFundsFor(web3, null, address, cost, signedTxs, () => {
266− resolve(false);
267− })
268− .then(res => {
269− resolve(res);
270− });
271− });
272− }
273−
274− getChainTitle(chainId) {
275− let map = {
276− '1': 'Mainnet - 1',
277− '2': 'Morden - 2',
278− '3': 'Ropsten - 3',
279− '4': 'Goerli - 4',
280− '42': 'Kovan - 42',
281− '1337': 'Geth private chains - 1337',
282− custom: 'Custom',
283− };
284−
285− return map[chainId];
286− }
287−
288− getChainOptions() {
289− return [
290− {
291− title: this.getChainTitle('1'),
292− value: '1',
293− },
294− {
295− title: this.getChainTitle('2'),
296− value: '2',
297− },
298− {
299− title: this.getChainTitle('3'),
300− value: '3',
301− },
302− {
303− title: this.getChainTitle('4'),
304− value: '4',
305− },
306− {
307− title: this.getChainTitle('42'),
308− value: '42',
309− },
310− {
311− title: this.getChainTitle('1337'),
312− value: '1337',
313− },
314− {
315− title: this.getChainTitle('custom'),
316− value: 'custom',
317− },
318− ];
319− }
320−
321− render() {
322− const { web3, canGenerate } = this.props;
323− const {
324− gasPrice,
325− gasLimit,
326− nonce,
327− chainId,
328− txn,
329− stx,
330− userApproval,
331− showGasDetails,
332− customChain,
333− txStatus,
334− } = this.state;
335−
336− const {
337− setNonce,
338− setChainId,
339− setGasLimit,
340− setGasPrice,
341− toggleGasDetails,
342− setUserApproval,
343− sendTxn,
344− createUnsignedTxn,
345− handleChainUpdate,
346− } = this;
347−
348− const { state } = this;
349−
350− const canSign = Just.hasInstance(txn);
351− const canApprove = Just.hasInstance(stx);
352− const canSend = Just.hasInstance(stx) && userApproval === true;
353−
354− const chainOptions = this.getChainOptions();
355−
356− const generateButtonColor = Nothing.hasInstance(txn) ? 'blue' : 'green';
357−
358− const signerButtonColor = Nothing.hasInstance(stx) ? 'blue' : 'green';
359−
360− const generateTxnButton = (
361− <Button
362− className={'mt-8'}
363− disabled={!canGenerate}
364− prop-color={generateButtonColor}
365− prop-size={'lg wide'}
366− onClick={createUnsignedTxn}>
367− {'Generate Transaction'}
368− </Button>
369− );
370−
371− const unsignedTxnDisplay = txn.matchWith({
372− Nothing: _ => '',
373− Just: tx => (
374− <React.Fragment>
375− <H3 className={'mt-8'}>{'Unsigned Transaction'}</H3>
376− <Code>{JSON.stringify(tx.value, null, 2)}</Code>
377− </React.Fragment>
378− ),
379− });
380−
381− const gasPriceRangeDialogue = (
382− <React.Fragment>
383− <div className="mt-12 flex space-between align-baseline">
384− <div>
385− <span>Gas Price:</span>
386− <span className="ml-4 text-700 text-sm">{gasPrice} gwei</span>
387− </div>
388− <div className="text-sm">
389− <span>Max transaction fee: </span>
390− <span className="text-700">
391− {(gasPrice * gasLimit) / 1000000000} eth
392− </span>
393− </div>
394− </div>
395−
396− <input
397− className="mt-4"
398− type="range"
399− min="2"
400− max="20"
401− list="gweiVals"
402− value={gasPrice}
403− onChange={this.rangeChange}
404− />
405−
406− <div className="flex space-between text-sm mb-8">
407− <div>Cheap</div>
408− <div>Fast</div>
409− </div>
410− </React.Fragment>
411− );
412−
413− const toggleGasDetailsDialogue = (
414− <span className="text-link" onClick={toggleGasDetails}>
415− Gas Details
416− </span>
417− );
418−
419− const gasPriceDialogue = (
420− <Input
421− className={'mono mt-4'}
422− prop-size={'md'}
423− prop-format={'innerLabel'}
424− value={gasPrice}
425− onChange={setGasPrice}>
426− <InnerLabel>{'Gas Price (gwei)'}</InnerLabel>
427− </Input>
428− );
429−
430− const gasLimitDialogue = (
431− <Input
432− className={'mono mt-4'}
433− prop-size={'md'}
434− prop-format={'innerLabel'}
435− value={gasLimit}
436− onChange={setGasLimit}>
437− <InnerLabel>{'Gas Limit'}</InnerLabel>
438− </Input>
439− );
440−
441− const nonceDialogue = (
442− <Input
443− className={'mono mt-4 mb-4'}
444− prop-size={'md'}
445− prop-format={'innerLabel'}
446− value={nonce}
447− onChange={setNonce}>
448− <InnerLabel>{'Nonce'}</InnerLabel>
449− </Input>
450− );
451−
452− const chainDialogueTitle = customChain
453− ? 'Custom'
454− : this.getChainTitle(chainId);
455− const chainDialogue = (
456− <InnerLabelDropdown
457− className={'mt-6'}
458− fullWidth={true}
459− title={'Chain ID'}
460− options={chainOptions}
461− handleUpdate={handleChainUpdate}
462− currentSelectionTitle={chainDialogueTitle}
463− />
464− );
465−
466− const customChainDialogue = !customChain ? null : (
467− <Input
468− className={'mono mt-4 mb-8'}
469− prop-size={'md'}
470− prop-format={'innerLabel'}
471− value={chainId}
472− onChange={setChainId}>
473− <InnerLabel>{'Chain ID'}</InnerLabel>
474− </Input>
475− );
476−
477− const onlineParamsDialogue = web3.matchWith({
478− Just: _ => <div />,
479− Nothing: _ => (
480− <React.Fragment>
481− {nonceDialogue}
482− {chainDialogue}
483− {customChainDialogue}
484− </React.Fragment>
485− ),
486− });
487−
488− const signTxnButton = (
489− <Button
490− disabled={!canSign}
491− className={'mt-8'}
492− prop-size={'lg wide'}
493− prop-color={signerButtonColor}
494− onClick={async () =>
495− this.setStx(
496− Just(
497− await signTransaction({
498− ...this.props,
499− ...this.state,
500− wallet: this.props.wallet.value,
501− txn: txn.value,
502− })
503− )
504− )
505− }>
506− {'Sign Transaction'}
507− </Button>
508− );
509−
510− const signedTxnDisplay = stx.matchWith({
511− Nothing: _ => '',
512− Just: tx => (
513− <React.Fragment>
514− <H3 className={'mt-8'}>{'Signed Transaction'}</H3>
515− <Code>{JSON.stringify(renderSignedTx(tx.value), null, 2)}</Code>
516− </React.Fragment>
517− ),
518− });
519−
520− const confirmButton = (
521− <CheckboxButton
522− className={'mt-8'}
523− disabled={!canApprove}
524− onToggle={setUserApproval}
525− state={userApproval}
526− label="I approve this transaction and wish to send."
527− />
528− );
529−
530− const sending = txStatus !== SUBMISSION_STATES.PROMPT;
531− const sendTxnSpinner = sending ? '' : 'hide';
532− const sendTxnButton = (
533− <Button
534− prop-size={'xl wide'}
535− className={'mt-8'}
536− disabled={!canSend || sending}
537− onClick={sendTxn}>
538− <span className="relative">
539− <span className={`btn-spinner ${sendTxnSpinner}`} />
540− {txStatus}
541− </span>
542− </Button>
543− );
544−
545− const sendDialogue = web3.matchWith({
546− Nothing: _ => '',
547− Just: _ => (
548− <React.Fragment>
549− {confirmButton}
550− {sendTxnButton}
551− </React.Fragment>
552− ),
553− });
554−
555− const txnErrorDialogue = Nothing.hasInstance(state.txError) ? (
556− ''
557− ) : (
558− <Warning className={'mt-8'}>
559− <H3 style={{ marginTop: 0, paddingTop: 0 }}>
560− {'There was an error sending your transaction.'}
561− </H3>
562− {state.txError.value}
563− </Warning>
564− );
565−
566− return (
567− <React.Fragment>
568− {generateTxnButton}
569− {unsignedTxnDisplay}
570−
571− {gasPriceRangeDialogue}
572− {toggleGasDetailsDialogue}
573−
574− {showGasDetails && (
575− <div>
576− {gasPriceDialogue}
577− {gasLimitDialogue}
578− </div>
579− )}
580− {onlineParamsDialogue}
581−
582− {signTxnButton}
583−
584− {signedTxnDisplay}
585− {sendDialogue}
586−
587− {txnErrorDialogue}
588− </React.Fragment>
589− );
590− }
591−}
592−
593−export default compose(
594− withNetwork,
595− withHistory,
596− withWallet,
597− withTxnCursor
598−)(StatelessTransaction);
src/lib/routeNames.js−1
@@ -26,7 +26,6 @@ export const ROUTE_NAMES = {
26 ISSUE_CHILD: Symbol('ISSUE_CHILD'),
26 ISSUE_CHILD: Symbol('ISSUE_CHILD'),
27 SET_KEYS: Symbol('SET_KEYS'),
27 SET_KEYS: Symbol('SET_KEYS'),
28 TRANSFER: Symbol('TRANSFER'),
28 TRANSFER: Symbol('TRANSFER'),
29− SENT_TRANSACTION: Symbol('SENT_TRANSACTION'),
30 GEN_KEYFILE: Symbol('GEN_KEYFILE'),
29 GEN_KEYFILE: Symbol('GEN_KEYFILE'),
31 PARTY_SET_POOL_SIZE: Symbol('PARTY_SET_POOL_SIZE'),
30 PARTY_SET_POOL_SIZE: Symbol('PARTY_SET_POOL_SIZE'),
32};
31};
src/lib/router.js+15−33
@@ -1,50 +1,32 @@
1−import Landing from '../views/Landing';
1+import Landing from 'views/Landing';
2−import Disclaimer from '../views/Disclaimer';
2+import Disclaimer from 'views/Disclaimer';
3−import Login from '../views/Login.js';
3+import Login from 'views/Login.js';
4−import InvitesManage from '../views/InvitesManage.js';
5−import Admin from '../views/Admin';
4+import Admin from 'views/Admin';
6−import AcceptTransfer from '../views/AcceptTransfer';
7−import CancelTransfer from '../views/CancelTransfer';
8−import CreateGalaxy from '../views/CreateGalaxy';
5+import CreateGalaxy from 'views/CreateGalaxy';
9−import IssueChild from '../views/IssueChild';
6+import IssueChild from 'views/IssueChild';
10−import ViewPoint from '../views/ViewPoint';
7+import ViewPoint from 'views/ViewPoint';
11−import SentTransaction from '../views/SentTransaction';
8+
12−import SetKeys from '../views/SetKeys';
13−import {
14− SetManagementProxy,
15− SetSpawnProxy,
16− SetTransferProxy,
17−} from '../views/SetProxy';
18−import Point from '../views/Point';
9+import Point from 'views/Point';
19−import Points from '../views/Points';
10+import Points from 'views/Points';
20−import Transfer from '../views/Transfer';
21import Invite from 'views/Invite';
11import Invite from 'views/Invite';
22import Activate from 'views/Activate';
12import Activate from 'views/Activate';
23
13
24−import { ROUTE_NAMES } from './routeNames';
25import PartySetPoolSize from 'views/Party/PartySetPoolSize';
14import PartySetPoolSize from 'views/Party/PartySetPoolSize';
26
15
16+import { ROUTE_NAMES } from './routeNames';
17+
27export const ROUTES = {
18export const ROUTES = {
28 [ROUTE_NAMES.LANDING]: Landing,
19 [ROUTE_NAMES.LANDING]: Landing,
29 [ROUTE_NAMES.DISCLAIMER]: Disclaimer,
20 [ROUTE_NAMES.DISCLAIMER]: Disclaimer,
30 [ROUTE_NAMES.ACTIVATE]: Activate,
21 [ROUTE_NAMES.ACTIVATE]: Activate,
31− [ROUTE_NAMES.INVITE]: Invite,
32− [ROUTE_NAMES.INVITES_MANAGE]: InvitesManage,
33− [ROUTE_NAMES.LOGIN]: Login,
34− [ROUTE_NAMES.ADMIN]: Admin,
35− [ROUTE_NAMES.ACCEPT_TRANSFER]: AcceptTransfer,
36− [ROUTE_NAMES.CANCEL_TRANSFER]: CancelTransfer,
37 [ROUTE_NAMES.VIEW_POINT]: ViewPoint,
22 [ROUTE_NAMES.VIEW_POINT]: ViewPoint,
23+ [ROUTE_NAMES.LOGIN]: Login,
38 [ROUTE_NAMES.POINTS]: Points,
24 [ROUTE_NAMES.POINTS]: Points,
39 [ROUTE_NAMES.POINT]: Point,
25 [ROUTE_NAMES.POINT]: Point,
40− [ROUTE_NAMES.SET_MANAGEMENT_PROXY]: SetManagementProxy,
41− [ROUTE_NAMES.SET_SPAWN_PROXY]: SetSpawnProxy,
42− [ROUTE_NAMES.SET_TRANSFER_PROXY]: SetTransferProxy,
43 [ROUTE_NAMES.CREATE_GALAXY]: CreateGalaxy,
26 [ROUTE_NAMES.CREATE_GALAXY]: CreateGalaxy,
44 [ROUTE_NAMES.ISSUE_CHILD]: IssueChild,
27 [ROUTE_NAMES.ISSUE_CHILD]: IssueChild,
45− [ROUTE_NAMES.SET_KEYS]: SetKeys,
46− [ROUTE_NAMES.TRANSFER]: Transfer,
47− [ROUTE_NAMES.SENT_TRANSACTION]: SentTransaction,
48 [ROUTE_NAMES.PARTY_SET_POOL_SIZE]: PartySetPoolSize,
28 [ROUTE_NAMES.PARTY_SET_POOL_SIZE]: PartySetPoolSize,
29+ [ROUTE_NAMES.INVITE]: Invite,
30+ [ROUTE_NAMES.ADMIN]: Admin,
49 // TODO: nest this route under the Party.js router
31 // TODO: nest this route under the Party.js router
50};
32};
src/lib/useEthereumTransaction.js+8−2
@@ -217,8 +217,14 @@ export default function useEthereumTransaction(
217 try {
217 try {
218 await refetch();
218 await refetch();
219 } catch (error) {
219 } catch (error) {
220− // NOTE: we don't handle this error on purpose
220+ // log the original
221 console.error(error);
221 console.error(error);
222+ // track a user-friendly error
223+ setError(
224+ new Error(
225+ 'The transaction succeeded but we were unable to refresh chain state. Refresh to continue.'
226+ )
227+ );
222 }
228 }
223 }
229 }
224
230
@@ -231,7 +237,7 @@ export default function useEthereumTransaction(
231 }
237 }
232
238
233 return () => (cancelled = true);
239 return () => (cancelled = true);
234− }, [confirmed, refetch, completed]);
240+ }, [confirmed, refetch, completed, setError]);
235
241
236 const values = useDeepEqualReference({
242 const values = useDeepEqualReference({
237 isDefaultState,
243 isDefaultState,
src/lib/useInputs.js+5−5
@@ -146,14 +146,15 @@ export function useTicketInput({ validators = [], deriving = false, ...rest }) {
146}
146}
147
147
148const kPointTransformers = [prependSig];
148const kPointTransformers = [prependSig];
149−export function usePointInput({ size = 4, ...rest }) {
149+export function usePointInput({ size = 4, validators = [], ...rest }) {
150− const validators = useMemo(
150+ const _validators = useMemo(
151 () => [
151 () => [
152+ ...validators,
152 validatePoint,
153 validatePoint,
153 validateMaximumPatpByteLength(size),
154 validateMaximumPatpByteLength(size),
154 validateNotEmpty,
155 validateNotEmpty,
155 ],
156 ],
156− [size]
157+ [size, validators]
157 );
158 );
158
159
159 return useFirstOf(
160 return useFirstOf(
@@ -162,7 +163,7 @@ export function usePointInput({ size = 4, ...rest }) {
162 type: 'text',
163 type: 'text',
163 label: 'Point',
164 label: 'Point',
164 placeholder: 'e.g. ~zod',
165 placeholder: 'e.g. ~zod',
165− validators,
166+ validators: _validators,
166 transformers: kPointTransformers,
167 transformers: kPointTransformers,
167 mono: true,
168 mono: true,
168 ...rest,
169 ...rest,
@@ -185,7 +186,6 @@ export function usePointInput({ size = 4, ...rest }) {
185
186
186export function useGalaxyInput(props) {
187export function useGalaxyInput(props) {
187 return usePointInput({
188 return usePointInput({
188− name: 'galaxy',
189 label: 'Galaxy Name',
189 label: 'Galaxy Name',
190 size: 1,
190 size: 1,
191 ...props,
191 ...props,
src/lib/validators.js+7
@@ -245,6 +245,13 @@ export const validateGreaterThan = l => m =>
245 error: `Must be at least ${l}.`,
245 error: `Must be at least ${l}.`,
246 });
246 });
247
247
248+export const validateInSet = (set, error) => m =>
249+ simpleValidatorWrapper({
250+ prevMessage: m,
251+ validator: d => set.has(d),
252+ error,
253+ });
254+
248export const validatePatpByteLength = byteLength => {
255export const validatePatpByteLength = byteLength => {
249 return validateLength(patpStringLength(byteLength));
256 return validateLength(patpStringLength(byteLength));
250};
257};
src/store/lib/usePointStore.js+1
@@ -48,6 +48,7 @@ export default function usePointStore() {
48 ...controlledPoints,
48 ...controlledPoints,
49 ...ecliptic,
49 ...ecliptic,
50 syncDetails,
50 syncDetails,
51+ syncRekeyDate,
51 syncInvites,
52 syncInvites,
52 syncControlledPoints,
53 syncControlledPoints,
53 syncKnownPoint,
54 syncKnownPoint,
src/views/AcceptTransfer.jsdeleted−121
@@ -1,121 +0,0 @@
1−import { Just } from 'folktale/maybe';
2−import React from 'react';
3−import { H1, P } from '../components/old/Base';
4−import {
5− InnerLabel,
6− AddressInput,
7− ShowBlockie,
8− Anchor,
9−} from '../components/old/Base';
10−import * as azimuth from 'azimuth-js';
11−import * as ob from 'urbit-ob';
12−import * as need from '../lib/need';
13−
14−import StatelessTransaction from '../components/old/StatelessTransaction';
15−import { NETWORK_TYPES } from '../lib/network';
16−import { isValidAddress } from '../lib/wallet';
17−import { withNetwork } from '../store/network';
18−import { compose } from '../lib/lib';
19−import { withWallet } from '../store/wallet';
20−import { withPointCursor } from '../store/pointCursor';
21−import View from 'components/View';
22−
23−class AcceptTransfer extends React.Component {
24− constructor(props) {
25− super(props);
26−
27− const receivingAddress = need.addressFromWallet(props.wallet);
28− const incomingPoint = need.point(props.pointCursor);
29−
30− this.state = {
31− receivingAddress: receivingAddress,
32− incomingPoint: incomingPoint,
33− };
34−
35− this.handleAddressInput = this.handleAddressInput.bind(this);
36− this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
37− this.statelessRef = React.createRef();
38− }
39−
40− handleAddressInput(proxyAddress) {
41− this.setState({ proxyAddress });
42− this.statelessRef.current.clearTxn();
43− }
44−
45− createUnsignedTxn() {
46− const { state, props } = this;
47−
48− const validContracts = need.contracts(props.contracts);
49− const validPoint = need.point(props.pointCursor);
50− //TODO state.incomingPoint ?
51−
52− return Just(
53− azimuth.ecliptic.transferPoint(
54− validContracts,
55− validPoint,
56− state.receivingAddress,
57− true
58− )
59− );
60− }
61−
62− render() {
63− const { state, props } = this;
64− const validAddress = isValidAddress(state.receivingAddress);
65− const canGenerate = validAddress === true;
66−
67− const esvisible =
68− props.networkType === NETWORK_TYPES.ROPSTEN ||
69− props.networkType === NETWORK_TYPES.MAINNET;
70−
71− const esdomain =
72− props.networkType === NETWORK_TYPES.ROPSTEN
73− ? 'ropsten.etherscan.io'
74− : 'etherscan.io';
75−
76− return (
77− <View>
78− <H1>
79− {'Accept Transfer of '}{' '}
80− <code>{` ${ob.patp(state.incomingPoint)} `}</code>
81− </H1>
82−
83− <P>
84− {"By default, the recipient is the address you're logged in " +
85− 'as. But you can transfer to any address you like.'}
86− </P>
87−
88− <AddressInput
89− className="mono mt-8"
90− prop-size="lg"
91− prop-format="innerLabel"
92− value={state.receivingAddress}
93− onChange={v => this.handleAddressInput(v)}>
94− <InnerLabel>{'Receiving Address'}</InnerLabel>
95− <ShowBlockie className={'mt-1'} address={state.receivingAddress} />
96− </AddressInput>
97−
98− <Anchor
99− className={'mt-1 sm'}
100− prop-size="sm"
101− prop-disabled={!isValidAddress(state.receivingAddress) || !esvisible}
102− target={'_blank'}
103− href={`https://${esdomain}/address/${state.receivingAddress}`}>
104− {'View on Etherscan ↗'}
105− </Anchor>
106−
107− <StatelessTransaction
108− canGenerate={canGenerate}
109− createUnsignedTxn={this.createUnsignedTxn}
110− ref={this.statelessRef}
111− />
112− </View>
113− );
114− }
115−}
116−
117−export default compose(
118− withNetwork,
119− withWallet,
120− withPointCursor
121−)(AcceptTransfer);
src/views/CancelTransfer.jsdeleted−81
@@ -1,81 +0,0 @@
1−import React from 'react';
2−import { Just } from 'folktale/maybe';
3−import * as azimuth from 'azimuth-js';
4−import * as ob from 'urbit-ob';
5−
6−import { H1, P } from '../components/old/Base';
7−import * as need from '../lib/need';
8−
9−import StatelessTransaction from '../components/old/StatelessTransaction';
10−import { ETH_ZERO_ADDR } from '../lib/wallet';
11−import { withNetwork } from '../store/network';
12−import { compose } from '../lib/lib';
13−import { withPointCursor } from '../store/pointCursor';
14−import { withPointCache } from '../store/pointCache';
15−import View from 'components/View';
16−
17−class CancelTransfer extends React.Component {
18− constructor(props) {
19− super(props);
20−
21− const pointInTransfer = need.point(props.pointCursor);
22−
23− this.state = {
24− proxyAddress: '',
25− pointInTransfer: pointInTransfer,
26− };
27−
28− this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
29− }
30−
31− createUnsignedTxn() {
32− const { props } = this;
33−
34− const validContracts = need.contracts(props.contracts);
35− const validPoint = need.point(props.pointCursor);
36− //TODO this.state.pointInTransfer ?
37−
38− const txn = azimuth.ecliptic.setTransferProxy(
39− validContracts,
40− validPoint,
41− ETH_ZERO_ADDR
42− );
43−
44− return Just(txn);
45− }
46−
47− render() {
48− const { props, state } = this;
49−
50− const online = Just.hasInstance(props.web3);
51−
52− const proxy = online
53− ? props.pointCache[state.pointInTransfer].transferProxy
54− : 'any outgoing addresses';
55−
56− // const canGenerate = validAddress === true
57−
58− const canGenerate = true;
59−
60− return (
61− <View>
62− <H1>
63− {'Cancel Transfer of '}{' '}
64− <code>{` ${ob.patp(state.pointInTransfer)} `}</code>
65− </H1>
66−
67− <P>{`This action will cancel the transfer to ${proxy}.`}</P>
68− <StatelessTransaction
69− canGenerate={canGenerate}
70− createUnsignedTxn={this.createUnsignedTxn}
71− />
72− </View>
73− );
74− }
75−}
76−
77−export default compose(
78− withNetwork,
79− withPointCursor,
80− withPointCache
81−)(CancelTransfer);
src/views/CreateGalaxy.js+3−2
@@ -62,12 +62,13 @@ export default function CreateGalaxy() {
62 { pass: validGalaxyName, syncPass: syncValidGalaxyName, value: galaxyName },
62 { pass: validGalaxyName, syncPass: syncValidGalaxyName, value: galaxyName },
63 // ^ we use value: here so our effect runs onChange
63 // ^ we use value: here so our effect runs onChange
64 ] = useGalaxyInput({
64 ] = useGalaxyInput({
65+ name: 'galaxy',
65 disabled: inputsLocked,
66 disabled: inputsLocked,
66 autoFocus: true,
67 autoFocus: true,
67 error:
68 error:
68 error ||
69 error ||
69 isAvailable.matchWith({
70 isAvailable.matchWith({
70− Nothing: () => 'Loading availabiliy...', // TODO: make async loading?
71+ Nothing: () => 'Loading availability...', // TODO: make async loading?
71 Just: p => (p.value ? undefined : 'This galaxy is already owned.'),
72 Just: p => (p.value ? undefined : 'This galaxy is already owned.'),
72 }),
73 }),
73 });
74 });
@@ -142,7 +143,7 @@ export default function CreateGalaxy() {
142 className={cn('f5', {
143 className={cn('f5', {
143 green3: completed,
144 green3: completed,
144 })}>
145 })}>
145− {galaxyName} has been created and is owned by {owner}.
146+ {galaxyName} has been created and can be claimed by {owner}.
146 </Grid.Item>
147 </Grid.Item>
147 )}
148 )}
148
149
src/views/InvitesManage.jsdeleted−185
@@ -1,185 +0,0 @@
1−import { Just, Nothing } from 'folktale/maybe';
2−import React from 'react';
3−import * as ob from 'urbit-ob';
4−import * as azimuth from 'azimuth-js';
5−import * as need from '../lib/need';
6−
7−import {
8− Warning,
9− Input,
10− PointInput,
11− InnerLabel,
12− ValidatedSigil,
13−} from '../components/old/Base';
14−import StatelessTransaction from '../components/old/StatelessTransaction';
15−import { withNetwork } from '../store/network';
16−import { compose } from '../lib/lib';
17−import { withPointCursor } from '../store/pointCursor';
18−import View from 'components/View';
19−
20−class InvitesManage extends React.Component {
21− constructor(props) {
22− super(props);
23−
24− this.state = {
25− invitesWillWork: Nothing(),
26− planetInput: '',
27− targetPlanet: Nothing(),
28− currentPoolSize: Nothing(),
29− cachedPoolSizes: {},
30− targetPoolSize: 5,
31− };
32−
33− this.checkCurrentInvites = this.checkCurrentInvites.bind(this);
34− this.handlePointInput = this.handlePointInput.bind(this);
35− this.updatePoolSize = this.updatePoolSize.bind(this);
36− this.handlePoolSizeInput = this.handlePoolSizeInput.bind(this);
37− this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
38− this.statelessRef = React.createRef();
39− }
40−
41− componentDidMount() {
42− this.point = need.point(this.props.pointCursor);
43− this.contracts = need.contracts(this.props.contracts);
44−
45− azimuth.azimuth
46− .isSpawnProxy(
47− this.contracts,
48− this.point,
49− this.contracts.delegatedSending.address
50− )
51− .then(isSpawnProxy => {
52− this.setState({ invitesWillWork: Just(isSpawnProxy) });
53− });
54− }
55−
56− componentDidUpdate(prevProps) {
57− //
58− }
59−
60− handlePointInput(targetPlanet) {
61− let newState = {
62− targetPlanet: Nothing(),
63− planetInput: targetPlanet,
64− canGenerate: false,
65− currentPoolSize: Nothing(),
66− };
67− if (targetPlanet.length === 14 && ob.isValidPatp(targetPlanet)) {
68− const target = ob.patp2dec(targetPlanet);
69− //TODO there must be a simpler way to do this check
70− const isChild = ob.sein(targetPlanet) === ob.patp(this.point);
71− if (isChild) {
72− newState.targetPlanet = Just(target);
73− newState.currentPoolSize = this.checkCurrentInvites(target);
74− newState.canGenerate = true;
75− }
76− }
77− this.setState(newState);
78− }
79−
80− checkCurrentInvites(who) {
81− const cachedPoolSizes = this.state.cachedPoolSizes;
82− if (typeof cachedPoolSizes[who] !== 'undefined') {
83− return Just(cachedPoolSizes[who]);
84− } else {
85− azimuth.delegatedSending.getPool(this.contracts, who).then(pool => {
86− azimuth.delegatedSending
87− .invitesInPool(this.contracts, pool, this.point)
88− .then(size => {
89− this.updatePoolSize(who, size);
90− });
91− });
92− return Nothing();
93− }
94− }
95−
96− updatePoolSize(who, size) {
97− const cache = this.state.cachedPoolSizes;
98− cache[who] = size;
99− this.setState({ cachedPoolSizes: cache });
100− const target = this.state.targetPlanet;
101− const match = Just.hasInstance(target) ? who === target.value : false;
102− if (match) {
103− this.setState({ currentPoolSize: Just(size) });
104− }
105− }
106−
107− handlePoolSizeInput(targetSize) {
108− this.setState({ targetPoolSize: targetSize });
109− }
110−
111− createUnsignedTxn() {
112− return Just(
113− azimuth.delegatedSending.setPoolSize(
114− this.contracts,
115− this.point,
116− this.state.targetPlanet.value,
117− this.state.targetPoolSize
118− )
119− );
120− }
121−
122− render() {
123− let spawnProxyWarning = null;
124− if (this.state.invitesWillWork.value === false) {
125− spawnProxyWarning = (
126− <Warning>
127− <h3 className={'mb-2'}>{'Warning'}</h3>
128− {'Planets under this star will not be able to send invites ' +
129− 'until the invite contract ('}
130− <code>{this.contracts.delegatedSending.address}</code>
131− {') is made spawn proxy for this star.'}
132− </Warning>
133− );
134− }
135−
136− let poolSizeText = this.state.currentPoolSize.matchWith({
137− Just: ps => `(currently ${ps.value})`,
138− Nothing: _ => '',
139− });
140−
141− return (
142− <View>
143− <p>
144− {
145− 'manage invites here, for stars. can only give invites to child planets'
146− }
147− </p>
148−
149− {spawnProxyWarning}
150−
151− <PointInput
152− prop-format="innerlabel"
153− prop-size="lg"
154− placeholder={'~sampel-sipnem'}
155− value={this.state.planetInput}
156− onChange={this.handlePointInput}>
157− <InnerLabel>{'Planet to set invites for'}</InnerLabel>
158− <ValidatedSigil
159− patp={this.state.targetPlanet}
160− validator={() => this.validatePoint}
161− />
162− </PointInput>
163−
164− <Input
165− prop-format="innerlabel"
166− prop-size="lg"
167− value={this.state.targetPoolSize}
168− onChange={this.handlePoolSizeInput}>
169− <InnerLabel>{`Available invites ${poolSizeText}`}</InnerLabel>
170− </Input>
171−
172− <StatelessTransaction
173− canGenerate={this.state.canGenerate}
174− createUnsignedTxn={this.createUnsignedTxn}
175− ref={this.statelessRef}
176− />
177− </View>
178− );
179− }
180−}
181−
182−export default compose(
183− withNetwork,
184− withPointCursor
185−)(InvitesManage);
src/views/IssueChild.js+165−243
@@ -1,254 +1,176 @@
1+import React, { useCallback, useEffect, useMemo, useState } from 'react';
1−import { Just, Nothing } from 'folktale/maybe';
2+import { Nothing, Just } from 'folktale/maybe';
2−import React from 'react';
3+import cn from 'classnames';
3−import { azimuth, ecliptic } from 'azimuth-js';
4+import { Grid, Text, Input } from 'indigo-react';
5+import * as azimuth from 'azimuth-js';
4import * as ob from 'urbit-ob';
6import * as ob from 'urbit-ob';
5−import * as need from '../lib/need';
6−
7−import {
8− H1,
9− P,
10− Anchor,
11− ShowBlockie,
12− ValidatedSigil,
13− PointInput,
14− AddressInput,
15− InnerLabel,
16−} from '../components/old/Base';
17−
18−import StatelessTransaction from '../components/old/StatelessTransaction';
19−
20−import { NETWORK_TYPES } from '../lib/network';
21−import { getSpawnCandidate } from '../lib/child';
22−import { canDecodePatp } from '../lib/txn';
23−
24−import { isValidAddress } from '../lib/wallet';
25−import { withNetwork } from '../store/network';
26−import { compose } from '../lib/lib';
27−import { withPointCursor } from '../store/pointCursor';
28−import View from 'components/View';
29−
30−const setFind = (set, pred) => {
31− for (const e of set) {
32− if (pred(e)) {
33− return e;
34− }
35− }
36− return undefined;
37−};
38−
39−class IssueChild extends React.Component {
40− constructor(props) {
41− super(props);
42−
43− const issuingPoint = need.point(props.pointCursor);
44−
45− const getCandidate = () => ob.patp(getSpawnCandidate(issuingPoint));
46−
47− const suggestions = [
48− getCandidate(),
49− getCandidate(),
50− getCandidate(),
51− getCandidate(),
52− ];
53−
54− this.state = {
55− receivingAddress: '',
56− issuingPoint: issuingPoint,
57− desiredPoint: '',
58− isAvailable: Nothing(), // use Nothing to allow attempt when offline
59− suggestions: suggestions,
60− validChildren: new Set(),
61− autoComplete: '',
62− };
63−
64− this.handlePointInput = this.handlePointInput.bind(this);
65− this.handleAddressInput = this.handleAddressInput.bind(this);
66− this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
67− this.statelessRef = React.createRef();
68− }
69−
70− handleAddressInput = receivingAddress => {
71− this.setState({ receivingAddress });
72− this.statelessRef.current.clearTxn();
73− };
74−
75− componentWillMount() {
76− const { issuingPoint } = this.state;
77−
78− const validContracts = need.contracts(this.props.contracts);
79−
80− azimuth
81− .getUnspawnedChildren(validContracts, parseInt(issuingPoint, 10))
82− .then(ps => this.setState({ validChildren: new Set(ps.map(ob.patp)) }));
83− }
84−
85− handlePointInput(desiredPoint) {
86− if (desiredPoint.length < 15) {
87− const suggestedPoint =
88− setFind(this.state.validChildren, e => e.startsWith(desiredPoint)) ||
89− '';
90− const rendered =
91− ' '.repeat(desiredPoint.length) +
92− suggestedPoint.substring(desiredPoint.length);
93−
94− const available = Just(this.state.validChildren.has(desiredPoint));
95−
96− this.setState({
97− desiredPoint,
98− isAvailable: available,
99− autoComplete: rendered,
100− });
101− this.statelessRef.current.clearTxn();
102− }
103− }
104−
105− createUnsignedTxn() {
106− const { state, props } = this;
107−
108− if (isValidAddress(state.receivingAddress) === false) return Nothing();
109− if (state.isAvailable === false) return Nothing();
110− if (canDecodePatp(state.desiredPoint) === false) return Nothing();
111−
112− const validContracts = need.contracts(props.contracts);
113−
114− const pointDec = ob.patp2dec(state.desiredPoint);
115
7
116− const txn = ecliptic.spawn(
8+import { useNetwork } from 'store/network';
117− validContracts,
9+import { useHistory } from 'store/history';
118− pointDec,
10+import { usePointCache } from 'store/pointCache';
119− state.receivingAddress
120− );
121
11
122− return Just(txn);
12+import * as need from 'lib/need';
123− }
13+import { useAddressInput, usePointInput } from 'lib/useInputs';
14+import useEthereumTransaction from 'lib/useEthereumTransaction';
15+import { GAS_LIMITS } from 'lib/constants';
16+import patp2dec from 'lib/patp2dec';
124
17
125− validatePoint = patp => {
18+import ViewHeader from 'components/ViewHeader';
126− const point = this.point;
19+import MiniBackButton from 'components/MiniBackButton';
127−
20+import InlineEthereumTransaction from 'components/InlineEthereumTransaction';
128− let vpatp = false;
21+import View from 'components/View';
129− try {
22+import useLifecycle from 'lib/useLifecycle';
130− vpatp = ob.isValidPatp(patp);
23+import { usePointCursor } from 'store/pointCursor';
131− } catch (_) {}
24+import { validateInSet } from 'lib/validators';
132−
25+import { getSpawnCandidate } from 'lib/child';
133− let vchild = false;
134− try {
135− vchild = ob.sein(patp) === ob.patp(point);
136− } catch (_) {}
137
26
138− return vpatp && vchild;
27+function useIssueChild() {
139− };
28+ const { contracts } = useNetwork();
29+ const { syncKnownPoint } = usePointCache();
140
30
141− render() {
31+ const _contracts = need.contracts(contracts);
142− const { props, state } = this;
143
32
144− const validAddress = isValidAddress(state.receivingAddress);
33+ const [spawnedPoint, setSpawnedPoint] = useState();
145
34
146− const canGenerate = props.web3.matchWith({
35+ return useEthereumTransaction(
147− Nothing: () => {
36+ useCallback(
148− return validAddress === true;
37+ (spawnedPoint, owner) => {
149− },
38+ setSpawnedPoint(spawnedPoint);
150− Just: () => {
39+ return azimuth.ecliptic.spawn(_contracts, spawnedPoint, owner);
151− return validAddress === true && state.isAvailable.value === true;
152 },
40 },
153− });
41+ [_contracts]
154−
42+ ),
155− const esvisible =
43+ useCallback(() => syncKnownPoint(spawnedPoint), [
156− props.networkType === NETWORK_TYPES.ROPSTEN ||
44+ spawnedPoint,
157− props.networkType === NETWORK_TYPES.MAINNET;
45+ syncKnownPoint,
158−
46+ ]),
159− const esdomain =
47+ GAS_LIMITS.DEFAULT
160− props.networkType === NETWORK_TYPES.ROPSTEN
161− ? 'ropsten.etherscan.io'
162− : 'etherscan.io';
163−
164− return (
165− <View>
166− <H1>
167− {'Issue a Child From '}{' '}
168− <code>{`${ob.patp(state.issuingPoint)}`}</code>
169− </H1>
170−
171− <P>
172− {`Please enter the point you would like to issue, and specify the
173− receiving Ethereum address. If you need to create an address, you
174− can also use Wallet Generator.`}
175− </P>
176−
177− <P>
178− {`Your point can only issue children with particular names. Some
179− valid suggestions for `}
180− {<code>{ob.patp(state.issuingPoint)}</code>}
181− {' are '}
182− <code>{state.suggestions[0]}</code>
183− {', '}
184− <code>{state.suggestions[1]}</code>
185− {', and '}
186− <code>{state.suggestions[2]}</code>
187− {'.'}
188− </P>
189−
190− <PointInput
191− prop-size="lg"
192− prop-format="innerLabel"
193− className={'mono mt-8'}
194− placeholder={`e.g. ${state.suggestions[3]}`}
195− value={state.desiredPoint}
196− onChange={this.handlePointInput}>
197− <InnerLabel>{'Point to Issue'}</InnerLabel>
198− <div
199− style={{
200− marginTop: '8.8rem',
201− marginLeft: '4.6rem',
202− fontSize: '5rem',
203− color: '#757575',
204− whiteSpace: 'pre-wrap',
205− }}
206− className="abs tl-0 mono">
207− {state.autoComplete}
208− </div>
209− <ValidatedSigil
210− className={'tr-0 mt-05 mr-0 abs'}
211− patp={state.desiredPoint}
212− size={68}
213− margin={8}
214− validator={() => this.validatePoint(state.desiredPoint)}
215− />
216− </PointInput>
217−
218− <AddressInput
219− className="text-mono mt-8"
220− prop-size="lg"
221− prop-format="innerLabel"
222− placeholder={`e.g. 0x84295d5e054d8cff5a22428b195f5a1615bd644f`}
223− value={state.receivingAddress}
224− disabled={
225− Nothing.hasInstance(state.isAvailable) || !state.isAvailable.value
226− }
227− onChange={v => this.handleAddressInput(v)}>
228− <InnerLabel>{'Receiving Address'}</InnerLabel>
229− <ShowBlockie className={'mt-1'} address={state.receivingAddress} />
230− </AddressInput>
231−
232− <Anchor
233− className={'mt-1'}
234− prop-size={'sm'}
235− prop-disabled={!isValidAddress(state.receivingAddress) || !esvisible}
236− target={'_blank'}
237− href={`https://${esdomain}/address/${state.receivingAddress}`}>
238− {'View on Etherscan ↗'}
239− </Anchor>
240−
241− <StatelessTransaction
242− canGenerate={canGenerate}
243− createUnsignedTxn={this.createUnsignedTxn}
244− ref={this.statelessRef}
245− />
246− </View>
247− );
48+ );
248− }
249}
49}
250
50
251−export default compose(
51+export default function IssueChild() {
252− withNetwork,
52+ const { pop } = useHistory();
253− withPointCursor
53+ const { contracts } = useNetwork();
254−)(IssueChild);
54+ const { pointCursor } = usePointCursor();
55+
56+ const _contracts = need.contracts(contracts);
57+ const _point = need.point(pointCursor);
58+
59+ const [availablePointNames, setAvailablePointNames] = useState(Nothing());
60+
61+ const candidates = useMemo(() => {
62+ const getCandidate = () => ob.patp(getSpawnCandidate(_point));
63+
64+ return [getCandidate(), getCandidate(), getCandidate(), getCandidate()];
65+ }, [_point]);
66+
67+ const {
68+ isDefaultState,
69+ construct,
70+ unconstruct,
71+ completed,
72+ inputsLocked,
73+ bind,
74+ } = useIssueChild();
75+
76+ const validators = useMemo(
77+ () => [
78+ validateInSet(
79+ availablePointNames.getOrElse(new Set()),
80+ 'This point cannot be spawned.'
81+ ),
82+ ],
83+ [availablePointNames]
84+ );
85+ const [
86+ pointNameInput,
87+ { pass: validPointName, value: pointName },
88+ // ^ we use value: here so our effect runs onChange
89+ ] = usePointInput({
90+ name: 'point',
91+ disabled: inputsLocked,
92+ autoFocus: true,
93+ validators,
94+ error: availablePointNames.matchWith({
95+ Nothing: () => 'Loading availability...',
96+ Just: () => undefined,
97+ }),
98+ });
99+
100+ const [ownerInput, { pass: validOwner, data: owner }] = useAddressInput({
101+ name: 'owner',
102+ label: `Ethereum Address`,
103+ disabled: inputsLocked,
104+ });
105+
106+ useEffect(() => {
107+ if (validPointName && validOwner) {
108+ construct(patp2dec(pointName), owner);
109+ } else {
110+ unconstruct();
111+ }
112+ }, [owner, construct, unconstruct, validPointName, validOwner, pointName]);
113+
114+ useLifecycle(() => {
115+ let mounted = true;
116+
117+ (async () => {
118+ const availablePoints = await azimuth.azimuth.getUnspawnedChildren(
119+ _contracts,
120+ _point
121+ );
122+
123+ if (!mounted) {
124+ return;
125+ }
126+
127+ setAvailablePointNames(Just(new Set(availablePoints.map(ob.patp))));
128+ })();
129+
130+ return () => (mounted = false);
131+ });
132+
133+ return (
134+ <View inset>
135+ <Grid>
136+ <Grid.Item full as={MiniBackButton} onClick={() => pop()} />
137+
138+ <Grid.Item full as={ViewHeader}>
139+ Issue Child Point
140+ </Grid.Item>
141+
142+ {isDefaultState && (
143+ <Grid.Item full as={Text}>
144+ Perhaps one of {candidates.slice(0, 3).join(', ')}, or{' '}
145+ {candidates[candidates.length - 1]}?
146+ </Grid.Item>
147+ )}
148+
149+ {completed && (
150+ <Grid.Item
151+ full
152+ as={Text}
153+ className={cn('f5', {
154+ green3: completed,
155+ })}>
156+ {pointName} has been spawned and can be claimed by {owner}.
157+ </Grid.Item>
158+ )}
159+
160+ {!completed && (
161+ <>
162+ <Grid.Item full as={Input} {...pointNameInput} className="mt4" />
163+ <Grid.Item full as={Input} {...ownerInput} className="mb4" />
164+ </>
165+ )}
166+
167+ <Grid.Item
168+ full
169+ as={InlineEthereumTransaction}
170+ {...bind}
171+ onReturn={() => pop()}
172+ />
173+ </Grid>
174+ </View>
175+ );
176+}
src/views/Point.js+14−1
@@ -23,7 +23,7 @@ export default function Point() {
23
23
24 const point = need.point(pointCursor);
24 const point = need.point(pointCursor);
25
25
26− const { isActiveOwner, canManage } = useCurrentPermissions();
26+ const { isActiveOwner, canManage, canSpawn } = useCurrentPermissions();
27
27
28 // fetch the invites for the current cursor
28 // fetch the invites for the current cursor
29 const { availableInvites } = useInvites(point);
29 const { availableInvites } = useInvites(point);
@@ -37,6 +37,11 @@ export default function Point() {
37 [push, names]
37 [push, names]
38 );
38 );
39
39
40+ const goIssuePoint = useCallback(() => push(names.ISSUE_CHILD), [
41+ names.ISSUE_CHILD,
42+ push,
43+ ]);
44+
40 const inviteButton = (() => {
45 const inviteButton = (() => {
41 switch (azimuth.getPointSize(point)) {
46 switch (azimuth.getPointSize(point)) {
42 case azimuth.PointSize.Planet:
47 case azimuth.PointSize.Planet:
@@ -76,6 +81,14 @@ export default function Point() {
76 Admin
81 Admin
77 </Grid.Item>
82 </Grid.Item>
78 <Grid.Divider />
83 <Grid.Divider />
84+ {canSpawn && (
85+ <>
86+ <Grid.Item full as={ForwardButton} onClick={goIssuePoint}>
87+ Issue Point
88+ </Grid.Item>
89+ <Grid.Divider />
90+ </>
91+ )}
79 <Grid.Item full as={BootArvoButton} disabled />
92 <Grid.Item full as={BootArvoButton} disabled />
80 </Grid>
93 </Grid>
81
94
src/views/SentTransaction.jsdeleted−172
@@ -1,172 +0,0 @@
1−import React from 'react';
2−import * as need from '../lib/need';
3−import { H1, H3, P, Warning, Anchor } from '../components/old/Base';
4−import { Button } from '../components/old/Base';
5−
6−import { ROUTE_NAMES } from '../lib/routeNames';
7−import { useHistory } from '../store/history';
8−import { BRIDGE_ERROR, renderTxnError } from '../lib/error';
9−import { NETWORK_TYPES } from '../lib/network';
10−import { useNetwork } from '../store/network';
11−import { useTxnCursor } from '../store/txnCursor';
12−import View from 'components/View';
13−import { Grid } from 'indigo-react';
14−
15−class Success extends React.Component {
16− constructor(props) {
17− super(props);
18−
19− this.state = {
20− pending: '.',
21− interval: null,
22− };
23− }
24−
25− componentDidMount() {
26− const nextDot = { '.': '..', '..': '...', '...': '.' };
27−
28− const interval = setInterval(() => {
29− this.setState(({ pending }) => ({ pending: nextDot[pending] }));
30− }, 1000);
31− this.setState({ interval: interval });
32− }
33−
34− componentWillUnmount() {
35− clearInterval(this.state.interval);
36− }
37−
38− render() {
39− const { props, state } = this;
40− const { networkType, hash } = props;
41− const { pending } = state;
42−
43− const esvisible =
44− networkType === NETWORK_TYPES.ROPSTEN ||
45− networkType === NETWORK_TYPES.MAINNET;
46−
47− const esdomain =
48− networkType === NETWORK_TYPES.ROPSTEN
49− ? 'ropsten.etherscan.io'
50− : 'etherscan.io';
51−
52− const esmessage =
53− esvisible === true
54− ? 'If you’d like to keep track of it, click the Etherscan link below.'
55− : '';
56−
57− const esanchor =
58− esvisible === false ? null : (
59− <Anchor
60− className={'mb-4 mt-1'}
61− prop-size={'sm'}
62− target={'_blank'}
63− href={`https://${esdomain}/tx/${hash}`}>
64− {'View on Etherscan ↗'}
65− </Anchor>
66− );
67−
68− const confirmations = 1;
69−
70− const requiredConfirmations = 1;
71−
72− const status =
73− confirmations < requiredConfirmations
74− ? `Pending${pending}`
75− : `Confirmed! (x${confirmations} confirmations)!`;
76−
77− return (
78− <Grid>
79− <Grid.Item full>
80− <H1>{'Your Transaction was Sent'}</H1>
81−
82− <P>
83− {`We sent your transaction to the chain. It can take some time to
84− execute, especially if the network is busy. ${esmessage}`}
85− </P>
86−
87− <H3>{'Transaction Hash'}</H3>
88− <P>{hash}</P>
89−
90− <H3>{'Transaction Status'}</H3>
91− <P>{status}</P>
92− <P>{esanchor}</P>
93− </Grid.Item>
94− </Grid>
95− );
96− }
97−}
98−
99−const Failure = props => (
100− <Grid>
101− <Grid.Item full>
102− <H1>{'Error!'}</H1>
103−
104− <Warning>
105− <H3>{'There was an error sending your transaction.'}</H3>
106− {renderTxnError(props.web3, props.message)}
107− </Warning>
108− </Grid.Item>
109− </Grid>
110−);
111−
112−function SentTransaction(props) {
113− const history = useHistory();
114− const { web3, networkType } = useNetwork();
115− const { txnCursor } = useTxnCursor();
116−
117− const promptKeyfile = history.data && history.data.promptKeyfile;
118−
119− const w3 = need.web3(web3);
120−
121− const result = txnCursor.matchWith({
122− Nothing: _ => {
123− throw new Error(BRIDGE_ERROR.MISSING_TXN);
124− },
125− Just: res => res.value,
126− });
127−
128− const body = result.matchWith({
129− Error: message => <Failure web3={w3} message={message.value} />,
130− Ok: hash => <Success hash={hash.value} networkType={networkType} />,
131− });
132−
133− const ok = (
134− <Grid>
135− <Grid.Item full>
136− <Button
137− prop-type={'link'}
138− onClick={() => {
139− history.pop();
140− }}>
141− {'Ok →'}
142− </Button>
143− </Grid.Item>
144− </Grid>
145− );
146−
147− let keyfile;
148−
149− if (promptKeyfile) {
150− keyfile = (
151− <Grid>
152− <Grid.Item full>
153− <Button
154− prop-type={'link'}
155− onClick={() => history.popAndPush(ROUTE_NAMES.GEN_KEYFILE)}>
156− {'Download Keyfile →'}
157− </Button>
158− </Grid.Item>
159− </Grid>
160− );
161− }
162−
163− return (
164− <View>
165− {body}
166− {ok}
167− {keyfile}
168− </View>
169− );
170−}
171−
172−export default SentTransaction;
src/views/SetKeys.jsdeleted−210
@@ -1,210 +0,0 @@
1−import React from 'react';
2−import { Just, Nothing } from 'folktale/maybe';
3−import * as azimuth from 'azimuth-js';
4−import * as ob from 'urbit-ob';
5−import * as need from '../lib/need';
6−import * as kg from 'urbit-key-generation/dist';
7−import { H1, P, Warning, CheckboxButton } from '../components/old/Base';
8−
9−import StatelessTransaction from '../components/old/StatelessTransaction';
10−import { attemptNetworkSeedDerivation } from '../lib/keys';
11−
12−import { addHexPrefix } from '../lib/wallet';
13−import { compose } from '../lib/lib';
14−
15−import { withNetwork } from '../store/network';
16−import { withWallet } from '../store/wallet';
17−import { withPointCursor } from '../store/pointCursor';
18−import { withPointCache } from '../store/pointCache';
19−import View from 'components/View';
20−
21−class SetKeys extends React.Component {
22− constructor(props) {
23− super(props);
24−
25− const point = need.point(props.pointCursor);
26−
27− this.state = {
28− auth: '',
29− encr: '',
30− newNetworkSeed: '',
31− nondeterministicSeed: false,
32− point: point,
33− cryptoSuiteVersion: 1,
34− discontinuity: false,
35− isManagementSeed: false,
36− };
37−
38− this.toggleDiscontinuity = this.toggleDiscontinuity.bind(this);
39− // Transaction
40− this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
41− }
42−
43− toggleDiscontinuity() {
44− this.setState({ discontinuity: !this.state.discontinuity });
45− }
46−
47− componentDidMount() {
48− const { props } = this;
49−
50− this.deriveSeed();
51−
52− const addr = need.addressFromWallet(props.wallet);
53−
54− this.determineManagementSeed(props.contracts.value, addr);
55− }
56−
57− async determineManagementSeed(ctrcs, addr) {
58− const managing = await azimuth.azimuth.getManagerFor(ctrcs, addr);
59−
60− this.setState({
61− isManagementSeed: managing.length !== 0,
62− });
63− }
64−
65− //TODO use web3.utils.randomHex when it gets fixed, see web3.js#1490
66− randomHex(len) {
67− let hex = '';
68−
69− for (var i = 0; i < len; i++) {
70− hex =
71− hex +
72− [
73− '0',
74− '1',
75− '2',
76− '3',
77− '4',
78− '5',
79− '6',
80− '7',
81− '8',
82− '9',
83− 'A',
84− 'B',
85− 'C',
86− 'D',
87− 'E',
88− 'F',
89− ][Math.floor(Math.random() * 16)];
90− }
91−
92− return hex;
93− }
94−
95− async deriveSeed() {
96− const next = true;
97− let nondeterministicSeed = false;
98−
99− const seed = await attemptNetworkSeedDerivation(next, this.props);
100− const newNetworkSeed = seed.matchWith({
101− Nothing: () => {
102− nondeterministicSeed = true;
103− return this.randomHex(64);
104− },
105− Just: p => p.value,
106− });
107−
108− this.setState({
109− newNetworkSeed,
110− nondeterministicSeed,
111− });
112− }
113−
114− createUnsignedTxn() {
115− const { state, props } = this;
116−
117− const validContracts = need.contracts(props.contracts);
118− const validPoint = need.point(props.pointCursor);
119−
120− // TODO: move this to a lib for validating things
121− const hexRegExp = /[0-9A-Fa-f]{64}/g;
122−
123− if (hexRegExp.test(state.newNetworkSeed)) {
124− // derive network keys
125− const pair = kg.deriveNetworkKeys(state.newNetworkSeed);
126−
127− const pencr = addHexPrefix(pair.crypt.public);
128− const pauth = addHexPrefix(pair.auth.public);
129−
130− const txn = azimuth.ecliptic.configureKeys(
131− validContracts,
132− validPoint,
133− pencr,
134− pauth,
135− 1,
136− state.discontinuity
137− );
138−
139− return Just(txn);
140− }
141−
142− return Nothing();
143− }
144−
145− render() {
146− const { props, state } = this;
147−
148− const canGenerate =
149− state.newNetworkSeed.length === 64 && state.newNetworkSeed.length === 64;
150−
151− const pointDetails = need.fromPointCache(props.pointCache, state.point);
152−
153− return (
154− <View>
155− <H1>
156− {'Set Network Keys For '} <code>{`${ob.patp(state.point)}`}</code>
157− </H1>
158−
159− <P className="mt-10">
160− {`Set new authentication and encryption keys for your Arvo ship.`}
161− </P>
162−
163− {state.nondeterministicSeed && (
164− <Warning>
165− <h3 className={'mb-2'}>{'Warning'}</h3>
166− {`Your network seed could not be derived automatically. We've
167− generated a random one for you, so you must download your Arvo
168− keyfile during this session after setting your keys.`}
169− </Warning>
170− )}
171−
172− {pointDetails.keyRevisionNumber === 0 ? (
173− <Warning>
174− <h3 className={'mb-2'}>{'Warning'}</h3>
175− {'Once these keys have been set, your point is considered ' +
176− "'linked'. This operation cannot be undone."}
177− </Warning>
178− ) : (
179− <div />
180− )}
181−
182− <CheckboxButton
183− className="mt-8"
184− onToggle={this.toggleDiscontinuity}
185− state={state.discontinuity}
186− label={`I have lost important off-chain data relating to this point
187− and need to do a hard reset.
188− (For example, rebooting an Arvo ship.)`}
189− />
190−
191− <StatelessTransaction
192− // Upper scope
193− {...props}
194− // Tx
195− canGenerate={canGenerate}
196− createUnsignedTxn={this.createUnsignedTxn}
197− newNetworkSeed={state.newNetworkSeed}
198− newRevision={parseInt(pointDetails.keyRevisionNumber) + 1}
199− />
200− </View>
201− );
202− }
203−}
204−
205−export default compose(
206− withNetwork,
207− withWallet,
208− withPointCursor,
209− withPointCache
210−)(SetKeys);
src/views/SetProxy.jsdeleted−181
@@ -1,181 +0,0 @@
1−import React from 'react';
2−import { Just, Nothing } from 'folktale/maybe';
3−import * as need from '../lib/need';
4−import * as azimuth from 'azimuth-js';
5−import {
6− H1,
7− P,
8− InnerLabel,
9− ShowBlockie,
10− Anchor,
11− HorizontalSelector,
12−} from '../components/old/Base';
13−import { AddressInput } from '../components/old/Base';
14−import * as ob from 'urbit-ob';
15−
16−import { PROXY_TYPE } from '../lib/proxy';
17−
18−import { NETWORK_TYPES } from '../lib/network';
19−
20−import StatelessTransaction from '../components/old/StatelessTransaction';
21−
22−import { isValidAddress } from '../lib/wallet';
23−import { withNetwork } from '../store/network';
24−import { compose } from '../lib/lib';
25−import { withPointCursor } from '../store/pointCursor';
26−import View from 'components/View';
27−
28−class _SetProxy extends React.Component {
29− constructor(props) {
30− super(props);
31−
32− const issuingPoint = need.point(props.pointCursor);
33−
34− this.state = {
35− proxyAddress: '',
36− issuingPoint: issuingPoint,
37− setProxy: true,
38− };
39−
40− this.statelessRef = React.createRef();
41− this.handleSetUnset = this.handleSetUnset.bind(this);
42− this.handleAddressInput = this.handleAddressInput.bind(this);
43− this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
44− }
45−
46− handleSetUnset(selected) {
47− const set = selected === 'set';
48− this.setState({
49− setProxy: set,
50− proxyAddress: set ? '' : '0x0000000000000000000000000000000000000000',
51− });
52− }
53−
54− handleAddressInput(proxyAddress) {
55− this.setState({ proxyAddress });
56−
57− this.statelessRef.current.clearTxn();
58− }
59−
60− createUnsignedTxn(proxyAddress) {
61− const { state, props } = this;
62−
63− const validContracts = need.contracts(props.contracts);
64− const validPoint = need.point(props.pointCursor);
65−
66− const txArgs = [validContracts, validPoint, state.proxyAddress];
67−
68− if (props.proxyType === PROXY_TYPE.TRANSFER_PROXY) {
69− return Just(azimuth.ecliptic.setTransferProxy(...txArgs));
70− }
71−
72− if (props.proxyType === PROXY_TYPE.SPAWN_PROXY) {
73− return Just(azimuth.ecliptic.setSpawnProxy(...txArgs));
74− }
75−
76− if (props.proxyType === PROXY_TYPE.MANAGEMENT_PROXY) {
77− return Just(azimuth.ecliptic.setManagementProxy(...txArgs));
78− }
79−
80− return Nothing();
81− }
82−
83− render() {
84− const { props, state } = this;
85−
86− const renderedProxyType = props.proxyType.toString();
87− const validAddress = isValidAddress(state.proxyAddress);
88− const canGenerate = validAddress === true;
89−
90− const esvisible =
91− props.networkType === NETWORK_TYPES.ROPSTEN ||
92− props.networkType === NETWORK_TYPES.MAINNET;
93−
94− const esdomain =
95− props.networkType === NETWORK_TYPES.ROPSTEN
96− ? 'ropsten.etherscan.io'
97− : 'etherscan.io';
98−
99− const ucFirst = w => w.charAt(0).toUpperCase() + w.slice(1);
100−
101− const setUnset = [
102− { title: 'Set', value: 'set' },
103− { title: 'Unset', value: 'unset' },
104− ];
105−
106− const titleVerb = this.state.setProxy ? 'Set' : 'Unset';
107−
108− let addressInput = null;
109− if (this.state.setProxy) {
110− addressInput = (
111− <>
112− <P className="mt-8">
113− {'Please provide an Ethereum address to act as the ' +
114− `${renderedProxyType} proxy. You can also use Wallet ` +
115− 'Generator to generate a keypair.'}
116− </P>
117−
118− <AddressInput
119− className="mono mt-8"
120− prop-size="lg"
121− prop-format="innerLabel"
122− placeholder={`e.g. 0x84295d5e054d8cff5a22428b195f5a1615bd644f`}
123− value={state.proxyAddress}
124− onChange={v => this.handleAddressInput(v)}>
125− <InnerLabel>{'Proxy Address'}</InnerLabel>
126− <ShowBlockie className={'mt-1'} address={state.proxyAddress} />
127− </AddressInput>
128−
129− <Anchor
130− className={'mt-1'}
131− prop-size={'sm'}
132− prop-disabled={!isValidAddress(state.proxyAddress) || !esvisible}
133− target={'_blank'}
134− href={`https://${esdomain}/address/${state.proxyAddress}`}>
135− {'View on Etherscan ↗'}
136− </Anchor>
137− </>
138− );
139− }
140−
141− return (
142− <View>
143− <H1>
144− {`${titleVerb} ${ucFirst(renderedProxyType)} Proxy For `}
145− <code>{`${ob.patp(state.issuingPoint)}`}</code>
146− </H1>
147−
148− <HorizontalSelector
149− options={setUnset}
150− onChange={this.handleSetUnset}
151− className="mt-8"
152− />
153−
154− {addressInput}
155−
156− <StatelessTransaction
157− ref={this.statelessRef}
158− createUnsignedTxn={this.createUnsignedTxn}
159− canGenerate={canGenerate}
160− />
161− </View>
162− );
163− }
164−}
165−
166−const SetProxy = compose(
167− withNetwork,
168− withPointCursor
169−)(_SetProxy);
170−
171−export const SetManagementProxy = props => (
172− <SetProxy {...props} proxyType={PROXY_TYPE.MANAGEMENT_PROXY} />
173−);
174−
175−export const SetSpawnProxy = props => (
176− <SetProxy {...props} proxyType={PROXY_TYPE.SPAWN_PROXY} />
177−);
178−
179−export const SetTransferProxy = props => (
180− <SetProxy {...props} proxyType={PROXY_TYPE.TRANSFER_PROXY} />
181−);
src/views/Transfer.jsdeleted−127
@@ -1,127 +0,0 @@
1−import React from 'react';
2−import { Just } from 'folktale/maybe';
3−import * as azimuth from 'azimuth-js';
4−import * as ob from 'urbit-ob';
5−import * as need from '../lib/need';
6−
7−import { H1, InnerLabel, ShowBlockie, P, Anchor } from '../components/old/Base';
8−import { AddressInput } from '../components/old/Base';
9−import StatelessTransaction from '../components/old/StatelessTransaction';
10−
11−import { NETWORK_TYPES } from '../lib/network';
12−
13−import { isValidAddress } from '../lib/wallet';
14−import { withNetwork } from '../store/network';
15−import { compose } from '../lib/lib';
16−import { withPointCursor } from '../store/pointCursor';
17−import View from 'components/View';
18−
19−class Transfer extends React.Component {
20− constructor(props) {
21− super(props);
22−
23− const issuingPoint = need.point(props.pointCursor);
24−
25− this.state = {
26− receivingAddress: '',
27− issuingPoint: issuingPoint,
28− };
29−
30− this.handleAddressInput = this.handleAddressInput.bind(this);
31− this.createUnsignedTxn = this.createUnsignedTxn.bind(this);
32− this.statelessRef = React.createRef();
33− }
34−
35− handleAddressInput(receivingAddress) {
36− this.setState({ receivingAddress });
37− this.statelessRef.current.clearTxn();
38− }
39−
40− handleConfirmAvailability() {
41− this.confirmPointAvailability().then(r => {
42− this.setState({ isAvailable: r });
43− });
44− }
45−
46− createUnsignedTxn() {
47− const { state, props } = this;
48−
49− const validContracts = need.contracts(props.contracts);
50− const validPoint = need.point(props.pointCursor);
51−
52− const txn = azimuth.ecliptic.setTransferProxy(
53− validContracts,
54− validPoint,
55− state.receivingAddress
56− );
57−
58− return Just(txn);
59− }
60−
61− render() {
62− const { props, state } = this;
63−
64− const validAddress = isValidAddress(state.receivingAddress);
65− const canGenerate = validAddress === true;
66−
67− const esvisible =
68− props.networkType === NETWORK_TYPES.ROPSTEN ||
69− props.networkType === NETWORK_TYPES.MAINNET;
70−
71− const esdomain =
72− props.networkType === NETWORK_TYPES.ROPSTEN
73− ? 'ropsten.etherscan.io'
74− : 'etherscan.io';
75−
76− return (
77− <View>
78− <H1>
79− {'Transfer'} <code>{` ${ob.patp(state.issuingPoint)} `}</code>
80− {'To a New Owner'}
81− </H1>
82−
83− <P>
84− {`Please provide the Ethereum address of the new owner. You own these
85− points until the recipient accepts the incoming transfer.
86− You may cancel the transfer until the transfer is accepted.`}
87− </P>
88−
89− <AddressInput
90− className={'mono mt-8'}
91− prop-size="lg"
92− prop-format="innerLabel"
93− value={state.receivingAddress}
94− placeholder={`e.g. 0x84295d5e054d8cff5a22428b195f5a1615bd644f`}
95− onChange={v => this.handleAddressInput(v)}>
96− <InnerLabel>{'New ownership address'}</InnerLabel>
97− <ShowBlockie className={'mt-1'} address={state.receivingAddress} />
98− </AddressInput>
99−
100− <Anchor
101− className={'mt-1'}
102− prop-size={'s'}
103− prop-disabled={!isValidAddress(state.receivingAddress) || !esvisible}
104− target={'_blank'}
105− href={`https://${esdomain}/address/${state.receivingAddress}`}>
106− {'View on Etherscan ↗'}
107− </Anchor>
108−
109− <StatelessTransaction
110− // Upper scope
111− {...props}
112− // Checks
113− userApproval={state.userApproval}
114− canGenerate={canGenerate}
115− // Methods
116− createUnsignedTxn={this.createUnsignedTxn}
117− ref={this.statelessRef}
118− />
119− </View>
120− );
121− }
122−}
123−
124−export default compose(
125− withNetwork,
126− withPointCursor
127−)(Transfer);