@@ -0,0 +1,438 @@
1+import lodash from 'lodash'
2+import { Just, Nothing } from 'folktale/maybe'
3+import Tx from 'ethereumjs-tx'
4+
5+import * as azimuth from 'azimuth-js'
6+import * as more from 'more-entropy'
7+import * as ob from 'urbit-ob'
8+import * as kg from '../../../node_modules/urbit-key-generation/dist/index'
9+import * as tank from './tank'
10+import { MIN_STAR, MIN_PLANET, SEED_ENTROPY_BITS,
11+ GALAXY_ENTROPY_BITS, STAR_ENTROPY_BITS, PLANET_ENTROPY_BITS,
12+ } from '../../walletgen/lib/constants'
13+
14+import JSZip from 'jszip'
15+import saveAs from 'file-saver'
16+
17+import {
18+ waitForTransactionConfirm,
19+ isTransactionConfirmed
20+} from './txn'
21+import { BRIDGE_ERROR } from './error'
22+import { attemptSeedDerivation } from './keys'
23+import {
24+ addressFromSecp256k1Public,
25+ addHexPrefix,
26+ WALLET_NAMES
27+} from './wallet'
28+
29+const SEED_LENGTH_BYTES = SEED_ENTROPY_BITS / 8
30+
31+const INVITE_STAGES = {
32+ INVITE_LOGIN: "invite login",
33+ INVITE_WALLET: "invite wallet",
34+ INVITE_VERIFY: "invite verify",
35+ INVITE_TRANSACTIONS: "invite transactions"
36+}
37+
38+const WALLET_STATES = {
39+ UNLOCKING: "Unlocking invite wallet",
40+ GENERATING: "Generating your wallet",
41+ RENDERING: "Creating paper collateral",
42+ PAPER_READY: "Download your wallet",
43+ DOWNLOADED: "Wallet downloaded",
44+ TRANSACTIONS: "Sending transactions"
45+}
46+
47+const TRANSACTION_STATES = {
48+ GENERATING: {
49+ label: 'Generating transactions',
50+ pct: "0%"
51+ },
52+ SIGNING: {
53+ label: 'Signing transactions',
54+ pct: "15%"
55+ },
56+ FUNDING_INVITE: {
57+ label: 'Funding invite wallet',
58+ pct: "30%"
59+ },
60+ CLAIMING: {
61+ label: 'Claiming invite',
62+ pct: "55%"
63+ },
64+ FUNDING_RECIPIENT: {
65+ label: 'Funding recipient wallet',
66+ pct: "65%"
67+ },
68+ CONFIGURING: {
69+ label: 'Configuring planet',
70+ pct: "85%"
71+ },
72+ CLEANING: {
73+ label: 'Cleaning up',
74+ pct: "95%"
75+ },
76+ DONE: {
77+ label: 'Done',
78+ pct: "100%"
79+ },
80+}
81+
82+//TODO should be moved to lib/walletgen
83+async function generateWallet(point) {
84+ const makeTicket = point => {
85+
86+ const bits = point < MIN_STAR
87+ ? GALAXY_ENTROPY_BITS
88+ : point < MIN_PLANET
89+ ? STAR_ENTROPY_BITS
90+ : PLANET_ENTROPY_BITS
91+
92+ const bytes = bits / 8
93+ const some = new Uint8Array(bytes)
94+ window.crypto.getRandomValues(some)
95+
96+ const gen = new more.Generator()
97+
98+ return new Promise((resolve, reject) => {
99+ gen.generate(bits, result => {
100+ const chunked = lodash.chunk(result, 2)
101+ const desired = chunked.slice(0, bytes) // only take required entropy
102+ const more = lodash.flatMap(desired, arr => arr[0] ^ arr[1])
103+ const entropy = lodash.zipWith(some, more, (x, y) => x ^ y)
104+ const buf = Buffer.from(entropy)
105+ const patq = ob.hex2patq(buf.toString('hex'))
106+ resolve(patq)
107+ reject('Entropy generation failed')
108+ })
109+ })
110+ }
111+
112+ const genWallet = async (point, ticket, cb) => {
113+
114+ const config = {
115+ ticket: ticket,
116+ seedSize: SEED_LENGTH_BYTES,
117+ ship: point,
118+ password: '',
119+ revisions: {},
120+ boot: false //TODO should this generate networking keys here already?
121+ };
122+
123+ const wallet = await kg.generateWallet(config);
124+
125+ // This is here to notify the anyone who opens console because the thread
126+ // hangs, blocking UI updates so this cannot be doen in the UI
127+ console.log('Generating Wallet for point address: ', point);
128+
129+ return wallet;
130+ }
131+
132+ const ticket = await makeTicket(point);
133+ const wallet = await genWallet(point, ticket);
134+ return wallet;
135+}
136+
137+//TODO should be moved to lib/walletgen
138+async function downloadWallet(paper) {
139+ return new Promise((resolve, reject) => {
140+ const zip = new JSZip();
141+ //TODO the categories here aren't explained in bridge at all...
142+
143+ const bin0 = paper.filter(item => item.bin === '0');
144+ const bin1 = paper.filter(item => item.bin === '1');
145+ const bin2 = paper.filter(item => item.bin === '2');
146+ const bin3 = paper.filter(item => item.bin === '3');
147+ const bin4 = paper.filter(item => item.bin === '4');
148+
149+ const bin0Folder = zip.folder("0. Public");
150+ const bin1Folder = zip.folder("1. Very High Friction Custody");
151+ const bin2Folder = zip.folder("2. High Friction Custody");
152+ const bin3Folder = zip.folder("3. Medium Friction Custody");
153+ const bin4Folder = zip.folder("4. Low Friction Custody");
154+
155+ bin0.forEach(item => bin0Folder.file(`${item.pageTitle}.png`, item.png))
156+ bin1.forEach(item => bin1Folder.file(`${item.pageTitle}.png`, item.png));
157+ bin2.forEach(item => bin2Folder.file(`${item.pageTitle}.png`, item.png));
158+ bin3.forEach(item => bin3Folder.file(`${item.pageTitle}.png`, item.png));
159+ bin4.forEach(item => bin4Folder.file(`${item.pageTitle}.png`, item.png));
160+
161+ zip.generateAsync({type:"blob"}).then((content) => {
162+ saveAs(content, 'urbit-wallet.zip');
163+ resolve(true)
164+ });
165+ })
166+}
167+
168+async function startTransactions(args) {
169+ let { realPointM, web3M, contractsM,
170+ inviteWalletM, realWalletM, setUrbitWallet, updateProgress } = args
171+
172+ if (Nothing.hasInstance(web3M)) {
173+ throw BRIDGE_ERROR.MISSING_WEB3;
174+ }
175+ const web3 = web3M.value;
176+
177+ if (Nothing.hasInstance(contractsM)) {
178+ throw BRIDGE_ERROR.MISSING_CONTRACTS;
179+ }
180+ const contracts = contractsM.value;
181+
182+ if (Nothing.hasInstance(inviteWalletM)) {
183+ throw BRIDGE_ERROR.MISSING_WALLET;
184+ }
185+ const inviteWallet = inviteWalletM.value;
186+
187+ if (Nothing.hasInstance(realWalletM)) {
188+ throw BRIDGE_ERROR.MISSING_WALLET;
189+ }
190+ const realWallet = realWalletM.value;
191+
192+ // const realWallet = await urbitWalletFromTicket(args.realTicket, point);
193+
194+ if (Nothing.hasInstance(realPointM)) {
195+ throw BRIDGE_ERROR.MISSING_POINT;
196+ }
197+ const point = realPointM.value;
198+
199+ const inviteAddress = addressFromSecp256k1Public(inviteWallet.publicKey);
200+ console.log('working as', inviteAddress);
201+
202+ // transfer from invite wallet to new wallet
203+
204+ let transferTx = azimuth.ecliptic.transferPoint(contracts, point, realWallet.ownership.keys.address);
205+ transferTx.gas = 500000; //TODO can maybe be lower?
206+
207+ // ping gas tank with txs if needed
208+
209+ let inviteNonce = await web3.eth.getTransactionCount(inviteAddress);
210+ transferTx.nonce = inviteNonce++;
211+ transferTx.gasPrice = 20000000000; //NOTE we pay the premium for faster ux
212+ transferTx.from = inviteAddress;
213+
214+ //NOTE using web3.eth.accounts.signTransaction is broken (1.0.0-beta51)
215+ let transferStx = new Tx(transferTx);
216+ transferStx.sign(inviteWallet.privateKey);
217+ let rawTransferStx = '0x'+transferStx.serialize().toString('hex');
218+
219+ const transferCost = transferTx.gas * transferTx.gasPrice;
220+ await ensureFundsFor(web3, point, inviteAddress, transferCost, [rawTransferStx], updateProgress, true);
221+
222+ updateProgress({
223+ type: "progress",
224+ value: TRANSACTION_STATES.CLAIMING
225+ });
226+
227+ // send transaction
228+ await sendAndAwaitConfirm(web3, [rawTransferStx]);
229+
230+ //
231+ // we're gonna be operating as the new wallet from here on out, so change
232+ // the relevant values
233+ //
234+ setUrbitWallet(Just(realWallet));
235+ const newAddress = realWallet.ownership.keys.address;
236+
237+ // configure management proxy
238+
239+ let managementTx = azimuth.ecliptic.setManagementProxy(contracts, point, realWallet.management.keys.address);
240+ managementTx.gas = 200000;
241+ managementTx.nonce = 0;
242+
243+ // configure networking public keys
244+ //TODO feels like more of this logic should live in a lib?
245+ let networkSeed = await attemptSeedDerivation(true, {
246+ walletType: WALLET_NAMES.TICKET,
247+ urbitWallet: Just(realWallet),
248+ pointCursor: Just(point),
249+ pointCache: {[point]: {keyRevisionNumber: 0}}
250+ });
251+ if (Nothing.hasInstance(networkSeed)) {
252+ throw new Error('wtf network seed not derived');
253+ }
254+ networkSeed = networkSeed.value;
255+ const networkKeys = kg.deriveNetworkKeys(networkSeed);
256+
257+ let keysTx = azimuth.ecliptic.configureKeys(
258+ contracts,
259+ point,
260+ addHexPrefix(networkKeys.crypt.public),
261+ addHexPrefix(networkKeys.auth.public),
262+ 1,
263+ false
264+ );
265+ keysTx.gas = 150000;
266+ keysTx.nonce = 1;
267+
268+ //TODO refactor this process into function
269+ let totalCost = 0;
270+ let txs = [managementTx, keysTx];
271+ txs = txs.map(tx => {
272+ tx.gasPrice = 20000000000;
273+ tx.from = newAddress;
274+ totalCost = totalCost + (tx.gasPrice * tx.gas);
275+ return tx;
276+ });
277+
278+ let rawStxs = txs.map(tx => {
279+ let stx = new Tx(tx);
280+ stx.sign(Buffer.from(realWallet.ownership.keys.private, 'hex'));
281+ return '0x'+stx.serialize().toString('hex');
282+ });
283+
284+ await ensureFundsFor(web3, point, newAddress, totalCost, rawStxs, updateProgress, false);
285+
286+ updateProgress({
287+ type: "progress",
288+ value: TRANSACTION_STATES.CONFIGURING
289+ });
290+
291+ await sendAndAwaitConfirm(web3, rawStxs);
292+
293+ updateProgress({
294+ type: "progress",
295+ value: TRANSACTION_STATES.CLEANING
296+ });
297+
298+ // if non-trivial eth left in invite wallet, transfer to new ownership
299+ let balance = await web3.eth.getBalance(inviteAddress);
300+ const gasPrice = 20000000000;
301+ const gasLimit = 21000;
302+ const sendEthCost = gasPrice * gasLimit;
303+ if (balance > sendEthCost) {
304+ const value = (balance - sendEthCost);
305+ console.log('sending', value);
306+ const tx = {
307+ to: newAddress,
308+ value: value,
309+ gasPrice: gasPrice,
310+ gas: gasLimit,
311+ nonce: inviteNonce++
312+ }
313+ let stx = new Tx(tx);
314+ stx.sign(inviteWallet.privateKey);
315+ const rawTx = '0x'+stx.serialize().toString('hex');
316+ web3.eth.sendSignedTransaction(rawTx).catch(err => {
317+ console.log('error sending value tx, who cares', err);
318+ });
319+ console.log('sent old balance');
320+ }
321+
322+ updateProgress({
323+ type: "progress",
324+ value: TRANSACTION_STATES.DONE
325+ });
326+
327+ // proceed without waiting for confirm
328+ setUrbitWallet(Just(realWallet));
329+}
330+
331+async function ensureFundsFor(web3, point, address, cost, signedTxs, updateProgress, firstTx) {
332+ let fundingState = firstTx? TRANSACTION_STATES.FUNDING_INVITE : TRANSACTION_STATES.FUNDING_RECIPIENT
333+
334+ updateProgress({
335+ type: "progress",
336+ value: fundingState
337+ });
338+
339+ let balance = await web3.eth.getBalance(address);
340+
341+ if (cost > balance) {
342+
343+ try {
344+
345+ const fundsRemaining = await tank.remainingTransactions(point);
346+ if (fundsRemaining < signedTxs.length) {
347+ throw new Error('request invalid');
348+ }
349+
350+ const res = await tank.fundTransactions(signedTxs);
351+ if (!res.success) {
352+ throw new Error('request rejected');
353+ } else {
354+ await waitForTransactionConfirm(web3, res.txHash);
355+ let newBalance = await web3.eth.getBalance(address);
356+ console.log('funds have confirmed', balance >= cost, balance, newBalance);
357+ }
358+
359+ } catch (e) {
360+
361+ console.log('funding failed', e);
362+ await waitForBalance(web3, address, cost, updateProgress);
363+
364+ }
365+
366+ } else {
367+ console.log('already have sufficient funds');
368+ }
369+}
370+
371+// resolves when address has at least minBalance
372+//
373+async function waitForBalance(web3, address, minBalance, updateProgress) {
374+ console.log('awaiting balance', address, minBalance);
375+ return new Promise((resolve, reject) => {
376+ let oldBalance = null;
377+ const checkForBalance = async () => {
378+ const balance = await web3.eth.getBalance(address);
379+ if (balance >= minBalance) {
380+ resolve();
381+ } else {
382+ if (balance !== oldBalance) {
383+ askForFunding(address, minBalance, balance, updateProgress);
384+ }
385+ setTimeout(checkForBalance, 13000);
386+ }
387+ };
388+ checkForBalance();
389+ });
390+}
391+
392+async function sendAndAwaitConfirm(web3, signedTxs) {
393+ await Promise.all(signedTxs.map(tx => {
394+ console.log('sending...');
395+ return new Promise((resolve, reject) => {
396+ web3.eth.sendSignedTransaction(tx).then(res => {
397+ console.log('sent, now waiting for confirm!', res.transactionHash);
398+ waitForTransactionConfirm(web3, res.transactionHash)
399+ .then(resolve);
400+ }).catch(async err => {
401+ // if there's an error, check if it's because the transaction was
402+ // already confirmed prior to sending.
403+ // this is really only the case in local dev environments.
404+ const txHash = web3.utils.keccak256(tx);
405+ console.log(err.message);
406+ if (err.message.slice(0,54) ===
407+ "Returned error: the tx doesn't have the correct nonce.") {
408+ console.log('nonce error, awaiting confirm', err.message.slice(55));
409+ //TODO max wait time before assume failure?
410+ let res = await waitForTransactionConfirm(web3, txHash);
411+ if (res) resolve();
412+ else reject(new Error('Unexpected tx failure'));
413+ } else {
414+ const confirmed = await isTransactionConfirmed(web3, txHash);
415+ console.log('error, but maybe confirmed:', confirmed);
416+ if (confirmed) resolve();
417+ else reject(err);
418+ }
419+ });
420+ });
421+ }));
422+}
423+
424+function askForFunding(address, amount, current, updateProgress) {
425+ updateProgress({
426+ type: "notify",
427+ value: `Please make sure ${address} has at least ${amount} wei, we'll continue once that's true. Current balance: ${current}. Waiting`
428+ });
429+}
430+
431+export {
432+ generateWallet,
433+ downloadWallet,
434+ startTransactions,
435+ INVITE_STAGES,
436+ WALLET_STATES,
437+ TRANSACTION_STATES
438+}