@@ -0,0 +1,438 @@
1+import lodash from 'lodash'
2+import { Just, Nothing } from 'folktale/maybe'
3+import Tx from 'ethereumjs-tx'
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'
14+import JSZip from 'jszip'
15+import saveAs from 'file-saver'
18+ waitForTransactionConfirm,
19+ isTransactionConfirmed
21+import { BRIDGE_ERROR } from './error'
22+import { attemptSeedDerivation } from './keys'
24+ addressFromSecp256k1Public,
29+const SEED_LENGTH_BYTES = SEED_ENTROPY_BITS / 8
31+const INVITE_STAGES = {
32+ INVITE_LOGIN: "invite login",
33+ INVITE_WALLET: "invite wallet",
34+ INVITE_VERIFY: "invite verify",
35+ INVITE_TRANSACTIONS: "invite transactions"
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"
47+const TRANSACTION_STATES = {
49+ label: 'Generating transactions',
53+ label: 'Signing transactions',
57+ label: 'Funding invite wallet',
61+ label: 'Claiming invite',
65+ label: 'Funding recipient wallet',
69+ label: 'Configuring planet',
73+ label: 'Cleaning up',
82+//TODO should be moved to lib/walletgen
83+async function generateWallet(point) {
84+ const makeTicket = point => {
86+ const bits = point < MIN_STAR
87+ ? GALAXY_ENTROPY_BITS
90+ : PLANET_ENTROPY_BITS
92+ const bytes = bits / 8
93+ const some = new Uint8Array(bytes)
94+ window.crypto.getRandomValues(some)
96+ const gen = new more.Generator()
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'))
107+ reject('Entropy generation failed')
112+ const genWallet = async (point, ticket, cb) => {
116+ seedSize: SEED_LENGTH_BYTES,
120+ boot: false //TODO should this generate networking keys here already?
123+ const wallet = await kg.generateWallet(config);
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);
132+ const ticket = await makeTicket(point);
133+ const wallet = await genWallet(point, ticket);
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...
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');
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");
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));
161+ zip.generateAsync({type:"blob"}).then((content) => {
162+ saveAs(content, 'urbit-wallet.zip');
168+async function startTransactions(args) {
169+ let { realPointM, web3M, contractsM,
170+ inviteWalletM, realWalletM, setUrbitWallet, updateProgress } = args
172+ if (Nothing.hasInstance(web3M)) {
173+ throw BRIDGE_ERROR.MISSING_WEB3;
175+ const web3 = web3M.value;
177+ if (Nothing.hasInstance(contractsM)) {
178+ throw BRIDGE_ERROR.MISSING_CONTRACTS;
180+ const contracts = contractsM.value;
182+ if (Nothing.hasInstance(inviteWalletM)) {
183+ throw BRIDGE_ERROR.MISSING_WALLET;
185+ const inviteWallet = inviteWalletM.value;
187+ if (Nothing.hasInstance(realWalletM)) {
188+ throw BRIDGE_ERROR.MISSING_WALLET;
190+ const realWallet = realWalletM.value;
192+ // const realWallet = await urbitWalletFromTicket(args.realTicket, point);
194+ if (Nothing.hasInstance(realPointM)) {
195+ throw BRIDGE_ERROR.MISSING_POINT;
197+ const point = realPointM.value;
199+ const inviteAddress = addressFromSecp256k1Public(inviteWallet.publicKey);
200+ console.log('working as', inviteAddress);
202+ // transfer from invite wallet to new wallet
204+ let transferTx = azimuth.ecliptic.transferPoint(contracts, point, realWallet.ownership.keys.address);
205+ transferTx.gas = 500000; //TODO can maybe be lower?
207+ // ping gas tank with txs if needed
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;
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');
219+ const transferCost = transferTx.gas * transferTx.gasPrice;
220+ await ensureFundsFor(web3, point, inviteAddress, transferCost, [rawTransferStx], updateProgress, true);
224+ value: TRANSACTION_STATES.CLAIMING
228+ await sendAndAwaitConfirm(web3, [rawTransferStx]);
231+ // we're gonna be operating as the new wallet from here on out, so change
232+ // the relevant values
234+ setUrbitWallet(Just(realWallet));
235+ const newAddress = realWallet.ownership.keys.address;
237+ // configure management proxy
239+ let managementTx = azimuth.ecliptic.setManagementProxy(contracts, point, realWallet.management.keys.address);
240+ managementTx.gas = 200000;
241+ managementTx.nonce = 0;
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}}
251+ if (Nothing.hasInstance(networkSeed)) {
252+ throw new Error('wtf network seed not derived');
254+ networkSeed = networkSeed.value;
255+ const networkKeys = kg.deriveNetworkKeys(networkSeed);
257+ let keysTx = azimuth.ecliptic.configureKeys(
260+ addHexPrefix(networkKeys.crypt.public),
261+ addHexPrefix(networkKeys.auth.public),
265+ keysTx.gas = 150000;
268+ //TODO refactor this process into function
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);
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');
284+ await ensureFundsFor(web3, point, newAddress, totalCost, rawStxs, updateProgress, false);
288+ value: TRANSACTION_STATES.CONFIGURING
291+ await sendAndAwaitConfirm(web3, rawStxs);
295+ value: TRANSACTION_STATES.CLEANING
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);
311+ nonce: inviteNonce++
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);
319+ console.log('sent old balance');
324+ value: TRANSACTION_STATES.DONE
327+ // proceed without waiting for confirm
328+ setUrbitWallet(Just(realWallet));
331+async function ensureFundsFor(web3, point, address, cost, signedTxs, updateProgress, firstTx) {
332+ let fundingState = firstTx? TRANSACTION_STATES.FUNDING_INVITE : TRANSACTION_STATES.FUNDING_RECIPIENT
339+ let balance = await web3.eth.getBalance(address);
341+ if (cost > balance) {
345+ const fundsRemaining = await tank.remainingTransactions(point);
346+ if (fundsRemaining < signedTxs.length) {
347+ throw new Error('request invalid');
350+ const res = await tank.fundTransactions(signedTxs);
352+ throw new Error('request rejected');
354+ await waitForTransactionConfirm(web3, res.txHash);
355+ let newBalance = await web3.eth.getBalance(address);
356+ console.log('funds have confirmed', balance >= cost, balance, newBalance);
361+ console.log('funding failed', e);
362+ await waitForBalance(web3, address, cost, updateProgress);
367+ console.log('already have sufficient funds');
371+// resolves when address has at least minBalance
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) {
382+ if (balance !== oldBalance) {
383+ askForFunding(address, minBalance, balance, updateProgress);
385+ setTimeout(checkForBalance, 13000);
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)
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);
412+ else reject(new Error('Unexpected tx failure'));
414+ const confirmed = await isTransactionConfirmed(web3, txHash);
415+ console.log('error, but maybe confirmed:', confirmed);
416+ if (confirmed) resolve();
424+function askForFunding(address, amount, current, updateProgress) {
427+ value: `Please make sure ${address} has at least ${amount} wei, we'll continue once that's true. Current balance: ${current}. Waiting`