on 82b00c2 help sign in
bridge/src/lib/useInviter.js
223 lines 6.2 kB history raw
1import { useState, useCallback, useEffect } from 'react';
2import { 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';
19import { safeToWei } from 'lib/lib';
21import { useNetwork } from 'store/network';
22import { useWallet } from 'store/wallet';
23import { usePointCursor } from 'store/pointCursor';
24import { usePointCache } from 'store/pointCache';
26import { WARNING } from 'form/helpers';
28const GAS_LIMIT = GAS_LIMITS.GIFT_PLANET;
29const STATUS = {
30 INPUT: 'INPUT',
31 SENDING: 'SENDING',
32 SUCCESS: 'SUCCESS',
33 FAILURE: 'FAILURE',
34};
36const useInviter = () => {
37 const { contracts, web3, networkType } = useNetwork();
38 const { wallet, walletType, walletHdPath, authToken } = useWallet();
39 const { pointCursor } = usePointCursor();
40 const { syncInvites } = usePointCache();
41 const { gasPrice } = useGasPrice();
43 const point = need.point(pointCursor);
45 const [needFunds, setNeedFunds] = useState(null);
46 const [invites, setInvites] = useState([]);
48 const [progress, setProgress] = useState(0);
49 const [txStatus, setTxStatus] = useState(STATUS.INPUT);
51 const isDone = txStatus === STATUS.SUCCESS;
52 useEffect(() => {
53 if (isDone) {
54 syncInvites(point);
55 }
56 }, [isDone, syncInvites, point]);
58 const resetInvites = useCallback(() => {
59 setProgress(0);
60 setInvites([]);
61 setTxStatus(STATUS.INPUT);
62 }, [setProgress, setInvites, setTxStatus]);
64 const generateInvites = useCallback(
65 async numInvites => {
66 const _contracts = contracts.getOrElse(null);
67 const _web3 = web3.getOrElse(null);
68 const _wallet = wallet.getOrElse(null);
69 const _authToken = authToken.getOrElse(null);
70 if (!_contracts || !_web3 || !_wallet || !_authToken) {
71 // not using need because we want a custom error
72 throw new Error('Internal Error: Missing Contracts/Web3/Wallet');
73 }
75 const nonce = await _web3.eth.getTransactionCount(_wallet.address);
76 const chainId = await _web3.eth.net.getId();
77 const planets = await azimuth.delegatedSending.getPlanetsToSend(
78 _contracts,
79 point,
80 numInvites
81 );
83 setTxStatus(STATUS.SENDING);
84 // account for the race condition where invites got used up while we were
85 // composing our target list
86 if (planets.length < numInvites) {
87 // resync invites to the cache, since they're out of date
88 syncInvites(point);
90 setTxStatus(STATUS.FAILURE);
92 return {
93 errors: {
94 [FORM_ERROR]:
95 `Can currently only send ${planets.length} invites. ` +
96 `Please remove invites until you are within the limit.`,
97 },
98 };
99 }
101 // NB(shrugs) - must be processed in serial because main thread, etc
102 let signedInvites = [];
103 for (let i = 0; i < numInvites; i++) {
104 setProgress(x => x + 1);
105 try {
106 const planet = planets[i];
108 const {
109 ticket,
110 owner,
111 } = await wg.generateTemporaryDeterministicWallet(planet, _authToken);
113 const inviteTx = azimuth.delegatedSending.sendPoint(
114 _contracts,
115 point,
116 planet,
117 owner.keys.address
118 );
120 const signedTx = await signTransaction({
121 wallet: _wallet,
122 walletType,
123 walletHdPath,
124 networkType,
125 chainId,
126 nonce: nonce + i,
127 // TODO: ^ make a useTransactionSigner to encapsulate this logic
128 txn: inviteTx,
129 gasPrice: gasPrice.toFixed(), // expects string gwei
130 gasLimit: GAS_LIMIT.toFixed(),
131 });
133 const rawTx = hexify(signedTx.serialize());
135 signedInvites.push({ ticket, signedTx, rawTx });
136 } catch (error) {
137 console.error(error);
138 return {
139 errors: {
140 [WARNING]: `There was an error while generating wallets.`,
141 },
142 };
143 }
144 }
146 const totalCost = toBN(GAS_LIMIT)
147 .mul(toBN(gasPrice))
148 .mul(toBN(numInvites));
149 const tankWasUsed = await tank.ensureFundsFor(
150 _web3,
151 point,
152 _wallet.address,
153 walletType,
154 safeToWei(totalCost, 'gwei'),
155 signedInvites.map(si => si.rawTx),
156 (address, minBalance, balance) =>
157 setNeedFunds({ address, minBalance, balance }),
158 () => setNeedFunds(undefined)
159 );
161 let inviteErrors = [];
162 let confirmedInvites = [];
163 const txAndMailings = signedInvites.map(async invite => {
164 try {
165 const txHash = await sendSignedTransaction(
166 _web3,
167 invite.signedTx,
168 tankWasUsed
169 );
171 await waitForTransactionConfirm(_web3, txHash);
172 confirmedInvites.push(invite);
173 } catch (error) {
174 console.error('Error sending invite tx:', error);
175 if (typeof error === 'string') {
176 inviteErrors.push(error);
177 } else if (typeof error === 'object' && error.message) {
178 inviteErrors.push(error.message);
179 }
180 return;
181 }
182 });
184 await Promise.all(txAndMailings);
186 setTxStatus(STATUS.SUCCESS);
187 setInvites(confirmedInvites);
189 if (inviteErrors.length > 0) {
190 return {
191 errors: {
192 [FORM_ERROR]: ['Error sending invites: ', ...inviteErrors],
193 },
194 };
195 }
197 return { invites: confirmedInvites };
198 },
199 [
200 authToken,
201 contracts,
202 gasPrice,
203 networkType,
204 point,
205 syncInvites,
206 wallet,
207 walletHdPath,
208 walletType,
209 web3,
210 ]
211 );
213 return {
214 progress,
215 txStatus,
216 needFunds,
217 invites,
218 generateInvites,
219 resetInvites,
220 };
221};
223export default useInviter;