on 6be0701 help sign in
bridge/src/lib/invite.js
314 lines 8.2 kB history raw
1import { Just } 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, hexify } from './txn';
13import { attemptNetworkSeedDerivation } from './keys';
14import { addHexPrefix, WALLET_TYPES } from './wallet';
16const INVITE_STAGES = {
17 INVITE_LOGIN: 'invite login',
18 INVITE_WALLET: 'invite wallet',
19 INVITE_VERIFY: 'invite verify',
20 INVITE_TRANSACTIONS: 'invite transactions',
21};
23const WALLET_STATES = {
24 UNLOCKING: 'Unlocking invite wallet',
25 GENERATING: 'Generating your wallet',
26 RENDERING: 'Creating paper collateral',
27 PAPER_READY: 'Download your wallet',
28 DOWNLOADED: 'Wallet downloaded',
29 TRANSACTIONS: 'Sending transactions',
30};
32const TRANSACTION_STATES = {
33 GENERATING: {
34 label: 'Generating Transactions...',
35 progress: 0.0,
36 },
37 SIGNING: {
38 label: 'Signing Transactions...',
39 progress: 0.15,
40 },
41 FUNDING: {
42 label: 'Funding Transactions...',
43 progress: 0.3,
44 },
45 CLAIMING: {
46 label: 'Claiming Invite...',
47 progress: 0.55,
48 },
49 CLEANING: {
50 label: 'Cleaning Up...',
51 progress: 0.95,
52 },
53 DONE: {
54 label: 'Done',
55 progress: 1.0,
56 },
57};
59async function generateWallet(point) {
60 const ticket = await wg.makeTicket(point);
61 const wallet = await wg.generateWallet(point, ticket);
62 return wallet;
65//TODO should be moved to lib/walletgen
66async function downloadWallet(paper) {
67 return new Promise((resolve, reject) => {
68 const zip = new JSZip();
69 //TODO the categories here aren't explained in bridge at all...
71 const bin0 = paper.filter(item => item.bin === '0');
72 const bin1 = paper.filter(item => item.bin === '1');
73 const bin2 = paper.filter(item => item.bin === '2');
74 const bin3 = paper.filter(item => item.bin === '3');
75 const bin4 = paper.filter(item => item.bin === '4');
77 const bin0Folder = zip.folder('0. Public');
78 const bin1Folder = zip.folder('1. Very High Friction Custody');
79 const bin2Folder = zip.folder('2. High Friction Custody');
80 const bin3Folder = zip.folder('3. Medium Friction Custody');
81 const bin4Folder = zip.folder('4. Low Friction Custody');
83 bin0.forEach(item => bin0Folder.file(`${item.pageTitle}.png`, item.png));
84 bin1.forEach(item => bin1Folder.file(`${item.pageTitle}.png`, item.png));
85 bin2.forEach(item => bin2Folder.file(`${item.pageTitle}.png`, item.png));
86 bin3.forEach(item => bin3Folder.file(`${item.pageTitle}.png`, item.png));
87 bin4.forEach(item => bin4Folder.file(`${item.pageTitle}.png`, item.png));
89 zip.generateAsync({ type: 'blob' }).then(content => {
90 saveAs(content, 'urbit-wallet.zip');
91 resolve(true);
92 });
93 });
96async function claimPointFromInvite({
97 inviteWallet,
98 wallet,
99 point,
100 web3,
101 contracts,
102 onUpdate,
103 transferEth,
104}) {
105 const askForFunding = (address, minBalance, balance) =>
106 onUpdate({
107 type: 'askFunding',
108 value: { address, minBalance, balance },
109 });
111 const gotFunding = () => onUpdate({ type: 'gotFunding' });
112 const progress = state => onUpdate({ type: 'progress', state });
114 const inviteAddress = inviteWallet.address;
116 //
117 // generate transactions
118 //
120 progress(TRANSACTION_STATES.GENERATING);
122 // transfer to invite wallet, so it can configure
123 //NOTE doing this in the invite wallet instead of the final wallet makes
124 // failure cases "solvable" by just telling the user to fully retry
126 //TODO no harm done if we already owned it, but should still get a bool arg
127 // for skipping this, if it isn't too big a burden for callers
128 let transferTmpTx = azimuth.ecliptic.transferPoint(
129 contracts,
130 point,
131 inviteAddress,
132 false
133 );
134 transferTmpTx.gas = 500000; //TODO can maybe be lower?
136 // configure networking public keys
138 //TODO feels like more of this logic should live in a lib?
139 const seed = await attemptNetworkSeedDerivation(true, {
140 walletType: WALLET_TYPES.TICKET,
141 urbitWallet: Just(wallet),
142 pointCursor: Just(point),
143 pointCache: { [point]: { keyRevisionNumber: 0 } },
144 });
146 const networkSeed = seed.matchWith({
147 Nothing: () => {
148 throw new Error('network seed not derived');
149 },
150 Just: p => p.value,
151 });
153 const networkKeys = kg.deriveNetworkKeys(networkSeed);
155 let keysTx = azimuth.ecliptic.configureKeys(
156 contracts,
157 point,
158 addHexPrefix(networkKeys.crypt.public),
159 addHexPrefix(networkKeys.auth.public),
160 1,
161 false
162 );
163 keysTx.gas = 150000;
165 // configure management proxy
167 let managementTx = azimuth.ecliptic.setManagementProxy(
168 contracts,
169 point,
170 wallet.management.keys.address
171 );
172 managementTx.gas = 200000;
174 // set spawn & voting proxies situationally
176 let txs = [transferTmpTx, keysTx, managementTx];
177 if (
178 azimuth.azimuth.getPointSize(point) !== azimuth.azimuth.PointSize.Planet
179 ) {
180 let spawnTx = azimuth.ecliptic.setSpawnProxy(
181 contracts,
182 point,
183 wallet.spawn.keys.address
184 );
185 spawnTx.gas = 200000;
186 txs.push(spawnTx);
188 if (
189 azimuth.azimuth.getPointSize(point) === azimuth.azimuth.PointSize.Galaxy
190 ) {
191 let votingTx = azimuth.ecliptic.setVotingProxy(
192 contracts,
193 point,
194 wallet.voting.keys.address
195 );
196 votingTx.gas = 200000;
197 txs.push(votingTx);
198 }
199 }
201 // transfer configured point to user's new wallet
203 let transferFinalTx = azimuth.ecliptic.transferPoint(
204 contracts,
205 point,
206 wallet.ownership.keys.address,
207 false
208 );
209 transferFinalTx.gas = 500000; //TODO can maybe be lower?
210 txs.push(transferFinalTx);
212 //
213 // finalizing & signing transactions
214 //
216 progress(TRANSACTION_STATES.SIGNING);
218 let totalCost = 0;
219 let inviteNonce = await web3.eth.getTransactionCount(inviteAddress);
220 txs = txs.map(tx => {
221 tx.from = inviteAddress;
222 tx.nonce = inviteNonce++;
223 tx.gasPrice = 20000000000; //NOTE we pay the premium for faster ux
224 totalCost = totalCost + tx.gasPrice * tx.gas;
225 return tx;
226 });
228 let txPairs = txs.map(tx => {
229 let stx = new Tx(tx);
230 stx.sign(Buffer.from(inviteWallet.privateKey, 'hex'));
231 return {
232 raw: '0x' + stx.serialize().toString('hex'),
233 signed: stx,
234 };
235 });
237 //
238 // ensuring funding for transactions
239 //
241 progress(TRANSACTION_STATES.FUNDING);
243 const usedTank = await tank.ensureFundsFor(
244 web3,
245 point,
246 inviteAddress,
247 totalCost,
248 txPairs.map(p => p.raw),
249 askForFunding,
250 gotFunding
251 );
252 console.log('used tank', usedTank);
254 //
255 // sending and awaiting transactions
256 //
258 progress(TRANSACTION_STATES.CLAIMING);
260 console.log('sending txpairs', txPairs.length);
261 await sendAndAwaitConfirm(web3, txPairs.map(p => Just(p.signed)), usedTank);
262 console.log('all done');
264 //
265 // move leftover eth
266 //
268 progress(TRANSACTION_STATES.CLEANING);
270 // if non-trivial eth left in invite wallet, transfer to new ownership
271 let balance = await web3.eth.getBalance(inviteAddress);
272 const gasPrice = 20000000000;
273 const gasLimit = 21000;
274 const sendEthCost = gasPrice * gasLimit;
275 if (transferEth && balance > sendEthCost) {
276 const value = balance - sendEthCost;
277 console.log('sending', value);
278 const tx = {
279 to: wallet.ownership.keys.address,
280 value: value,
281 gasPrice: gasPrice,
282 gas: gasLimit,
283 nonce: inviteNonce++,
284 };
285 let stx = new Tx(tx);
286 stx.sign(inviteWallet.privateKey);
287 const rawTx = hexify(stx.serialize());
288 web3.eth.sendSignedTransaction(rawTx).catch(err => {
289 console.log('error sending value tx, who cares', err);
290 });
291 console.log('sent old balance');
292 }
294 progress(TRANSACTION_STATES.DONE);
297async function sendAndAwaitConfirm(web3, signedTxs, usedTank) {
298 await Promise.all(
299 signedTxs.map(tx => {
300 return new Promise((resolve, reject) => {
301 sendSignedTransaction(web3, tx, usedTank, resolve);
302 });
303 })
304 );
307export {
308 generateWallet,
309 downloadWallet,
310 claimPointFromInvite,
311 INVITE_STAGES,
312 WALLET_STATES,
313 TRANSACTION_STATES,
314};