@@ -1,7 +1,12 @@
11import lodash from 'lodash'
2+import { Just, Nothing } from 'folktale/maybe'
3+import Tx from 'ethereumjs-tx'
4+
5+import * as azimuth from 'azimuth-js'
26import * as more from 'more-entropy'
37import * as ob from 'urbit-ob'
48import * as kg from '../../../node_modules/urbit-key-generation/dist/index'
9+import * as tank from './tank'
510import { MAX_GALAXY, MIN_STAR, MAX_STAR, MIN_PLANET,
611 GALAXY_ENTROPY_BITS, STAR_ENTROPY_BITS, PLANET_ENTROPY_BITS,
712 SEED_ENTROPY_BITS,
@@ -11,6 +16,22 @@ import { MAX_GALAXY, MIN_STAR, MAX_STAR, MIN_PLANET,
1116import JSZip from 'jszip'
1217import saveAs from 'file-saver'
1318
19+import {
20+ signTransaction,
21+ sendSignedTransaction,
22+ waitForTransactionConfirm,
23+ isTransactionConfirmed
24+} from './txn'
25+import { BRIDGE_ERROR } from './error'
26+import { attemptSeedDerivation, genKey } from './keys'
27+import {
28+ EthereumWallet,
29+ addressFromSecp256k1Public,
30+ urbitWalletFromTicket,
31+ addHexPrefix,
32+ WALLET_NAMES
33+} from './wallet'
34+
1435const NEXT_STEP_NUM = 6;
1536const SEED_LENGTH_BYTES = SEED_ENTROPY_BITS / 8
1637
@@ -30,6 +51,16 @@ const WALLET_STATES = {
3051 TRANSACTIONS: "Sending transactions"
3152}
3253
54+const TRANSACTION_STATES = {
55+ GENERATING: 'Generating transactions',
56+ SIGNING: 'Signing transactions',
57+ FUNDING: 'Funding transactions',
58+ CLAIMING: 'Claiming invite',
59+ CONFIGURING: 'Configuring planet',
60+ CLEANING: 'Cleaning up',
61+ DONE: 'Done'
62+}
63+
3364async function generateWallet(point) {
3465 const makeTicket = point => {
3566
@@ -115,9 +146,267 @@ async function downloadWallet(paper) {
115146 })
116147}
117148
149+async function startTransactions(args) {
150+ let { realPointM, web3M, contractsM, realTicket,
151+ inviteWalletM, realWalletM, setUrbitWallet, updateProgress } = args
152+
153+ if (Nothing.hasInstance(web3M)) {
154+ throw BRIDGE_ERROR.MISSING_WEB3;
155+ }
156+ const web3 = web3M.value;
157+
158+ if (Nothing.hasInstance(contractsM)) {
159+ throw BRIDGE_ERROR.MISSING_CONTRACTS;
160+ }
161+ const contracts = contractsM.value;
162+
163+ if (Nothing.hasInstance(inviteWalletM)) {
164+ throw BRIDGE_ERROR.MISSING_WALLET;
165+ }
166+ const inviteWallet = inviteWalletM.value;
167+
168+ // if (Nothing.hasInstance(realWalletM)) {
169+ // throw BRIDGE_ERROR.MISSING_WALLET;
170+ // }
171+ // const realWallet = realWalletM.value;
172+
173+ if (Nothing.hasInstance(realPointM)) {
174+ throw BRIDGE_ERROR.MISSING_POINT;
175+ }
176+ const point = realPointM.value;
177+
178+ const inviteAddress = addressFromSecp256k1Public(inviteWallet.publicKey);
179+ console.log('working as', inviteAddress);
180+
181+ console.log('realTicket = ', realTicket)
182+ console.log('realPoint = ', point)
183+ console.log('inviteWallet = ', inviteWallet)
184+ debugger
185+
186+ const realWallet = await urbitWalletFromTicket(realTicket, point);
187+
188+ // transfer from invite wallet to new wallet
189+
190+ let transferTx = azimuth.ecliptic.transferPoint(contracts, point, realWallet.ownership.keys.address);
191+ transferTx.gas = 500000; //TODO can maybe be lower?
192+
193+ // ping gas tank with txs if needed
194+
195+ let inviteNonce = await web3.eth.getTransactionCount(inviteAddress);
196+ transferTx.nonce = inviteNonce++;
197+ transferTx.gasPrice = 20000000000; //NOTE we pay the premium for faster ux
198+ transferTx.from = inviteAddress;
199+
200+ //NOTE using web3.eth.accounts.signTransaction is broken (1.0.0-beta51)
201+ let transferStx = new Tx(transferTx);
202+ transferStx.sign(inviteWallet.privateKey);
203+ let rawTransferStx = '0x'+transferStx.serialize().toString('hex');
204+
205+ const transferCost = transferTx.gas * transferTx.gasPrice;
206+ await ensureFundsFor(web3, point, inviteAddress, transferCost, [rawTransferStx], updateProgress);
207+
208+ // send transaction
209+ updateProgress(TRANSACTION_STATES.CLAIMING);
210+ await sendAndAwaitConfirm(web3, [rawTransferStx]);
211+
212+ //
213+ // we're gonna be operating as the new wallet from here on out, so change
214+ // the relevant values
215+ //
216+ setUrbitWallet(Just(realWallet));
217+ const newAddress = realWallet.ownership.keys.address;
218+
219+ // configure management proxy
220+
221+ let managementTx = azimuth.ecliptic.setManagementProxy(contracts, point, realWallet.management.keys.address);
222+ managementTx.gas = 200000;
223+ managementTx.nonce = 0;
224+
225+ // configure networking public keys
226+ //TODO feels like more of this logic should live in a lib?
227+ let networkSeed = await attemptSeedDerivation(true, {
228+ walletType: WALLET_NAMES.TICKET,
229+ urbitWallet: Just(realWallet),
230+ pointCursor: Just(point),
231+ pointCache: {[point]: {keyRevisionNumber: 0}}
232+ });
233+ if (Nothing.hasInstance(networkSeed)) {
234+ throw new Error('wtf network seed not derived');
235+ }
236+ networkSeed = networkSeed.value;
237+ const networkKeys = kg.deriveNetworkKeys(networkSeed);
238+
239+ let keysTx = azimuth.ecliptic.configureKeys(
240+ contracts,
241+ point,
242+ addHexPrefix(networkKeys.crypt.public),
243+ addHexPrefix(networkKeys.auth.public),
244+ 1,
245+ false
246+ );
247+ keysTx.gas = 150000;
248+ keysTx.nonce = 1;
249+
250+ //TODO refactor this process into function
251+ let totalCost = 0;
252+ let txs = [managementTx, keysTx];
253+ txs = txs.map(tx => {
254+ tx.gasPrice = 20000000000;
255+ tx.from = newAddress;
256+ totalCost = totalCost + (tx.gasPrice * tx.gas);
257+ return tx;
258+ });
259+
260+ let rawStxs = txs.map(tx => {
261+ let stx = new Tx(tx);
262+ stx.sign(Buffer.from(realWallet.ownership.keys.private, 'hex'));
263+ return '0x'+stx.serialize().toString('hex');
264+ });
265+
266+ await ensureFundsFor(web3, point, newAddress, totalCost, rawStxs, updateProgress);
267+
268+ updateProgress(TRANSACTION_STATES.CONFIGURING);
269+ await sendAndAwaitConfirm(web3, rawStxs);
270+
271+ // if non-trivial eth left in invite wallet, transfer to new ownership
272+ updateProgress(TRANSACTION_STATES.CLEANING);
273+ let balance = await web3.eth.getBalance(inviteAddress);
274+ const gasPrice = 20000000000;
275+ const gasLimit = 21000;
276+ const sendEthCost = gasPrice * gasLimit;
277+ if (balance > sendEthCost) {
278+ const value = (balance - sendEthCost);
279+ console.log('sending', value);
280+ const tx = {
281+ to: newAddress,
282+ value: value,
283+ gasPrice: gasPrice,
284+ gas: gasLimit,
285+ nonce: inviteNonce++
286+ }
287+ let stx = new Tx(tx);
288+ stx.sign(inviteWallet.privateKey);
289+ const rawTx = '0x'+stx.serialize().toString('hex');
290+ web3.eth.sendSignedTransaction(rawTx).catch(err => {
291+ console.log('error sending value tx, who cares', err);
292+ });
293+ console.log('sent old balance');
294+ }
295+
296+ // proceed without waiting for confirm
297+ updateProgress(TRANSACTION_STATES.DONE);
298+ setUrbitWallet(Just(realWallet));
299+ //TODO forward to "all done!" screen
300+}
301+
302+async function ensureFundsFor(web3, point, address, cost, signedTxs, updateProgress) {
303+ updateProgress(TRANSACTION_STATES.FUNDING);
304+ let balance = await web3.eth.getBalance(address);
305+
306+ if (cost > balance) {
307+
308+ try {
309+
310+ const fundsRemaining = await tank.remainingTransactions(point);
311+ if (fundsRemaining < signedTxs.length) {
312+ throw new Error('request invalid');
313+ }
314+
315+ const res = await tank.fundTransactions(signedTxs);
316+ if (!res.success) {
317+ throw new Error('request rejected');
318+ } else {
319+ await waitForTransactionConfirm(web3, res.txHash);
320+ let newBalance = await web3.eth.getBalance(address);
321+ console.log('funds have confirmed', balance >= cost, balance, newBalance);
322+ }
323+
324+ } catch (e) {
325+
326+ console.log('funding failed', e);
327+ await waitForBalance(web3, address, cost, updateProgress);
328+
329+ }
330+
331+ } else {
332+ console.log('already have sufficient funds');
333+ }
334+}
335+
336+// resolves when address has at least minBalance
337+//
338+async function waitForBalance(web3, address, minBalance, updateProgress) {
339+ console.log('awaiting balance', address, minBalance);
340+ return new Promise((resolve, reject) => {
341+ let oldBalance = null;
342+ const checkForBalance = async () => {
343+ const balance = await web3.eth.getBalance(address);
344+ if (balance >= minBalance) {
345+ resolve();
346+ } else {
347+ if (balance !== oldBalance) {
348+ askForFunding(address, minBalance, balance, updateProgress);
349+ }
350+ setTimeout(checkForBalance, 13000);
351+ }
352+ };
353+ checkForBalance();
354+ });
355+}
356+
357+async function sendAndAwaitConfirm(web3, signedTxs) {
358+ await Promise.all(signedTxs.map(tx => {
359+ console.log('sending...');
360+ return new Promise((resolve, reject) => {
361+ web3.eth.sendSignedTransaction(tx).then(res => {
362+ console.log('sent, now waiting for confirm!', res.transactionHash);
363+ waitForTransactionConfirm(web3, res.transactionHash)
364+ .then(resolve);
365+ }).catch(async err => {
366+ // if there's an error, check if it's because the transaction was
367+ // already confirmed prior to sending.
368+ // this is really only the case in local dev environments.
369+ const txHash = web3.utils.keccak256(tx);
370+ console.log(err.message);
371+ if (err.message.slice(0,54) ===
372+ "Returned error: the tx doesn't have the correct nonce.") {
373+ console.log('nonce error, awaiting confirm', err.message.slice(55));
374+ //TODO max wait time before assume failure?
375+ let res = await waitForTransactionConfirm(web3, txHash);
376+ if (res) resolve();
377+ else reject(new Error('Unexpected tx failure'));
378+ } else {
379+ const confirmed = await isTransactionConfirmed(web3, txHash);
380+ console.log('error, but maybe confirmed:', confirmed);
381+ if (confirmed) resolve();
382+ else reject(err);
383+ }
384+ });
385+ });
386+ }));
387+}
388+
389+function awaitForResult(desired, retryTime, func) {
390+ return new Promise((resolve, reject) => {
391+ const retry = async () => {
392+ const result = await func();
393+ console.log('result vs desired', result, desired);
394+ if (result === desired) resolve();
395+ else setTimeout(retry, retryTime);
396+ };
397+ retry();
398+ });
399+}
400+
401+function askForFunding(address, amount, current, updateProgress) {
402+ updateProgress(`Please make sure ${address} has at least ${amount} wei, we'll continue once that's true. Current balance: ${current}. Waiting`);
403+}
404+
118405export {
119406 generateWallet,
120407 downloadWallet,
408+ startTransactions,
121409 INVITE_STAGES,
122− WALLET_STATES
410+ WALLET_STATES,
411+ TRANSACTION_STATES
123412}