on 2043bfc help sign in
bridge/src/lib/invite.js
363 lines 9.6 kB history raw
1import { Just, Nothing } from 'folktale/maybe';
2import Tx from 'ethereumjs-tx';
4import * as azimuth from 'azimuth-js';
5import * as kg from 'urbit-key-generation/dist';
6import * as wg from './walletgen';
7import * as tank from './tank';
9import JSZip from 'jszip';
10import saveAs from 'file-saver';
12import { sendSignedTransaction } from './txn';
13import { BRIDGE_ERROR } from './error';
14import { attemptNetworkSeedDerivation } from './keys';
15import { addHexPrefix, WALLET_TYPES } from './wallet';
17const INVITE_STAGES = {
18 INVITE_LOGIN: 'invite login',
19 INVITE_WALLET: 'invite wallet',
20 INVITE_VERIFY: 'invite verify',
21 INVITE_TRANSACTIONS: 'invite transactions',
22};
24const WALLET_STATES = {
25 UNLOCKING: 'Unlocking invite wallet',
26 GENERATING: 'Generating your wallet',
27 RENDERING: 'Creating paper collateral',
28 PAPER_READY: 'Download your wallet',
29 DOWNLOADED: 'Wallet downloaded',
30 TRANSACTIONS: 'Sending transactions',
31};
33const TRANSACTION_STATES = {
34 GENERATING: {
35 label: 'Generating transactions',
36 pct: '0%',
37 },
38 SIGNING: {
39 label: 'Signing transactions',
40 pct: '15%',
41 },
42 FUNDING_INVITE: {
43 label: 'Funding invite wallet',
44 pct: '30%',
45 },
46 CLAIMING: {
47 label: 'Claiming invite',
48 pct: '55%',
49 },
50 FUNDING_RECIPIENT: {
51 label: 'Funding recipient wallet',
52 pct: '65%',
53 },
54 CONFIGURING: {
55 label: 'Configuring planet',
56 pct: '85%',
57 },
58 CLEANING: {
59 label: 'Cleaning up',
60 pct: '95%',
61 },
62 DONE: {
63 label: 'Done',
64 pct: '100%',
65 },
66};
68async function generateWallet(point) {
69 const ticket = await wg.makeTicket(point);
70 const wallet = await wg.generateWallet(point, ticket);
71 return wallet;
74//TODO should be moved to lib/walletgen
75async function downloadWallet(paper) {
76 return new Promise((resolve, reject) => {
77 const zip = new JSZip();
78 //TODO the categories here aren't explained in bridge at all...
80 const bin0 = paper.filter(item => item.bin === '0');
81 const bin1 = paper.filter(item => item.bin === '1');
82 const bin2 = paper.filter(item => item.bin === '2');
83 const bin3 = paper.filter(item => item.bin === '3');
84 const bin4 = paper.filter(item => item.bin === '4');
86 const bin0Folder = zip.folder('0. Public');
87 const bin1Folder = zip.folder('1. Very High Friction Custody');
88 const bin2Folder = zip.folder('2. High Friction Custody');
89 const bin3Folder = zip.folder('3. Medium Friction Custody');
90 const bin4Folder = zip.folder('4. Low Friction Custody');
92 bin0.forEach(item => bin0Folder.file(`${item.pageTitle}.png`, item.png));
93 bin1.forEach(item => bin1Folder.file(`${item.pageTitle}.png`, item.png));
94 bin2.forEach(item => bin2Folder.file(`${item.pageTitle}.png`, item.png));
95 bin3.forEach(item => bin3Folder.file(`${item.pageTitle}.png`, item.png));
96 bin4.forEach(item => bin4Folder.file(`${item.pageTitle}.png`, item.png));
98 zip.generateAsync({ type: 'blob' }).then(content => {
99 saveAs(content, 'urbit-wallet.zip');
100 resolve(true);
101 });
102 });
105async function startTransactions(args) {
106 let {
107 realPointM,
108 web3: web3M,
109 contracts: contractsM,
110 inviteWalletM,
111 realWalletM,
112 setUrbitWallet,
113 updateProgress,
114 } = args;
116 const askForFunding = (address, amount, current) => {
117 updateProgress({
118 type: 'notify',
119 value: `Please make sure ${address} has at least ${amount} wei, we'll continue once that's true. Current balance: ${current}. Waiting`,
120 });
121 };
123 if (Nothing.hasInstance(web3M)) {
124 throw BRIDGE_ERROR.MISSING_WEB3;
125 }
126 const web3 = web3M.value;
128 if (Nothing.hasInstance(contractsM)) {
129 throw BRIDGE_ERROR.MISSING_CONTRACTS;
130 }
131 const contracts = contractsM.value;
133 if (Nothing.hasInstance(inviteWalletM)) {
134 throw BRIDGE_ERROR.MISSING_WALLET;
135 }
136 const inviteWallet = inviteWalletM.value;
138 if (Nothing.hasInstance(realWalletM)) {
139 throw BRIDGE_ERROR.MISSING_WALLET;
140 }
141 const realWallet = realWalletM.value;
143 // const realWallet = await urbitWalletFromTicket(args.realTicket, point);
145 if (Nothing.hasInstance(realPointM)) {
146 throw BRIDGE_ERROR.MISSING_POINT;
147 }
148 const point = realPointM.value;
150 const inviteAddress = inviteWallet.address;
151 console.log('working as', inviteAddress);
153 // transfer from invite wallet to new wallet
155 let transferTx = azimuth.ecliptic.transferPoint(
156 contracts,
157 point,
158 realWallet.ownership.keys.address
159 );
160 transferTx.gas = 500000; //TODO can maybe be lower?
162 // ping gas tank with txs if needed
164 let inviteNonce = await web3.eth.getTransactionCount(inviteAddress);
165 transferTx.nonce = inviteNonce++;
166 transferTx.gasPrice = 20000000000; //NOTE we pay the premium for faster ux
167 transferTx.from = inviteAddress;
169 //NOTE using web3.eth.accounts.signTransaction is broken (1.0.0-beta51)
170 let transferStx = new Tx(transferTx);
171 transferStx.sign(inviteWallet.privateKey);
172 let rawTransferStx = '0x' + transferStx.serialize().toString('hex');
174 const transferCost = transferTx.gas * transferTx.gasPrice;
175 updateProgress({
176 type: 'progress',
177 value: TRANSACTION_STATES.FUNDING_INVITE,
178 });
179 let usedTank = await tank.ensureFundsFor(
180 web3,
181 point,
182 inviteAddress,
183 transferCost,
184 [rawTransferStx],
185 askForFunding
186 );
188 updateProgress({
189 type: 'progress',
190 value: TRANSACTION_STATES.CLAIMING,
191 });
193 // send transaction
194 await sendAndAwaitConfirm(web3, [Just(transferStx)], usedTank);
196 //
197 // we're gonna be operating as the new wallet from here on out, so change
198 // the relevant values
199 //TODO though, should we be? it might be better, especially for the
200 // reticketing case, to try and exhaust the old wallet first
201 setUrbitWallet(Just(realWallet));
202 const newAddress = realWallet.ownership.keys.address;
204 // configure management proxy
206 let managementTx = azimuth.ecliptic.setManagementProxy(
207 contracts,
208 point,
209 realWallet.management.keys.address
210 );
211 managementTx.gas = 200000;
212 managementTx.nonce = 0;
214 // configure networking public keys
215 //TODO feels like more of this logic should live in a lib?
216 const seed = await attemptNetworkSeedDerivation(true, {
217 walletType: WALLET_TYPES.TICKET,
218 urbitWallet: Just(realWallet),
219 pointCursor: Just(point),
220 pointCache: { [point]: { keyRevisionNumber: 0 } },
221 });
223 const networkSeed = seed.matchWith({
224 Nothing: () => {
225 throw new Error('network seed not derived');
226 },
227 Just: p => p.value,
228 });
230 const networkKeys = kg.deriveNetworkKeys(networkSeed);
232 let keysTx = azimuth.ecliptic.configureKeys(
233 contracts,
234 point,
235 addHexPrefix(networkKeys.crypt.public),
236 addHexPrefix(networkKeys.auth.public),
237 1,
238 false
239 );
240 keysTx.gas = 150000;
241 keysTx.nonce = 1;
243 let txs = [managementTx, keysTx];
245 if (
246 azimuth.azimuth.getPointSize(point) !== azimuth.azimuth.PointSize.Planet
247 ) {
248 let spawnTx = azimuth.ecliptic.setSpawnProxy(
249 contracts,
250 point,
251 realWallet.spawn.keys.address
252 );
253 spawnTx.gas = 200000;
254 spawnTx.nonce = 2;
255 txs.push(spawnTx);
257 if (
258 azimuth.azimuth.getPointSize(point) === azimuth.azimuth.PointSize.Galaxy
259 ) {
260 let votingTx = azimuth.ecliptic.setVotingProxy(
261 contracts,
262 point,
263 realWallet.voting.keys.address
264 );
265 votingTx.gas = 200000;
266 votingTx.nonce = 3;
267 txs.push(votingTx);
268 }
269 }
271 let totalCost = 0;
272 txs = txs.map(tx => {
273 tx.gasPrice = 20000000000;
274 tx.from = newAddress;
275 totalCost = totalCost + tx.gasPrice * tx.gas;
276 return tx;
277 });
279 let txPairs = txs.map(tx => {
280 let stx = new Tx(tx);
281 stx.sign(Buffer.from(realWallet.ownership.keys.private, 'hex'));
282 return {
283 raw: '0x' + stx.serialize().toString('hex'),
284 signed: stx,
285 };
286 });
288 updateProgress({
289 type: 'progress',
290 value: TRANSACTION_STATES.FUNDING_RECIPIENT,
291 });
292 usedTank = await tank.ensureFundsFor(
293 web3,
294 point,
295 newAddress,
296 totalCost,
297 txPairs.map(p => p.raw),
298 askForFunding
299 );
301 updateProgress({
302 type: 'progress',
303 value: TRANSACTION_STATES.CONFIGURING,
304 });
306 await sendAndAwaitConfirm(web3, txPairs.map(p => Just(p.signed)), usedTank);
308 updateProgress({
309 type: 'progress',
310 value: TRANSACTION_STATES.CLEANING,
311 });
313 // if non-trivial eth left in invite wallet, transfer to new ownership
314 let balance = await web3.eth.getBalance(inviteAddress);
315 const gasPrice = 20000000000;
316 const gasLimit = 21000;
317 const sendEthCost = gasPrice * gasLimit;
318 if (balance > sendEthCost) {
319 const value = balance - sendEthCost;
320 console.log('sending', value);
321 const tx = {
322 to: newAddress,
323 value: value,
324 gasPrice: gasPrice,
325 gas: gasLimit,
326 nonce: inviteNonce++,
327 };
328 let stx = new Tx(tx);
329 stx.sign(inviteWallet.privateKey);
330 const rawTx = '0x' + stx.serialize().toString('hex');
331 web3.eth.sendSignedTransaction(rawTx).catch(err => {
332 console.log('error sending value tx, who cares', err);
333 });
334 console.log('sent old balance');
335 }
337 updateProgress({
338 type: 'progress',
339 value: TRANSACTION_STATES.DONE,
340 });
342 // proceed without waiting for confirm
343 setUrbitWallet(Just(realWallet));
346async function sendAndAwaitConfirm(web3, signedTxs, usedTank) {
347 await Promise.all(
348 signedTxs.map(tx => {
349 return new Promise((resolve, reject) => {
350 sendSignedTransaction(web3, tx, usedTank, resolve);
351 });
352 })
353 );
356export {
357 generateWallet,
358 downloadWallet,
359 startTransactions,
360 INVITE_STAGES,
361 WALLET_STATES,
362 TRANSACTION_STATES,
363};