@@ -0,0 +1,479 @@
1+import React, { useMemo, useCallback, useState, useEffect } from 'react';
2+import { Grid, ErrorText } from 'indigo-react';
3+import { get } from 'lodash';
4+import * as azimuth from 'azimuth-js';
5+import { FORM_ERROR } from 'final-form';
6+import { toWei, toBN } from 'web3-utils';
8+import * as need from 'lib/need';
9+import * as wg from 'lib/walletgen';
10+import * as tank from 'lib/tank';
11+// import useMailer from 'lib/useMailer';
12+import timeout from 'lib/timeout';
13+import useCopiable from 'lib/useCopiable';
15+import Tabs from 'components/Tabs';
16+import ProgressButton from 'components/ProgressButton';
17+import { ForwardButton } from 'components/Buttons';
18+import NeedFundsNotice from 'components/NeedFundsNotice';
20+import { useNetwork } from 'store/network';
21+import { useWallet } from 'store/wallet';
22+import { usePointCursor } from 'store/pointCursor';
23+import { usePointCache } from 'store/pointCache';
28+ sendSignedTransaction,
29+ waitForTransactionConfirm,
32+import { MIN_PLANET, GAS_LIMITS } from 'lib/constants';
33+import pluralize from 'lib/pluralize';
34+import useGasPrice from 'lib/useGasPrice';
35+import useSetState from 'lib/useSetState';
37+import EmailChipInput from 'form/EmailChipInput';
38+import FormError from 'form/FormError';
39+import SubmitButton from 'form/SubmitButton';
40+import BridgeForm from 'form/BridgeForm';
42+ buildEmailArrayValidator,
45+} from 'form/validators';
46+import { WARNING, onlyHasWarning } from 'form/helpers';
48+const GAS_LIMIT = GAS_LIMITS.GIFT_PLANET;
57+const nameForEmailField = i => `emails[${i}]`;
59+const useInviter = sendInvites => {
60+ const { contracts, web3, networkType } = useNetwork();
61+ const { wallet, walletType, walletHdPath } = useWallet();
62+ const { pointCursor } = usePointCursor();
63+ const { syncInvites } = usePointCache();
64+ const { gasPrice } = useGasPrice();
65+ const point = need.point(pointCursor);
67+ const [needFunds, setNeedFunds] = useState(null);
68+ const [invites, setInvites] = useState([]);
70+ const [progress, setProgress] = useState(0);
71+ const [txStatus, setTxStatus] = useState(STATUS.INPUT);
72+ const [errors, addError] = useSetState();
74+ const isDone = txStatus === STATUS.SUCCESS;
79+ }, [isDone, syncInvites, point]);
81+ const resetInvites = useCallback(() => {
84+ setTxStatus(STATUS.INPUT);
85+ }, [setProgress, setInvites, setTxStatus]);
86+ const generateInvites = useCallback(
87+ async numInvites => {
88+ const _contracts = contracts.getOrElse(null);
89+ const _web3 = web3.getOrElse(null);
90+ const _wallet = wallet.getOrElse(null);
91+ if (!_contracts || !_web3 || !_wallet) {
92+ // not using need because we want a custom error
93+ throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
96+ const nonce = await _web3.eth.getTransactionCount(_wallet.address);
97+ const chainId = await _web3.eth.net.getId();
99+ const planets = await azimuth.delegatedSending.getPlanetsToSend(
105+ setTxStatus(STATUS.SENDING);
106+ // account for the race condition where invites got used up while we were
107+ // composing our target list
109+ if (planets.length < numInvites) {
110+ // resync invites to the cache, since they're out of date
113+ setTxStatus(STATUS.FAILURE);
117+ `Can currently only send ${planets.length} invites. ` +
118+ `Please remove invites until you are within the limit.`,
123+ // NB(shrugs) - must be processed in serial because main thread, etc
124+ let signedInvites = [];
126+ for (let i = 0; i < numInvites; i++) {
127+ console.log(`c: ${i}`);
128+ const name = nameForEmailField(i);
129+ setProgress(x => x + 1);
131+ const planet = planets[i];
133+ const { ticket, owner } = await wg.generateTemporaryTicketAndWallet(
135+ // we're always giving planets, so generate a ticket of the correct size
138+ const inviteTx = azimuth.delegatedSending.sendPoint(
145+ const signedTx = await signTransaction({
152+ // TODO: ^ make a useTransactionSigner to encapsulate this logic
154+ gasPrice: gasPrice.toFixed(), // expects string gwei
155+ gasLimit: GAS_LIMIT.toFixed(),
158+ const rawTx = hexify(signedTx.serialize());
160+ signedInvites.push({ ticket, signedTx, rawTx });
162+ console.error(error);
164+ [WARNING]: `There was an error while generating wallets.`,
169+ const totalCost = toBN(GAS_LIMIT)
170+ .mul(toBN(gasPrice))
171+ .mul(toBN(numInvites));
172+ const tankWasUsed = await tank.ensureFundsFor(
176+ toWei(totalCost.toString(), 'gwei'),
177+ Object.keys(invites).map(name => invites[name].rawTx),
178+ (address, minBalance, balance) =>
179+ setNeedFunds({ address, minBalance, balance }),
180+ () => setNeedFunds(undefined)
183+ let unsentInvites = [];
184+ let confirmedInvites = [];
185+ const txAndMailings = signedInvites.map(async invite => {
187+ const txHash = await sendSignedTransaction(
193+ await waitForTransactionConfirm(_web3, txHash);
194+ confirmedInvites.push(invite);
196+ console.error(error);
197+ unsentInvites.push(invite);
202+ await Promise.all(txAndMailings);
204+ setTxStatus(STATUS.SUCCESS);
205+ setInvites(confirmedInvites);
206+ console.log(unsentInvites);
208+ if (unsentInvites.length > 0) {
209+ return { [FORM_ERROR]: unsentInvites };
212+ if (errorCount > 0) {
214+ [WARNING]: `There ${pluralize(errorCount, 'was', 'were')} ${pluralize(
217+ )} while generating wallets. You can still send the invites that generated correctly.`,
245+const InviteMail = () => {
255+ const [status, setStatus] = useState(STATUS.INPUT);
257+ const [count, setCount] = useState(0);
258+ const canInput = status === STATUS.INPUT;
259+ const isSending = txStatus === STATUS.SENDING || status === STATUS.SENDING;
260+ const isFailed = status === STATUS.FAILURE || txStatus === STATUS.FAILURE;
261+ const isDone = status === STATUS.SUCCESS;
262+ const onSubmit = useCallback(
264+ console.log('GENERATING');
265+ const emailCount = values.emails.length;
266+ console.log(values);
267+ setCount(emailCount);
268+ setStatus(STATUS.SENDING);
269+ const errors = await generateInvites(emailCount);
270+ console.log(errors);
272+ setStatus(STATUS.FAILED);
276+ [generateInvites, setCount, setStatus]
280+ async function processInvites() {
281+ if (invites.length !== 0 && txStatus === STATUS.SUCCESS) {
282+ console.log(JSON.stringify(invites));
287+ setStatus(STATUS.SUCCESS);
291+ }, [invites, setStatus, status, txStatus, resetInvites]);
293+ const buttonText = () => {
296+ return 'Add to Invite Group';
297+ case STATUS.SENDING:
298+ return `Sending ${pluralize(count, 'Invite')}...`;
299+ case STATUS.SUCCESS:
300+ return `✓ ${pluralize(count, 'Invite')} sent`;
301+ case STATUS.FAILURE:
307+ const renderButton = handleSubmit => {
310+ <Grid.Item full as={SubmitButton} handleSubmit={handleSubmit}>
317+ const percentProgress = progress / count;
326+ progress={percentProgress}>
333+ <Grid.Item full className="mt4 p3 bg-green3 white f5">
342+ const validate = useMemo(
343+ () => composeValidator({ emails: buildEmailArrayValidator() }),
347+ <Grid.Item full as={Grid}>
350+ initialValues={{ emails: [] }}
351+ onSubmit={onSubmit}>
352+ {({ handleSubmit, values }) => (
354+ <Grid.Item full as={EmailChipInput} name="emails" className="mt4" />
355+ {renderButton(handleSubmit)}
357+ <Grid.Item full as={FormError} />
359+ <Grid.Item full as={NeedFundsNotice} {...needFunds} />
368+const InviteUrl = () => {
378+ const [error, setError] = useState();
380+ const generateInvite = useCallback(async () => {
381+ const errors = await generateInvites(1);
382+ if (hasErrors(errors)) {
385+ }, [generateInvites]);
387+ const renderGenerateButton = () => {
390+ <Grid.Item full className="mt4 p3 bg-red3 white f5">
391+ Error generating invite
403+ onClick={() => generateInvite()}>
407+ case STATUS.SENDING:
417+ Generating Invite URL
425+ const url = `https://bridge.urbit.org/#${get(invites, '[0].ticket', '')}`;
427+ const [doCopy, didCopy] = useCopiable(url);
430+ <Grid.Item full as={Grid}>
431+ {invites.length > 0 && (
432+ <Grid.Item full className="b-gray3 b1 mv4 p1 flex flex-center">
433+ <div className="flex1 ml1">{url}</div>
435+ className="pv3 ph4 bg-black white"
436+ onClick={() => doCopy()}>
437+ {didCopy ? 'Copied' : 'Copy'}
441+ {invites.length === 0 && renderGenerateButton()}
442+ {needFunds && <Grid.Item full as={NeedFundsNotice} {...needFunds} />}
443+ {error && <Grid.Item as={ErrorText}>{JSON.stringify(error)}</Grid.Item>}
448+const InviteCohort = () => {
449+ const [tab, setTab] = useState(NAMES.MAIL);
454+ tabClassName="t-center flex1"
459+ onTabChange={setTab}
470+ [NAMES.MAIL]: InviteMail,
471+ [NAMES.URL]: InviteUrl,
475+ { text: 'Email', value: NAMES.MAIL },
476+ { text: 'URL', value: NAMES.URL },
479+export default InviteCohort;