on 6cfddff help sign in
bridge/src/lib/reticket.js
287 lines 7.4 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, INITIAL_NETWORK_KEY_REVISION } from './constants';
18import { toBN } from 'web3-utils';
19import { safeToWei } from './lib';
20import { isGalaxy, isPlanet } from './utils/point';
22export const TRANSACTION_PROGRESS = {
23 GENERATING: 0.01,
24 SIGNING: 0.15,
25 FUNDING: 0.3,
26 TRANSFERRING: 0.55,
27 CLEANING: 0.95,
28 DONE: 1.0,
29};
31export async function reticketPointBetweenWallets({
32 fromWallet,
33 fromWalletType,
34 fromWalletHdPath,
35 toWallet,
36 point,
37 web3,
38 contracts,
39 networkType,
40 onUpdate,
41 transferEth = false,
42 nextRevision = INITIAL_NETWORK_KEY_REVISION,
43 txnSigner,
44 txnSender,
45 doSetSpawnProxy = true,
46}) {
47 const askForFunding = (address, minBalance, balance) =>
48 onUpdate({
49 type: 'askFunding',
50 value: { address, minBalance, balance },
51 });
53 const gotFunding = () => onUpdate({ type: 'gotFunding' });
54 const progress = state => onUpdate({ type: 'progress', state });
56 //
57 // generate transactions
58 //
60 progress(TRANSACTION_PROGRESS.GENERATING);
62 // transfer to invite wallet, so it can configure
63 //NOTE doing this in the invite wallet instead of the final wallet makes
64 // failure cases "solvable" by just telling the user to fully retry.
65 // in other words, the invite flow is effectively idempotent.
67 //TODO no harm done if we already owned it, but should still get a bool arg
68 // for skipping this, if it isn't too big a burden for callers
69 const transferTmpTx = azimuth.ecliptic.transferPoint(
70 contracts,
71 point,
72 fromWallet.address,
73 false
74 );
75 transferTmpTx.gas = GAS_LIMITS.TRANSFER;
77 // configure networking public keys
78 const seed = await deriveNetworkSeedFromUrbitWallet(toWallet, nextRevision);
80 const networkSeed = seed.matchWith({
81 Nothing: () => {
82 throw new Error('network seed not derived');
83 },
84 Just: p => p.value,
85 });
87 const networkKeys = deriveNetworkKeys(networkSeed);
89 const keysTx = azimuth.ecliptic.configureKeys(
90 contracts,
91 point,
92 addHexPrefix(networkKeys.crypt.public),
93 addHexPrefix(networkKeys.auth.public),
94 CRYPTO_SUITE_VERSION,
95 /* discontinuous */ false
96 );
97 keysTx.gas = GAS_LIMITS.CONFIGURE_KEYS;
99 // configure management proxy
101 const managementTx = azimuth.ecliptic.setManagementProxy(
102 contracts,
103 point,
104 toWallet.management.keys.address
105 );
106 managementTx.gas = GAS_LIMITS.SET_PROXY;
108 // set spawn & voting proxies situationally
110 // skip this step if reticketing an L1 point with L2-migrated spawn proxy
111 let txs = [transferTmpTx, keysTx, managementTx];
112 if (!isPlanet(point) && doSetSpawnProxy) {
113 const spawnTx = azimuth.ecliptic.setSpawnProxy(
114 contracts,
115 point,
116 toWallet.spawn.keys.address
117 );
118 spawnTx.gas = GAS_LIMITS.SET_PROXY;
119 txs.push(spawnTx);
120 }
122 if (isGalaxy(point)) {
123 const votingTx = azimuth.ecliptic.setVotingProxy(
124 contracts,
125 point,
126 toWallet.voting.keys.address
127 );
128 votingTx.gas = GAS_LIMITS.SET_PROXY;
129 txs.push(votingTx);
130 }
132 // transfer configured point to user's new wallet
134 const transferFinalTx = azimuth.ecliptic.transferPoint(
135 contracts,
136 point,
137 toWallet.ownership.keys.address,
138 false
139 );
140 transferFinalTx.gas = GAS_LIMITS.TRANSFER;
141 txs.push(transferFinalTx);
143 //
144 // finalizing & signing transactions
145 //
147 progress(TRANSACTION_PROGRESS.SIGNING);
149 const gasPriceData = (await getSuggestedGasPrice(networkType)).average;
150 const gasPriceGwei = gasPriceData.price.toFixed();
151 const chainId = await web3.eth.net.getId();
152 const gasPriceWeiBN = toBN(safeToWei(gasPriceGwei, 'gwei'));
153 let inviteNonce = await web3.eth.getTransactionCount(fromWallet.address);
154 const totalCost = txs.reduce(
155 (acc, tx) => acc.add(gasPriceWeiBN.mul(toBN(tx.gas))),
156 toBN(0)
157 );
159 let txPairs = [];
161 // Must be done in serial else hardware will give nonce errors
162 for (let i = 0; i < txs.length; i++) {
163 const stx = await signTransaction({
164 wallet: fromWallet,
165 walletType: fromWalletType,
166 walletHdPath: fromWalletHdPath,
167 networkType,
168 txn: txs[i],
169 nonce: inviteNonce + i,
170 chainId,
171 gasPriceData,
172 gasLimit: txs[i].gas,
173 txnSigner,
174 txnSender,
175 web3,
176 });
177 txPairs.push({
178 raw: hexify(stx.serialize()),
179 signed: stx,
180 });
181 }
183 inviteNonce = inviteNonce + txs.length;
185 //
186 // ensuring funding for transactions
187 //
189 progress(TRANSACTION_PROGRESS.FUNDING);
191 const usedTank = await tank.ensureFundsFor(
192 web3,
193 point,
194 fromWallet.address,
195 fromWalletType,
196 totalCost,
197 txPairs.map(p => p.raw),
198 askForFunding,
199 gotFunding
200 );
202 //
203 // sending and awaiting transactions
204 //
206 progress(TRANSACTION_PROGRESS.TRANSFERRING);
208 const receipts = await sendAndAwaitAllSerial(
209 web3,
210 txPairs.map(p => p.signed),
211 usedTank
212 );
214 const realCost = gasPriceWeiBN.mul(
215 receipts.reduce((sum, receipt) => {
216 return sum.add(toBN(receipt.gasUsed));
217 }, toBN(0))
218 );
220 //
221 // move leftover eth
222 //
224 progress(TRANSACTION_PROGRESS.CLEANING);
226 let balance = toBN(await web3.eth.getBalance(fromWallet.address));
227 const gasLimit = GAS_LIMITS.SEND_ETH;
228 const sendEthCost = gasPriceWeiBN.mul(toBN(gasLimit));
230 // if we have funds left over from what the gas tank gave us, send those back
231 if (usedTank && realCost.add(sendEthCost).lt(totalCost)) {
232 try {
233 const remainder = totalCost.sub(realCost).sub(sendEthCost);
234 console.log('returning to gas tank', remainder.toString());
235 const txn = { to: tank.TANK_ADDRESS, value: remainder };
236 const stx = await signTransaction({
237 wallet: fromWallet,
238 walletType: fromWalletType,
239 walletHdPath: fromWalletHdPath,
240 txn,
241 chainId,
242 networkType,
243 gasPriceData,
244 gasLimit,
245 nonce: inviteNonce,
246 web3,
247 });
248 sendSignedTransaction(web3, stx);
250 // only update these after the above succeeded
251 inviteNonce++;
252 balance = balance.sub(remainder).sub(sendEthCost);
253 } catch (err) {
254 console.log('error returning gas, safely ignored:');
255 console.log(err);
256 }
257 }
259 // if non-trivial eth left in invite wallet, transfer to new ownership
260 if (transferEth && balance.gt(sendEthCost)) {
261 try {
262 const value = balance.sub(sendEthCost);
263 const txn = {
264 to: toWallet.ownership.keys.address,
265 value: value,
266 };
267 const stx = await signTransaction({
268 wallet: fromWallet,
269 walletType: fromWalletType,
270 walletHdPath: fromWalletHdPath,
271 txn,
272 chainId,
273 networkType,
274 gasPriceData,
275 gasLimit,
276 nonce: inviteNonce,
277 web3,
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);