@@ -0,0 +1,226 @@
1+import { useState, useCallback, useEffect } from 'react';
2+import { toWei, toBN } from 'web3-utils';
3+import * as azimuth from 'azimuth-js';
4+import { FORM_ERROR } from 'final-form';
5+
6+import * as wg from 'lib/walletgen';
7+import * as tank from 'lib/tank';
8+import * as need from 'lib/need';
9+import pluralize from 'lib/pluralize';
10+import { GAS_LIMITS } from 'lib/constants';
11+import {
12+ signTransaction,
13+ sendSignedTransaction,
14+ waitForTransactionConfirm,
15+ hexify,
16+} from 'lib/txn';
17+import useGasPrice from 'lib/useGasPrice';
18+import { MIN_PLANET } from 'lib/constants';
19+
20+import { useNetwork } from 'store/network';
21+import { useWallet } from 'store/wallet';
22+import { usePointCursor } from 'store/pointCursor';
23+import { usePointCache } from 'store/pointCache';
24+
25+import { WARNING } from 'form/helpers';
26+
27+const GAS_LIMIT = GAS_LIMITS.GIFT_PLANET;
28+const STATUS = {
29+ INPUT: 'INPUT',
30+ SENDING: 'SENDING',
31+ SUCCESS: 'SUCCESS',
32+ FAILURE: 'FAILURE',
33+};
34+
35+const useInviter = () => {
36+ const { contracts, web3, networkType } = useNetwork();
37+ const { wallet, walletType, walletHdPath } = useWallet();
38+ const { pointCursor } = usePointCursor();
39+ const { syncInvites } = usePointCache();
40+ const { gasPrice } = useGasPrice();
41+ const point = need.point(pointCursor);
42+
43+ const [needFunds, setNeedFunds] = useState(null);
44+ const [invites, setInvites] = useState([]);
45+
46+ const [progress, setProgress] = useState(0);
47+ const [txStatus, setTxStatus] = useState(STATUS.INPUT);
48+
49+ const isDone = txStatus === STATUS.SUCCESS;
50+ useEffect(() => {
51+ if (isDone) {
52+ syncInvites(point);
53+ }
54+ }, [isDone, syncInvites, point]);
55+
56+ const resetInvites = useCallback(() => {
57+ setProgress(0);
58+ setInvites([]);
59+ setTxStatus(STATUS.INPUT);
60+ }, [setProgress, setInvites, setTxStatus]);
61+
62+ const generateInvites = useCallback(
63+ async numInvites => {
64+ const _contracts = contracts.getOrElse(null);
65+ const _web3 = web3.getOrElse(null);
66+ const _wallet = wallet.getOrElse(null);
67+ if (!_contracts || !_web3 || !_wallet) {
68+ // not using need because we want a custom error
69+ throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
70+ }
71+
72+ const nonce = await _web3.eth.getTransactionCount(_wallet.address);
73+ const chainId = await _web3.eth.net.getId();
74+ const planets = await azimuth.delegatedSending.getPlanetsToSend(
75+ _contracts,
76+ point,
77+ numInvites
78+ );
79+
80+ setTxStatus(STATUS.SENDING);
81+ // account for the race condition where invites got used up while we were
82+ // composing our target list
83+ if (planets.length < numInvites) {
84+ // resync invites to the cache, since they're out of date
85+ syncInvites(point);
86+
87+ setTxStatus(STATUS.FAILURE);
88+
89+ return {
90+ errors: {
91+ [FORM_ERROR]:
92+ `Can currently only send ${planets.length} invites. ` +
93+ `Please remove invites until you are within the limit.`,
94+ },
95+ };
96+ }
97+
98+ // NB(shrugs) - must be processed in serial because main thread, etc
99+ let signedInvites = [];
100+ let errorCount = 0;
101+ for (let i = 0; i < numInvites; i++) {
102+ setProgress(x => x + 1);
103+ try {
104+ const planet = planets[i];
105+
106+ const { ticket, owner } = await wg.generateTemporaryTicketAndWallet(
107+ MIN_PLANET
108+ // we're always giving planets, so generate a ticket of the correct size
109+ );
110+
111+ const inviteTx = azimuth.delegatedSending.sendPoint(
112+ _contracts,
113+ point,
114+ planet,
115+ owner.keys.address
116+ );
117+
118+ const signedTx = await signTransaction({
119+ wallet: _wallet,
120+ walletType,
121+ walletHdPath,
122+ networkType,
123+ chainId,
124+ nonce: nonce + i,
125+ // TODO: ^ make a useTransactionSigner to encapsulate this logic
126+ txn: inviteTx,
127+ gasPrice: gasPrice.toFixed(), // expects string gwei
128+ gasLimit: GAS_LIMIT.toFixed(),
129+ });
130+
131+ const rawTx = hexify(signedTx.serialize());
132+
133+ signedInvites.push({ ticket, signedTx, rawTx });
134+ } catch (error) {
135+ console.error(error);
136+ return {
137+ errors: {
138+ [WARNING]: `There was an error while generating wallets.`,
139+ },
140+ };
141+ }
142+ }
143+
144+ const totalCost = toBN(GAS_LIMIT)
145+ .mul(toBN(gasPrice))
146+ .mul(toBN(numInvites));
147+ const tankWasUsed = await tank.ensureFundsFor(
148+ _web3,
149+ point,
150+ _wallet.address,
151+ toWei(totalCost.toString(), 'gwei'),
152+ Object.keys(invites).map(name => invites[name].rawTx),
153+ (address, minBalance, balance) =>
154+ setNeedFunds({ address, minBalance, balance }),
155+ () => setNeedFunds(undefined)
156+ );
157+
158+ let unsentInvites = [];
159+ let confirmedInvites = [];
160+ const txAndMailings = signedInvites.map(async invite => {
161+ try {
162+ const txHash = await sendSignedTransaction(
163+ _web3,
164+ invite.signedTx,
165+ tankWasUsed
166+ );
167+
168+ await waitForTransactionConfirm(_web3, txHash);
169+ confirmedInvites.push(invite);
170+ } catch (error) {
171+ console.error(error);
172+ unsentInvites.push(invite);
173+ return;
174+ }
175+ });
176+
177+ await Promise.all(txAndMailings);
178+
179+ setTxStatus(STATUS.SUCCESS);
180+ setInvites(confirmedInvites);
181+
182+ if (unsentInvites.length > 0) {
183+ return { errors: { [FORM_ERROR]: unsentInvites } };
184+ }
185+
186+ if (errorCount > 0) {
187+ return {
188+ errors: {
189+ [WARNING]: `There ${pluralize(
190+ errorCount,
191+ 'was',
192+ 'were'
193+ )} ${pluralize(
194+ errorCount,
195+ 'error'
196+ )} while generating wallets. You can still send the invites that generated correctly.`,
197+ },
198+ };
199+ }
200+ return { invites: confirmedInvites };
201+ },
202+ [
203+ contracts,
204+ gasPrice,
205+ invites,
206+ networkType,
207+ point,
208+ syncInvites,
209+ wallet,
210+ walletHdPath,
211+ walletType,
212+ web3,
213+ ]
214+ );
215+
216+ return {
217+ progress,
218+ txStatus,
219+ needFunds,
220+ invites,
221+ generateInvites,
222+ resetInvites,
223+ };
224+};
225+
226+export default useInviter;