on 04765bb help sign in
bridge/src/lib/invite.js
336 lines 8.9 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 //
200 setUrbitWallet(Just(realWallet));
201 const newAddress = realWallet.ownership.keys.address;
203 // configure management proxy
205 let managementTx = azimuth.ecliptic.setManagementProxy(
206 contracts,
207 point,
208 realWallet.management.keys.address
209 );
210 managementTx.gas = 200000;
211 managementTx.nonce = 0;
213 // configure networking public keys
214 //TODO feels like more of this logic should live in a lib?
215 const seed = await attemptNetworkSeedDerivation(true, {
216 walletType: WALLET_TYPES.TICKET,
217 urbitWallet: Just(realWallet),
218 pointCursor: Just(point),
219 pointCache: { [point]: { keyRevisionNumber: 0 } },
220 });
222 const networkSeed = seed.matchWith({
223 Nothing: () => {
224 throw new Error('network seed not derived');
225 },
226 Just: p => p.value,
227 });
229 const networkKeys = kg.deriveNetworkKeys(networkSeed);
231 let keysTx = azimuth.ecliptic.configureKeys(
232 contracts,
233 point,
234 addHexPrefix(networkKeys.crypt.public),
235 addHexPrefix(networkKeys.auth.public),
236 1,
237 false
238 );
239 keysTx.gas = 150000;
240 keysTx.nonce = 1;
242 //TODO refactor this process into function
243 let totalCost = 0;
244 let txs = [managementTx, keysTx];
245 txs = txs.map(tx => {
246 tx.gasPrice = 20000000000;
247 tx.from = newAddress;
248 totalCost = totalCost + tx.gasPrice * tx.gas;
249 return tx;
250 });
252 let txPairs = txs.map(tx => {
253 let stx = new Tx(tx);
254 stx.sign(Buffer.from(realWallet.ownership.keys.private, 'hex'));
255 return {
256 raw: '0x' + stx.serialize().toString('hex'),
257 signed: stx,
258 };
259 });
261 updateProgress({
262 type: 'progress',
263 value: TRANSACTION_STATES.FUNDING_RECIPIENT,
264 });
265 usedTank = await tank.ensureFundsFor(
266 web3,
267 point,
268 newAddress,
269 totalCost,
270 txPairs.map(p => p.raw),
271 askForFunding
272 );
274 updateProgress({
275 type: 'progress',
276 value: TRANSACTION_STATES.CONFIGURING,
277 });
279 await sendAndAwaitConfirm(web3, txPairs.map(p => Just(p.signed)), usedTank);
281 updateProgress({
282 type: 'progress',
283 value: TRANSACTION_STATES.CLEANING,
284 });
286 // if non-trivial eth left in invite wallet, transfer to new ownership
287 let balance = await web3.eth.getBalance(inviteAddress);
288 const gasPrice = 20000000000;
289 const gasLimit = 21000;
290 const sendEthCost = gasPrice * gasLimit;
291 if (balance > sendEthCost) {
292 const value = balance - sendEthCost;
293 console.log('sending', value);
294 const tx = {
295 to: newAddress,
296 value: value,
297 gasPrice: gasPrice,
298 gas: gasLimit,
299 nonce: inviteNonce++,
300 };
301 let stx = new Tx(tx);
302 stx.sign(inviteWallet.privateKey);
303 const rawTx = '0x' + stx.serialize().toString('hex');
304 web3.eth.sendSignedTransaction(rawTx).catch(err => {
305 console.log('error sending value tx, who cares', err);
306 });
307 console.log('sent old balance');
308 }
310 updateProgress({
311 type: 'progress',
312 value: TRANSACTION_STATES.DONE,
313 });
315 // proceed without waiting for confirm
316 setUrbitWallet(Just(realWallet));
319async function sendAndAwaitConfirm(web3, signedTxs, usedTank) {
320 await Promise.all(
321 signedTxs.map(tx => {
322 return new Promise((resolve, reject) => {
323 sendSignedTransaction(web3, tx, usedTank, resolve);
324 });
325 })
326 );
329export {
330 generateWallet,
331 downloadWallet,
332 startTransactions,
333 INVITE_STAGES,
334 WALLET_STATES,
335 TRANSACTION_STATES,
336};