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