on 4c8c607 help sign in
bridge/src/lib/reticket.js
283 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 './wallet';
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}) {
46 const askForFunding = (address, minBalance, balance) =>
47 onUpdate({
48 type: 'askFunding',
49 value: { address, minBalance, balance },
50 });
52 const gotFunding = () => onUpdate({ type: 'gotFunding' });
53 const progress = state => onUpdate({ type: 'progress', state });
55 //
56 // generate transactions
57 //
59 progress(TRANSACTION_PROGRESS.GENERATING);
61 // transfer to invite wallet, so it can configure
62 //NOTE doing this in the invite wallet instead of the final wallet makes
63 // failure cases "solvable" by just telling the user to fully retry.
64 // in other words, the invite flow is effectively idempotent.
66 //TODO no harm done if we already owned it, but should still get a bool arg
67 // for skipping this, if it isn't too big a burden for callers
68 const transferTmpTx = azimuth.ecliptic.transferPoint(
69 contracts,
70 point,
71 fromWallet.address,
72 false
73 );
74 transferTmpTx.gas = GAS_LIMITS.TRANSFER;
76 // configure networking public keys
77 const seed = await deriveNetworkSeedFromUrbitWallet(toWallet, nextRevision);
79 const networkSeed = seed.matchWith({
80 Nothing: () => {
81 throw new Error('network seed not derived');
82 },
83 Just: p => p.value,
84 });
86 const networkKeys = deriveNetworkKeys(networkSeed);
88 const keysTx = azimuth.ecliptic.configureKeys(
89 contracts,
90 point,
91 addHexPrefix(networkKeys.crypt.public),
92 addHexPrefix(networkKeys.auth.public),
93 CRYPTO_SUITE_VERSION,
94 /* discontinuous */ false
95 );
96 keysTx.gas = GAS_LIMITS.CONFIGURE_KEYS;
98 // configure management proxy
100 const managementTx = azimuth.ecliptic.setManagementProxy(
101 contracts,
102 point,
103 toWallet.management.keys.address
104 );
105 managementTx.gas = GAS_LIMITS.SET_PROXY;
107 // set spawn & voting proxies situationally
109 let txs = [transferTmpTx, keysTx, managementTx];
110 if (
111 azimuth.azimuth.getPointSize(point) !== azimuth.azimuth.PointSize.Planet
112 ) {
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);
121 if (
122 azimuth.azimuth.getPointSize(point) === azimuth.azimuth.PointSize.Galaxy
123 ) {
124 const votingTx = azimuth.ecliptic.setVotingProxy(
125 contracts,
126 point,
127 toWallet.voting.keys.address
128 );
129 votingTx.gas = GAS_LIMITS.SET_PROXY;
130 txs.push(votingTx);
131 }
132 }
134 // transfer configured point to user's new wallet
136 const transferFinalTx = azimuth.ecliptic.transferPoint(
137 contracts,
138 point,
139 toWallet.ownership.keys.address,
140 false
141 );
142 transferFinalTx.gas = GAS_LIMITS.TRANSFER;
143 txs.push(transferFinalTx);
145 //
146 // finalizing & signing transactions
147 //
149 progress(TRANSACTION_PROGRESS.SIGNING);
151 const gasPriceGwei = (await getSuggestedGasPrice(networkType)).toFixed();
152 const chainId = await web3.eth.net.getId();
153 const gasPriceWeiBN = toBN(safeToWei(gasPriceGwei, 'gwei'));
154 let inviteNonce = await web3.eth.getTransactionCount(fromWallet.address);
155 const totalCost = txs.reduce(
156 (acc, tx) => acc.add(gasPriceWeiBN.mul(toBN(tx.gas))),
157 toBN(0)
158 );
160 let txPairs = [];
162 // Must be done in serial else hardware will give nonce errors
163 for (let i = 0; i < txs.length; i++) {
164 const stx = await signTransaction({
165 wallet: fromWallet,
166 walletType: fromWalletType,
167 walletHdPath: fromWalletHdPath,
168 networkType,
169 txn: txs[i],
170 nonce: inviteNonce + i,
171 chainId,
172 gasPrice: gasPriceGwei,
173 gasLimit: txs[i].gas,
174 });
175 txPairs.push({
176 raw: hexify(stx.serialize()),
177 signed: stx,
178 });
179 }
181 inviteNonce = inviteNonce + txs.length;
183 //
184 // ensuring funding for transactions
185 //
187 progress(TRANSACTION_PROGRESS.FUNDING);
189 const usedTank = await tank.ensureFundsFor(
190 web3,
191 point,
192 fromWallet.address,
193 fromWalletType,
194 totalCost,
195 txPairs.map(p => p.raw),
196 askForFunding,
197 gotFunding
198 );
200 //
201 // sending and awaiting transactions
202 //
204 progress(TRANSACTION_PROGRESS.TRANSFERRING);
206 const receipts = await sendAndAwaitAllSerial(
207 web3,
208 txPairs.map(p => p.signed),
209 usedTank
210 );
212 const realCost = gasPriceWeiBN.mul(
213 receipts.reduce((sum, receipt) => {
214 return sum.add(toBN(receipt.gasUsed));
215 }, toBN(0))
216 );
218 //
219 // move leftover eth
220 //
222 progress(TRANSACTION_PROGRESS.CLEANING);
224 let balance = toBN(await web3.eth.getBalance(fromWallet.address));
225 const gasLimit = GAS_LIMITS.SEND_ETH;
226 const sendEthCost = gasPriceWeiBN.mul(toBN(gasLimit));
228 // if we have funds left over from what the gas tank gave us, send those back
229 if (usedTank && realCost.add(sendEthCost).lt(totalCost)) {
230 try {
231 const remainder = totalCost.sub(realCost).sub(sendEthCost);
232 console.log('returning to gas tank', remainder.toString());
233 const txn = { to: tank.TANK_ADDRESS, value: remainder };
234 const stx = await signTransaction({
235 wallet: fromWallet,
236 walletType: fromWalletType,
237 walletHdPath: fromWalletHdPath,
238 txn,
239 chainId,
240 networkType,
241 gasPrice: gasPriceGwei,
242 gasLimit,
243 nonce: inviteNonce,
244 });
245 sendSignedTransaction(web3, stx);
247 // only update these after the above succeeded
248 inviteNonce++;
249 balance = balance.sub(remainder).sub(sendEthCost);
250 } catch (err) {
251 console.log('error returning gas, safely ignored:');
252 console.log(err);
253 }
254 }
256 // if non-trivial eth left in invite wallet, transfer to new ownership
257 if (transferEth && balance.gt(sendEthCost)) {
258 try {
259 const value = balance.sub(sendEthCost);
260 const txn = {
261 to: toWallet.ownership.keys.address,
262 value: value,
263 };
264 const stx = await signTransaction({
265 wallet: fromWallet,
266 walletType: fromWalletType,
267 walletHdPath: fromWalletHdPath,
268 txn,
269 chainId,
270 networkType,
271 gasPrice: gasPriceGwei,
272 gasLimit,
273 nonce: inviteNonce,
274 });
275 sendSignedTransaction(web3, stx);
276 } catch (err) {
277 console.log('error sending value tx, safely ignored:');
278 console.log(err);
279 }
280 }
282 progress(TRANSACTION_PROGRESS.DONE);