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