on ff9fd78 help sign in
bridge/src/lib/reticket.js
287 lines 7.3 kB history raw
1import * as azimuth from 'azimuth-js';
2import * as tank from './tank';
4import { hexify } from './txn';
5import {
6 deriveNetworkSeedFromUrbitWallet,
7 deriveNetworkKeys,
8 CRYPTO_SUITE_VERSION,
9} from './keys';
10import { addHexPrefix } from './utils/address';
11import {
12 sendAndAwaitAllSerial,
13 signTransaction,
14 sendSignedTransaction,
15} from './txn';
16import getSuggestedGasPrice from './getSuggestedGasPrice';
17import { GAS_LIMITS } from './constants';
18import { toBN } from 'web3-utils';
19import { safeToWei } from './lib';
21// the initial network key revision is always 1
22const INITIAL_NETWORK_KEY_REVISION = 1;
24export const TRANSACTION_PROGRESS = {
25 GENERATING: 0.01,
26 SIGNING: 0.15,
27 FUNDING: 0.3,
28 TRANSFERRING: 0.55,
29 CLEANING: 0.95,
30 DONE: 1.0,
31};
33export async function reticketPointBetweenWallets({
34 fromWallet,
35 fromWalletType,
36 fromWalletHdPath,
37 toWallet,
38 point,
39 web3,
40 contracts,
41 networkType,
42 onUpdate,
43 transferEth = false,
44 nextRevision = INITIAL_NETWORK_KEY_REVISION,
45 txnSigner,
46 txnSender,
47}) {
48 const askForFunding = (address, minBalance, balance) =>
49 onUpdate({
50 type: 'askFunding',
51 value: { address, minBalance, balance },
52 });
54 const gotFunding = () => onUpdate({ type: 'gotFunding' });
55 const progress = state => onUpdate({ type: 'progress', state });
57 //
58 // generate transactions
59 //
61 progress(TRANSACTION_PROGRESS.GENERATING);
63 // transfer to invite wallet, so it can configure
64 //NOTE doing this in the invite wallet instead of the final wallet makes
65 // failure cases "solvable" by just telling the user to fully retry.
66 // in other words, the invite flow is effectively idempotent.
68 //TODO no harm done if we already owned it, but should still get a bool arg
69 // for skipping this, if it isn't too big a burden for callers
70 const transferTmpTx = azimuth.ecliptic.transferPoint(
71 contracts,
72 point,
73 fromWallet.address,
74 false
75 );
76 transferTmpTx.gas = GAS_LIMITS.TRANSFER;
78 // configure networking public keys
79 const seed = await deriveNetworkSeedFromUrbitWallet(toWallet, nextRevision);
81 const networkSeed = seed.matchWith({
82 Nothing: () => {
83 throw new Error('network seed not derived');
84 },
85 Just: p => p.value,
86 });
88 const networkKeys = deriveNetworkKeys(networkSeed);
90 const keysTx = azimuth.ecliptic.configureKeys(
91 contracts,
92 point,
93 addHexPrefix(networkKeys.crypt.public),
94 addHexPrefix(networkKeys.auth.public),
95 CRYPTO_SUITE_VERSION,
96 /* discontinuous */ false
97 );
98 keysTx.gas = GAS_LIMITS.CONFIGURE_KEYS;
100 // configure management proxy
102 const managementTx = azimuth.ecliptic.setManagementProxy(
103 contracts,
104 point,
105 toWallet.management.keys.address
106 );
107 managementTx.gas = GAS_LIMITS.SET_PROXY;
109 // set spawn & voting proxies situationally
111 let txs = [transferTmpTx, keysTx, managementTx];
112 if (
113 azimuth.azimuth.getPointSize(point) !== azimuth.azimuth.PointSize.Planet
114 ) {
115 const spawnTx = azimuth.ecliptic.setSpawnProxy(
116 contracts,
117 point,
118 toWallet.spawn.keys.address
119 );
120 spawnTx.gas = GAS_LIMITS.SET_PROXY;
121 txs.push(spawnTx);
123 if (
124 azimuth.azimuth.getPointSize(point) === azimuth.azimuth.PointSize.Galaxy
125 ) {
126 const votingTx = azimuth.ecliptic.setVotingProxy(
127 contracts,
128 point,
129 toWallet.voting.keys.address
130 );
131 votingTx.gas = GAS_LIMITS.SET_PROXY;
132 txs.push(votingTx);
133 }
134 }
136 // transfer configured point to user's new wallet
138 const transferFinalTx = azimuth.ecliptic.transferPoint(
139 contracts,
140 point,
141 toWallet.ownership.keys.address,
142 false
143 );
144 transferFinalTx.gas = GAS_LIMITS.TRANSFER;
145 txs.push(transferFinalTx);
147 //
148 // finalizing & signing transactions
149 //
151 progress(TRANSACTION_PROGRESS.SIGNING);
153 const gasPriceGwei = (await getSuggestedGasPrice(networkType)).toFixed();
154 const chainId = await web3.eth.net.getId();
155 const gasPriceWeiBN = toBN(safeToWei(gasPriceGwei, 'gwei'));
156 let inviteNonce = await web3.eth.getTransactionCount(fromWallet.address);
157 const totalCost = txs.reduce(
158 (acc, tx) => acc.add(gasPriceWeiBN.mul(toBN(tx.gas))),
159 toBN(0)
160 );
162 let txPairs = [];
164 // Must be done in serial else hardware will give nonce errors
165 for (let i = 0; i < txs.length; i++) {
166 const stx = await signTransaction({
167 wallet: fromWallet,
168 walletType: fromWalletType,
169 walletHdPath: fromWalletHdPath,
170 networkType,
171 txn: txs[i],
172 nonce: inviteNonce + i,
173 chainId,
174 gasPrice: gasPriceGwei,
175 gasLimit: txs[i].gas,
176 txnSigner,
177 txnSender,
178 });
179 txPairs.push({
180 raw: hexify(stx.serialize()),
181 signed: stx,
182 });
183 }
185 inviteNonce = inviteNonce + txs.length;
187 //
188 // ensuring funding for transactions
189 //
191 progress(TRANSACTION_PROGRESS.FUNDING);
193 const usedTank = await tank.ensureFundsFor(
194 web3,
195 point,
196 fromWallet.address,
197 fromWalletType,
198 totalCost,
199 txPairs.map(p => p.raw),
200 askForFunding,
201 gotFunding
202 );
204 //
205 // sending and awaiting transactions
206 //
208 progress(TRANSACTION_PROGRESS.TRANSFERRING);
210 const receipts = await sendAndAwaitAllSerial(
211 web3,
212 txPairs.map(p => p.signed),
213 usedTank
214 );
216 const realCost = gasPriceWeiBN.mul(
217 receipts.reduce((sum, receipt) => {
218 return sum.add(toBN(receipt.gasUsed));
219 }, toBN(0))
220 );
222 //
223 // move leftover eth
224 //
226 progress(TRANSACTION_PROGRESS.CLEANING);
228 let balance = toBN(await web3.eth.getBalance(fromWallet.address));
229 const gasLimit = GAS_LIMITS.SEND_ETH;
230 const sendEthCost = gasPriceWeiBN.mul(toBN(gasLimit));
232 // if we have funds left over from what the gas tank gave us, send those back
233 if (usedTank && realCost.add(sendEthCost).lt(totalCost)) {
234 try {
235 const remainder = totalCost.sub(realCost).sub(sendEthCost);
236 console.log('returning to gas tank', remainder.toString());
237 const txn = { to: tank.TANK_ADDRESS, value: remainder };
238 const stx = await signTransaction({
239 wallet: fromWallet,
240 walletType: fromWalletType,
241 walletHdPath: fromWalletHdPath,
242 txn,
243 chainId,
244 networkType,
245 gasPrice: gasPriceGwei,
246 gasLimit,
247 nonce: inviteNonce,
248 });
249 sendSignedTransaction(web3, stx);
251 // only update these after the above succeeded
252 inviteNonce++;
253 balance = balance.sub(remainder).sub(sendEthCost);
254 } catch (err) {
255 console.log('error returning gas, safely ignored:');
256 console.log(err);
257 }
258 }
260 // if non-trivial eth left in invite wallet, transfer to new ownership
261 if (transferEth && balance.gt(sendEthCost)) {
262 try {
263 const value = balance.sub(sendEthCost);
264 const txn = {
265 to: toWallet.ownership.keys.address,
266 value: value,
267 };
268 const stx = await signTransaction({
269 wallet: fromWallet,
270 walletType: fromWalletType,
271 walletHdPath: fromWalletHdPath,
272 txn,
273 chainId,
274 networkType,
275 gasPrice: gasPriceGwei,
276 gasLimit,
277 nonce: inviteNonce,
278 });
279 sendSignedTransaction(web3, stx);
280 } catch (err) {
281 console.log('error sending value tx, safely ignored:');
282 console.log(err);
283 }
284 }
286 progress(TRANSACTION_PROGRESS.DONE);