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