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