on fbcd6b6 help sign in
bridge/src/lib/invite.js
315 lines 8.3 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';
15import { GAS_LIMITS } from './constants';
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 progress: 0.0,
37 },
38 SIGNING: {
39 label: 'Signing Transactions...',
40 progress: 0.15,
41 },
42 FUNDING: {
43 label: 'Funding Transactions...',
44 progress: 0.3,
45 },
46 CLAIMING: {
47 label: 'Claiming Invite...',
48 progress: 0.55,
49 },
50 CLEANING: {
51 label: 'Cleaning Up...',
52 progress: 0.95,
53 },
54 DONE: {
55 label: 'Done',
56 progress: 1.0,
57 },
58};
60async function generateWallet(point) {
61 const ticket = await wg.makeTicket(point);
62 const wallet = await wg.generateWallet(point, ticket);
63 return wallet;
66//TODO should be moved to lib/walletgen
67async function downloadWallet(paper) {
68 return new Promise((resolve, reject) => {
69 const zip = new JSZip();
70 //TODO the categories here aren't explained in bridge at all...
72 const bin0 = paper.filter(item => item.bin === '0');
73 const bin1 = paper.filter(item => item.bin === '1');
74 const bin2 = paper.filter(item => item.bin === '2');
75 const bin3 = paper.filter(item => item.bin === '3');
76 const bin4 = paper.filter(item => item.bin === '4');
78 const bin0Folder = zip.folder('0. Public');
79 const bin1Folder = zip.folder('1. Very High Friction Custody');
80 const bin2Folder = zip.folder('2. High Friction Custody');
81 const bin3Folder = zip.folder('3. Medium Friction Custody');
82 const bin4Folder = zip.folder('4. Low Friction Custody');
84 bin0.forEach(item => bin0Folder.file(`${item.pageTitle}.png`, item.png));
85 bin1.forEach(item => bin1Folder.file(`${item.pageTitle}.png`, item.png));
86 bin2.forEach(item => bin2Folder.file(`${item.pageTitle}.png`, item.png));
87 bin3.forEach(item => bin3Folder.file(`${item.pageTitle}.png`, item.png));
88 bin4.forEach(item => bin4Folder.file(`${item.pageTitle}.png`, item.png));
90 zip.generateAsync({ type: 'blob' }).then(content => {
91 saveAs(content, 'urbit-wallet.zip');
92 resolve(true);
93 });
94 });
97async function claimPointFromInvite({
98 inviteWallet,
99 wallet,
100 point,
101 web3,
102 contracts,
103 onUpdate,
104 transferEth,
105}) {
106 const askForFunding = (address, minBalance, balance) =>
107 onUpdate({
108 type: 'askFunding',
109 value: { address, minBalance, balance },
110 });
112 const gotFunding = () => onUpdate({ type: 'gotFunding' });
113 const progress = state => onUpdate({ type: 'progress', state });
115 const inviteAddress = inviteWallet.address;
117 //
118 // generate transactions
119 //
121 progress(TRANSACTION_STATES.GENERATING);
123 // transfer to invite wallet, so it can configure
124 //NOTE doing this in the invite wallet instead of the final wallet makes
125 // failure cases "solvable" by just telling the user to fully retry
127 //TODO no harm done if we already owned it, but should still get a bool arg
128 // for skipping this, if it isn't too big a burden for callers
129 let transferTmpTx = azimuth.ecliptic.transferPoint(
130 contracts,
131 point,
132 inviteAddress,
133 false
134 );
135 transferTmpTx.gas = GAS_LIMITS.TRANSFER;
137 // configure networking public keys
139 //TODO feels like more of this logic should live in a lib?
140 const seed = await attemptNetworkSeedDerivation(true, {
141 walletType: WALLET_TYPES.TICKET,
142 urbitWallet: Just(wallet),
143 pointCursor: Just(point),
144 pointCache: { [point]: { keyRevisionNumber: 0 } },
145 });
147 const networkSeed = seed.matchWith({
148 Nothing: () => {
149 throw new Error('network seed not derived');
150 },
151 Just: p => p.value,
152 });
154 const networkKeys = kg.deriveNetworkKeys(networkSeed);
156 let keysTx = azimuth.ecliptic.configureKeys(
157 contracts,
158 point,
159 addHexPrefix(networkKeys.crypt.public),
160 addHexPrefix(networkKeys.auth.public),
161 1,
162 false
163 );
164 keysTx.gas = GAS_LIMITS.CONFIGURE_KEYS;
166 // configure management proxy
168 let managementTx = azimuth.ecliptic.setManagementProxy(
169 contracts,
170 point,
171 wallet.management.keys.address
172 );
173 managementTx.gas = GAS_LIMITS.SET_PROXY;
175 // set spawn & voting proxies situationally
177 let txs = [transferTmpTx, keysTx, managementTx];
178 if (
179 azimuth.azimuth.getPointSize(point) !== azimuth.azimuth.PointSize.Planet
180 ) {
181 let spawnTx = azimuth.ecliptic.setSpawnProxy(
182 contracts,
183 point,
184 wallet.spawn.keys.address
185 );
186 spawnTx.gas = GAS_LIMITS.SET_PROXY;
187 txs.push(spawnTx);
189 if (
190 azimuth.azimuth.getPointSize(point) === azimuth.azimuth.PointSize.Galaxy
191 ) {
192 let votingTx = azimuth.ecliptic.setVotingProxy(
193 contracts,
194 point,
195 wallet.voting.keys.address
196 );
197 votingTx.gas = GAS_LIMITS.SET_PROXY;
198 txs.push(votingTx);
199 }
200 }
202 // transfer configured point to user's new wallet
204 let transferFinalTx = azimuth.ecliptic.transferPoint(
205 contracts,
206 point,
207 wallet.ownership.keys.address,
208 false
209 );
210 transferFinalTx.gas = GAS_LIMITS.TRANSFER;
211 txs.push(transferFinalTx);
213 //
214 // finalizing & signing transactions
215 //
217 progress(TRANSACTION_STATES.SIGNING);
219 let totalCost = 0;
220 let inviteNonce = await web3.eth.getTransactionCount(inviteAddress);
221 txs = txs.map(tx => {
222 tx.from = inviteAddress;
223 tx.nonce = inviteNonce++;
224 tx.gasPrice = 20000000000; //NOTE we pay the premium for faster ux
225 totalCost = totalCost + tx.gasPrice * tx.gas;
226 return tx;
227 });
229 let txPairs = txs.map(tx => {
230 let stx = new Tx(tx);
231 stx.sign(Buffer.from(inviteWallet.privateKey, 'hex'));
232 return {
233 raw: hexify(stx.serialize()),
234 signed: stx,
235 };
236 });
238 //
239 // ensuring funding for transactions
240 //
242 progress(TRANSACTION_STATES.FUNDING);
244 const usedTank = await tank.ensureFundsFor(
245 web3,
246 point,
247 inviteAddress,
248 totalCost,
249 txPairs.map(p => p.raw),
250 askForFunding,
251 gotFunding
252 );
253 console.log('used tank', usedTank);
255 //
256 // sending and awaiting transactions
257 //
259 progress(TRANSACTION_STATES.CLAIMING);
261 console.log('sending txpairs', txPairs.length);
262 await sendAndAwaitConfirm(web3, txPairs.map(p => Just(p.signed)), usedTank);
263 console.log('all done');
265 //
266 // move leftover eth
267 //
269 progress(TRANSACTION_STATES.CLEANING);
271 // if non-trivial eth left in invite wallet, transfer to new ownership
272 let balance = await web3.eth.getBalance(inviteAddress);
273 const gasPrice = 20000000000;
274 const gasLimit = 21000;
275 const sendEthCost = gasPrice * gasLimit;
276 if (transferEth && balance > sendEthCost) {
277 const value = balance - sendEthCost;
278 console.log('sending', value);
279 const tx = {
280 to: wallet.ownership.keys.address,
281 value: value,
282 gasPrice: gasPrice,
283 gas: gasLimit,
284 nonce: inviteNonce++,
285 };
286 let stx = new Tx(tx);
287 stx.sign(inviteWallet.privateKey);
288 const rawTx = hexify(stx.serialize());
289 web3.eth.sendSignedTransaction(rawTx).catch(err => {
290 console.log('error sending value tx, who cares', err);
291 });
292 console.log('sent old balance');
293 }
295 progress(TRANSACTION_STATES.DONE);
298async function sendAndAwaitConfirm(web3, signedTxs, usedTank) {
299 await Promise.all(
300 signedTxs.map(tx => {
301 return new Promise((resolve, reject) => {
302 sendSignedTransaction(web3, tx, usedTank, resolve);
303 });
304 })
305 );
308export {
309 generateWallet,
310 downloadWallet,
311 claimPointFromInvite,
312 INVITE_STAGES,
313 WALLET_STATES,
314 TRANSACTION_STATES,
315};