on c53aba4 help sign in
bridge/src/lib/reticket.js
220 lines 5.7 kB history raw
1import Tx from 'ethereumjs-tx';
2import * as azimuth from 'azimuth-js';
3import * as tank from './tank';
5import { hexify } from './txn';
6import {
7 deriveNetworkSeedFromUrbitWallet,
8 deriveNetworkKeys,
9 CRYPTO_SUITE_VERSION,
10} from './keys';
11import { addHexPrefix } from './wallet';
12import { sendAndAwaitAll } from './txn';
13import getSuggestedGasPrice from './getSuggestedGasPrice';
14import { GAS_LIMITS } from './constants';
15import { toBN } from 'web3-utils';
16import { safeToWei } from './lib';
18// the initial network key revision is always 1
19const INITIAL_NETWORK_KEY_REVISION = 1;
21export const TRANSACTION_PROGRESS = {
22 GENERATING: 0.01,
23 SIGNING: 0.15,
24 FUNDING: 0.3,
25 TRANSFERRING: 0.55,
26 CLEANING: 0.95,
27 DONE: 1.0,
28};
30export async function reticketPointBetweenWallets({
31 fromWallet,
32 toWallet,
33 point,
34 web3,
35 contracts,
36 networkType,
37 onUpdate,
38 transferEth = false,
39 nextRevision = INITIAL_NETWORK_KEY_REVISION,
40}) {
41 const askForFunding = (address, minBalance, balance) =>
42 onUpdate({
43 type: 'askFunding',
44 value: { address, minBalance, balance },
45 });
47 const gotFunding = () => onUpdate({ type: 'gotFunding' });
48 const progress = state => onUpdate({ type: 'progress', state });
50 //
51 // generate transactions
52 //
54 progress(TRANSACTION_PROGRESS.GENERATING);
56 // transfer to invite wallet, so it can configure
57 //NOTE doing this in the invite wallet instead of the final wallet makes
58 // failure cases "solvable" by just telling the user to fully retry.
59 // in other words, the invite flow is effectively idempotent.
61 //TODO no harm done if we already owned it, but should still get a bool arg
62 // for skipping this, if it isn't too big a burden for callers
63 const transferTmpTx = azimuth.ecliptic.transferPoint(
64 contracts,
65 point,
66 fromWallet.address,
67 false
68 );
69 transferTmpTx.gas = GAS_LIMITS.TRANSFER;
71 // configure networking public keys
72 const seed = await deriveNetworkSeedFromUrbitWallet(toWallet, nextRevision);
74 const networkSeed = seed.matchWith({
75 Nothing: () => {
76 throw new Error('network seed not derived');
77 },
78 Just: p => p.value,
79 });
81 const networkKeys = deriveNetworkKeys(networkSeed);
83 const keysTx = azimuth.ecliptic.configureKeys(
84 contracts,
85 point,
86 addHexPrefix(networkKeys.crypt.public),
87 addHexPrefix(networkKeys.auth.public),
88 CRYPTO_SUITE_VERSION,
89 /* discontinuous */ false
90 );
91 keysTx.gas = GAS_LIMITS.CONFIGURE_KEYS;
93 // configure management proxy
95 const managementTx = azimuth.ecliptic.setManagementProxy(
96 contracts,
97 point,
98 toWallet.management.keys.address
99 );
100 managementTx.gas = GAS_LIMITS.SET_PROXY;
102 // set spawn & voting proxies situationally
104 let txs = [transferTmpTx, keysTx, managementTx];
105 if (
106 azimuth.azimuth.getPointSize(point) !== azimuth.azimuth.PointSize.Planet
107 ) {
108 const spawnTx = azimuth.ecliptic.setSpawnProxy(
109 contracts,
110 point,
111 toWallet.spawn.keys.address
112 );
113 spawnTx.gas = GAS_LIMITS.SET_PROXY;
114 txs.push(spawnTx);
116 if (
117 azimuth.azimuth.getPointSize(point) === azimuth.azimuth.PointSize.Galaxy
118 ) {
119 const votingTx = azimuth.ecliptic.setVotingProxy(
120 contracts,
121 point,
122 toWallet.voting.keys.address
123 );
124 votingTx.gas = GAS_LIMITS.SET_PROXY;
125 txs.push(votingTx);
126 }
127 }
129 // transfer configured point to user's new wallet
131 const transferFinalTx = azimuth.ecliptic.transferPoint(
132 contracts,
133 point,
134 toWallet.ownership.keys.address,
135 false
136 );
137 transferFinalTx.gas = GAS_LIMITS.TRANSFER;
138 txs.push(transferFinalTx);
140 //
141 // finalizing & signing transactions
142 //
144 progress(TRANSACTION_PROGRESS.SIGNING);
146 const suggestedGasPrice = await getSuggestedGasPrice(networkType);
147 const gasPrice = safeToWei(suggestedGasPrice.toFixed(), 'gwei');
148 const gasPriceBN = toBN(gasPrice);
149 let totalCost = toBN(0);
150 let inviteNonce = await web3.eth.getTransactionCount(fromWallet.address);
151 txs = txs.map(tx => {
152 tx.from = fromWallet.address;
153 tx.nonce = inviteNonce++;
154 tx.gasPrice = gasPriceBN;
155 totalCost = totalCost.add(gasPriceBN.mul(toBN(tx.gas)));
156 return tx;
157 });
159 let txPairs = txs.map(tx => {
160 let stx = new Tx(tx);
161 stx.sign(Buffer.from(fromWallet.privateKey, 'hex'));
162 return {
163 raw: hexify(stx.serialize()),
164 signed: stx,
165 };
166 });
168 //
169 // ensuring funding for transactions
170 //
172 progress(TRANSACTION_PROGRESS.FUNDING);
174 const usedTank = await tank.ensureFundsFor(
175 web3,
176 point,
177 fromWallet.address,
178 totalCost,
179 txPairs.map(p => p.raw),
180 askForFunding,
181 gotFunding
182 );
184 //
185 // sending and awaiting transactions
186 //
188 progress(TRANSACTION_PROGRESS.TRANSFERRING);
190 await sendAndAwaitAll(web3, txPairs.map(p => p.signed), usedTank);
192 //
193 // move leftover eth
194 //
196 progress(TRANSACTION_PROGRESS.CLEANING);
198 // if non-trivial eth left in invite wallet, transfer to new ownership
199 let balance = toBN(await web3.eth.getBalance(fromWallet.address));
200 const gasLimit = GAS_LIMITS.SEND_ETH;
201 const sendEthCost = gasPriceBN.mul(toBN(gasLimit));
202 if (transferEth && balance.gt(sendEthCost)) {
203 const value = balance.sub(sendEthCost);
204 const tx = {
205 to: toWallet.ownership.keys.address,
206 value: value,
207 gasPrice: gasPriceBN,
208 gas: gasLimit,
209 nonce: inviteNonce++,
210 };
211 let stx = new Tx(tx);
212 stx.sign(fromWallet.privateKey);
213 const rawTx = hexify(stx.serialize());
214 web3.eth.sendSignedTransaction(rawTx).catch(err => {
215 console.log('error sending value tx, who cares', err);
216 });
217 }
219 progress(TRANSACTION_PROGRESS.DONE);