on 062509b help sign in
bridge/src/lib/useInviter.js
226 lines 6.3 kB history raw
1import { useState, useCallback, useEffect } from 'react';
2import { toWei, toBN } from 'web3-utils';
3import * as azimuth from 'azimuth-js';
4import { FORM_ERROR } from 'final-form';
6import * as wg from 'lib/walletgen';
7import * as tank from 'lib/tank';
8import * as need from 'lib/need';
9import pluralize from 'lib/pluralize';
10import { GAS_LIMITS } from 'lib/constants';
11import {
12 signTransaction,
13 sendSignedTransaction,
14 waitForTransactionConfirm,
15 hexify,
16} from 'lib/txn';
17import useGasPrice from 'lib/useGasPrice';
18import { MIN_PLANET } from 'lib/constants';
20import { useNetwork } from 'store/network';
21import { useWallet } from 'store/wallet';
22import { usePointCursor } from 'store/pointCursor';
23import { usePointCache } from 'store/pointCache';
25import { WARNING } from 'form/helpers';
27const GAS_LIMIT = GAS_LIMITS.GIFT_PLANET;
28const STATUS = {
29 INPUT: 'INPUT',
30 SENDING: 'SENDING',
31 SUCCESS: 'SUCCESS',
32 FAILURE: 'FAILURE',
33};
35const 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);
43 const [needFunds, setNeedFunds] = useState(null);
44 const [invites, setInvites] = useState([]);
46 const [progress, setProgress] = useState(0);
47 const [txStatus, setTxStatus] = useState(STATUS.INPUT);
49 const isDone = txStatus === STATUS.SUCCESS;
50 useEffect(() => {
51 if (isDone) {
52 syncInvites(point);
53 }
54 }, [isDone, syncInvites, point]);
56 const resetInvites = useCallback(() => {
57 setProgress(0);
58 setInvites([]);
59 setTxStatus(STATUS.INPUT);
60 }, [setProgress, setInvites, setTxStatus]);
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 }
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 );
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);
87 setTxStatus(STATUS.FAILURE);
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 }
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];
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 );
111 const inviteTx = azimuth.delegatedSending.sendPoint(
112 _contracts,
113 point,
114 planet,
115 owner.keys.address
116 );
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 });
131 const rawTx = hexify(signedTx.serialize());
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 }
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 );
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 );
168 await waitForTransactionConfirm(_web3, txHash);
169 confirmedInvites.push(invite);
170 } catch (error) {
171 console.error(error);
172 unsentInvites.push(invite);
173 return;
174 }
175 });
177 await Promise.all(txAndMailings);
179 setTxStatus(STATUS.SUCCESS);
180 setInvites(confirmedInvites);
182 if (unsentInvites.length > 0) {
183 return { errors: { [FORM_ERROR]: unsentInvites } };
184 }
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 );
216 return {
217 progress,
218 txStatus,
219 needFunds,
220 invites,
221 generateInvites,
222 resetInvites,
223 };
224};
226export default useInviter;