on master help sign in
- [WIP] move transaction logic to lib/invite, almost complete flow
commitaf5c226ed9cabb4c16b99cbe6ed9d630e886c72c authorChris Johnson <chris@cjohnson.io> dateMon 29 Apr 2019, 08:39 (8 years ago) parent32a3c99
4 files changed+449−282
package-lock.json+82−29 src/bridge/lib/invite.js+290−1 src/bridge/views/InviteTicket.js+77−13 src/bridge/views/InviteTransactions.js−239
package-lock.json+82−29
@@ -1,6 +1,6 @@
11{
22 "name": "bridge",
3− "version": "1.2.1",
3+ "version": "1.3.1",
44 "lockfileVersion": 1,
55 "requires": true,
66 "dependencies": {
@@ -1058,6 +1058,19 @@
10581058 "text-encoding": "^0.6.1"
10591059 }
10601060 },
1061+ "@types/bn.js": {
1062+ "version": "4.11.5",
1063+ "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.5.tgz",
1064+ "integrity": "sha512-AEAZcIZga0JgVMHNtl1CprA/hXX7/wPt79AgR4XqaDt7jyj3QWYw6LPoOiznPtugDmlubUnAahMs2PFxGcQrng==",
1065+ "requires": {
1066+ "@types/node": "*"
1067+ }
1068+ },
1069+ "@types/node": {
1070+ "version": "10.14.5",
1071+ "resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.5.tgz",
1072+ "integrity": "sha512-Ja7d4s0qyGFxjGeDq5S7Si25OFibSAHUi6i17UWnwNnpitADN7hah9q0Tl25gxuV5R1u2Bx+np6w4LHXfHyj/g=="
1073+ },
10611074 "@types/q": {
10621075 "version": "1.5.1",
10631076 "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.1.tgz",
@@ -1981,19 +1994,18 @@
19811994 }
19821995 },
19831996 "azimuth-js": {
1984− "version": "0.10.0",
1985− "resolved": "https://registry.npmjs.org/azimuth-js/-/azimuth-js-0.10.0.tgz",
1986− "integrity": "sha512-VDg60BYSpTjsv1K/DX7VY5F0HbxHV0X1cSoG3c1iZYH5Lxy8TErYq4CJ+D6er0xtI7JI3hmKOZS+p+ru42sT3A==",
1997+ "version": "0.13.2",
1998+ "resolved": "https://registry.npmjs.org/azimuth-js/-/azimuth-js-0.13.2.tgz",
1999+ "integrity": "sha512-DiteSHZTqst5Ikf5yoOetnU7MIE/El5JIuO9FjyaQHWPYA8ECB4XUaZJCl4VfScnAzzP1YCnORXz/mtVn2D9ng==",
19872000 "requires": {
1988− "azimuth-solidity": "1.0.2",
1989− "ethereumjs-util": "^5.2.0",
1990− "web3": "1.0.0-beta.33"
2001+ "azimuth-solidity": "1.1.0",
2002+ "ethereumjs-util": "^5.2.0"
19912003 }
19922004 },
19932005 "azimuth-solidity": {
1994− "version": "1.0.2",
1995− "resolved": "https://registry.npmjs.org/azimuth-solidity/-/azimuth-solidity-1.0.2.tgz",
1996− "integrity": "sha512-Yj+iAz/o7ladrRmjlWnnn7zqq5aVTQ1iKKprUBcbhxWnTfSfKvHwcF99Nyx6jNN7UjdiQ+gWt+4lJTlg1iewPg==",
2006+ "version": "1.1.0",
2007+ "resolved": "https://registry.npmjs.org/azimuth-solidity/-/azimuth-solidity-1.1.0.tgz",
2008+ "integrity": "sha512-YhVWKql1ZZ9Zae96nYA7QLwQlTzGspkNOErq170sl/UJwncQbvAyibSv+heMaS18n4soPDiRSpa1vHFoGoIX1A==",
19972009 "requires": {
19982010 "babel-polyfill": "^6.26.0",
19992011 "babel-register": "^6.26.0",
@@ -18775,14 +18787,6 @@
1877518787 "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
1877618788 "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
1877718789 },
18778− "typedarray-to-buffer": {
18779− "version": "3.1.5",
18780− "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
18781− "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
18782− "requires": {
18783− "is-typedarray": "^1.0.0"
18784− }
18785− },
1878618790 "typeforce": {
1878718791 "version": "1.18.0",
1878818792 "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz",
@@ -19591,18 +19595,32 @@
1959119595 "integrity": "sha1-j93qQuGbvyUh7IeVRkV6Yjqdye8=",
1959219596 "requires": {
1959319597 "underscore": "1.8.3",
19594− "web3-core-helpers": "1.0.0-beta.33",
19595− "websocket": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2"
19598+ "web3-core-helpers": "1.0.0-beta.33"
1959619599 },
1959719600 "dependencies": {
1959819601 "websocket": {
1959919602 "version": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2",
19600− "from": "git://github.com/frozeman/WebSocket-Node.git#browserifyCompatible",
19603+ "from": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2",
1960119604 "requires": {
1960219605 "debug": "^2.2.0",
1960319606 "nan": "^2.3.3",
1960419607 "typedarray-to-buffer": "^3.1.2",
1960519608 "yaeti": "^0.0.6"
19609+ },
19610+ "dependencies": {
19611+ "typedarray-to-buffer": {
19612+ "version": "3.1.5",
19613+ "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
19614+ "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
19615+ "requires": {
19616+ "is-typedarray": "^1.0.0"
19617+ }
19618+ },
19619+ "yaeti": {
19620+ "version": "0.0.6",
19621+ "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
19622+ "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc="
19623+ }
1960619624 }
1960719625 }
1960819626 }
@@ -19637,10 +19655,50 @@
1963719655 "from": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2",
1963819656 "requires": {
1963919657 "debug": "^2.2.0",
19640− "nan": "^2.3.3",
19641− "typedarray-to-buffer": "^3.1.2",
19642− "yaeti": "^0.0.6"
19658+ "nan": "^2.3.3"
19659+ }
19660+ }
19661+ }
19662+ },
19663+ "web3-utils": {
19664+ "version": "1.0.0-beta.52",
19665+ "resolved": "https://registry.npmjs.org/web3-utils/-/web3-utils-1.0.0-beta.52.tgz",
19666+ "integrity": "sha512-WdHyzPcZu/sOnNrkcOZT20QEX9FhwD9OJJXENojQNvMK2a1xo3n8JWBcC2gzAGwsa0Aah6z2B3Xwa1P//8FaoA==",
19667+ "requires": {
19668+ "@babel/runtime": "^7.3.1",
19669+ "@types/bn.js": "^4.11.4",
19670+ "@types/node": "^10.12.18",
19671+ "bn.js": "4.11.8",
19672+ "eth-lib": "0.2.8",
19673+ "ethjs-unit": "^0.1.6",
19674+ "lodash": "^4.17.11",
19675+ "number-to-bn": "1.7.0",
19676+ "randomhex": "0.1.5",
19677+ "utf8": "2.1.1"
19678+ },
19679+ "dependencies": {
19680+ "@babel/runtime": {
19681+ "version": "7.4.3",
19682+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.3.tgz",
19683+ "integrity": "sha512-9lsJwJLxDh/T3Q3SZszfWOTkk3pHbkmH+3KY+zwIDmsNlxsumuhS2TH3NIpktU4kNvfzy+k3eLT7aTJSPTo0OA==",
19684+ "requires": {
19685+ "regenerator-runtime": "^0.13.2"
1964319686 }
19687+ },
19688+ "eth-lib": {
19689+ "version": "0.2.8",
19690+ "resolved": "https://registry.npmjs.org/eth-lib/-/eth-lib-0.2.8.tgz",
19691+ "integrity": "sha512-ArJ7x1WcWOlSpzdoTBX8vkwlkSQ85CjjifSZtV4co64vWxSV8geWfPI9x4SVYu3DSxnX4yWFVTtGL+j9DUFLNw==",
19692+ "requires": {
19693+ "bn.js": "^4.11.6",
19694+ "elliptic": "^6.4.0",
19695+ "xhr-request-promise": "^0.1.2"
19696+ }
19697+ },
19698+ "regenerator-runtime": {
19699+ "version": "0.13.2",
19700+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz",
19701+ "integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA=="
1964419702 }
1964519703 }
1964619704 },
@@ -20705,11 +20763,6 @@
2070520763 "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
2070620764 "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE="
2070720765 },
20708− "yaeti": {
20709− "version": "0.0.6",
20710− "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
20711− "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc="
20712− },
2071320766 "yallist": {
2071420767 "version": "2.1.2",
2071520768 "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
src/bridge/lib/invite.js+290−1
@@ -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}
src/bridge/views/InviteTicket.js+77−13
@@ -1,6 +1,6 @@
11import { Just, Nothing } from 'folktale/maybe'
22import React from 'react'
3−import { InnerLabel, ValidatedSigil, PointInput,
3+import { InnerLabel, ValidatedSigil, PointInput, Warning,
44 TicketInput, Button, Row, Col, H1, P, Passport } from '../components/Base'
55import * as kg from '../../../node_modules/urbit-key-generation/dist/index'
66import * as ob from 'urbit-ob'
@@ -10,8 +10,8 @@ import { ROUTE_NAMES } from '../lib/router'
1010import { DEFAULT_HD_PATH, urbitWalletFromTicket,
1111 walletFromMnemonic, addressFromSecp256k1Public } from '../lib/wallet'
1212import { BRIDGE_ERROR } from '../lib/error'
13−import { INVITE_STAGES, WALLET_STATES } from '../lib/invite'
14−import { generateWallet } from '../lib/invite'
13+import { INVITE_STAGES, WALLET_STATES, TRANSACTION_STATES } from '../lib/invite'
14+import { generateWallet, startTransactions } from '../lib/invite'
1515
1616const placeholder = (len) => {
1717 let bytes = window.crypto.getRandomValues(new Uint8Array(len))
@@ -29,12 +29,16 @@ class InviteTicket extends React.Component {
2929
3030 this.state = {
3131 inviteTicket: '',
32+ inviteWallet: Nothing(),
3233 verifyTicket: '',
34+ realTicket: '',
3335 realPoint: Nothing(),
3436 realWallet: Nothing(),
3537 walletReady: false,
38+ walletStates: [],
39+ errors: [],
3640 stage: INVITE_STAGES.INVITE_LOGIN,
37− walletStates: []
41+ transactionProgress: TRANSACTION_STATES.GENERATING
3842 }
3943
4044 this.pointPlaceholder = placeholder(4)
@@ -42,6 +46,7 @@ class InviteTicket extends React.Component {
4246
4347 this.handleInviteTicketInput = this.handleInviteTicketInput.bind(this)
4448 this.handleVerifyTicketInput = this.handleVerifyTicketInput.bind(this)
49+ this.updateProgress = this.updateProgress.bind(this)
4550 this.pushWalletState = this.pushWalletState.bind(this)
4651 this.navigateLogin = this.navigateLogin.bind(this)
4752 }
@@ -65,6 +70,10 @@ class InviteTicket extends React.Component {
6570 this.setState({ verifyTicket })
6671 }
6772
73+ updateTransactionProgress(transactionProgress) {
74+ this.setState({ transactionProgress })
75+ }
76+
6877 async unlockInviteWallet(inviteTicket) {
6978 this.setState({
7079 walletStates: this.state.walletStates.concat(WALLET_STATES.UNLOCKING),
@@ -97,22 +106,29 @@ class InviteTicket extends React.Component {
97106 walletStates: this.state.walletStates.concat(WALLET_STATES.GENERATING)
98107 })
99108
100− let point
101− let wallet
109+ let realPoint = Nothing()
110+ let realWallet = Nothing()
111+ let errors = []
102112
103113 if (incoming.length > 0) {
104114 let pointNum = parseInt(incoming[0]);
105− point = Just(pointNum);
106− wallet = await generateWallet(pointNum);
115+ realPoint = Just(pointNum);
116+ let genWallet = await generateWallet(pointNum);
117+
118+ realWallet = Just(genWallet)
107119 if (incoming.length > 1) {
108120 //TODO notify user "...and others / ticket reusable"
109121 }
122+ } else {
123+ errors = ["Invite code has no claimable ship. Check your invite code and try again."]
110124 }
111125
112126 this.setState({
113− realPoint: point,
114− realWallet: Just(wallet),
115− walletStates: this.state.walletStates.concat(WALLET_STATES.RENDERING)
127+ realPoint,
128+ realWallet,
129+ inviteWallet: Just(inviteWallet),
130+ walletStates: this.state.walletStates.concat(WALLET_STATES.RENDERING),
131+ errors
116132 })
117133 }
118134
@@ -179,7 +195,7 @@ class InviteTicket extends React.Component {
179195 <div className="passport-progress-filled"></div>
180196 </div>
181197 <div className="flex justify-between">
182− <div className="text-mono text-sm lh-6 green-dark">Executing transactions</div>
198+ <div className="text-mono text-sm lh-6 green-dark">{this.state.transactionProgress}</div>
183199 <div className="text-mono text-sm lh-6">3 minutes</div>
184200 </div>
185201 </div>
@@ -188,9 +204,18 @@ class InviteTicket extends React.Component {
188204 }
189205 }
190206
207+ updateProgress(transactionProgress) {
208+ this.setState({
209+ transactionProgress
210+ })
211+ }
212+
191213 getContinueButton() {
192214 if (this.state.stage === INVITE_STAGES.INVITE_TRANSACTIONS) return null
193215
216+ const { realPoint, realWallet, inviteWallet, verifyTicket, inviteTicket } = this.state
217+ const { web3, contracts, setUrbitWallet } = this.props
218+
194219 let clickHandler
195220 let btnColor = this.state.walletStates.includes(WALLET_STATES.PAPER_READY)
196221 ? 'green'
@@ -200,7 +225,7 @@ class InviteTicket extends React.Component {
200225 switch(this.state.stage) {
201226 case INVITE_STAGES.INVITE_LOGIN:
202227 clickHandler = () => {
203− this.unlockInviteWallet(this.state.inviteTicket)
228+ this.unlockInviteWallet(inviteTicket)
204229 }
205230 continueReady = true
206231 break
@@ -214,10 +239,34 @@ class InviteTicket extends React.Component {
214239 break
215240 case INVITE_STAGES.INVITE_VERIFY:
216241 clickHandler = () => {
242+ const realTicket = realWallet.matchWith({
243+ Just: w => w.value.ticket,
244+ Nothing: null
245+ })
246+
247+ // if (realTicket !== verifyTicket) {
248+ // const verifyError = "Incorrect ticket. Please double-check the Master Ticket field from your generated wallet and try again."
249+ //
250+ // this.setState({
251+ // errors: [verifyError]
252+ // })
253+ // } else {
217254 this.setState({
218255 stage: INVITE_STAGES.INVITE_TRANSACTIONS,
219256 walletStates: this.state.walletStates.concat(WALLET_STATES.TRANSACTIONS)
220257 })
258+
259+ startTransactions({
260+ inviteWalletM: inviteWallet,
261+ realWalletM: realWallet,
262+ realPointM: realPoint,
263+ realTicket: verifyTicket,
264+ web3M: web3,
265+ contractsM: contracts,
266+ setUrbitWallet,
267+ updateProgress: this.updateProgress
268+ })
269+ // }
221270 }
222271 continueReady = true
223272 break
@@ -250,6 +299,19 @@ class InviteTicket extends React.Component {
250299 )
251300 }
252301
302+ getErrors() {
303+ if (this.state.errors.length === 0) return null
304+
305+ let errorElems = this.state.errors.map(e => <span>{e}</span>)
306+
307+ return (
308+ <Warning>
309+ <h3 className={'mb-2'}>{'Warning'}</h3>
310+ { errorElems }
311+ </Warning>
312+ )
313+ }
314+
253315 render() {
254316 const { wallet } = this.props
255317 const { inviteTicket } = this.state
@@ -258,11 +320,13 @@ class InviteTicket extends React.Component {
258320 let stageText = this.getStageText()
259321 let ticketElem = this.getTicketElement()
260322 let continueButton = this.getContinueButton()
323+ let errorDisplay = this.getErrors()
261324
262325 return (
263326 <Row>
264327 <Col className="col-md-4 mt-">
265328 {stageDisplay}
329+ {errorDisplay}
266330 {stageText}
267331 {ticketElem}
268332 {continueButton}
src/bridge/views/InviteTransactions.js−239
@@ -68,245 +68,6 @@ class InviteTransactions extends React.Component {
6868 this.setState({progress: msg+'...'});
6969 }
7070
71− askForFunding(address, amount, current) {
72− this.updateProgress(`Please make sure ${address} has at least ${amount} wei, we'll continue once that's true. Current balance: ${current}. Waiting`);
73− }
74−
75− async startTransactions() {
76− const { point } = this.state;
77−
78− if (Nothing.hasInstance(this.props.web3)) {
79− throw BRIDGE_ERROR.MISSING_WEB3;
80− }
81− const web3 = this.props.web3.value;
82−
83− if (Nothing.hasInstance(this.props.contracts)) {
84− throw BRIDGE_ERROR.MISSING_CONTRACTS;
85− }
86− const contracts = this.props.contracts.value;
87−
88− if (Nothing.hasInstance(this.props.wallet)) {
89− throw BRIDGE_ERROR.MISSING_WALLET;
90− }
91− const inviteWallet = this.props.wallet.value;
92− const inviteAddress = addressFromSecp256k1Public(inviteWallet.publicKey);
93− console.log('working as', inviteAddress);
94−
95− let newUrbitWallet = await urbitWalletFromTicket(this.state.ticket, point);
96−
97− // transfer from invite wallet to new wallet
98−
99− let transferTx = azimuth.ecliptic.transferPoint(contracts, point, newUrbitWallet.ownership.keys.address);
100− transferTx.gas = 500000; //TODO can maybe be lower?
101−
102− // ping gas tank with txs if needed
103−
104− let inviteNonce = await web3.eth.getTransactionCount(inviteAddress);
105− transferTx.nonce = inviteNonce++;
106− transferTx.gasPrice = 20000000000; //NOTE we pay the premium for faster ux
107− transferTx.from = inviteAddress;
108−
109− //NOTE using web3.eth.accounts.signTransaction is broken (1.0.0-beta51)
110− let transferStx = new Tx(transferTx);
111− transferStx.sign(inviteWallet.privateKey);
112− let rawTransferStx = '0x'+transferStx.serialize().toString('hex');
113−
114− const transferCost = transferTx.gas * transferTx.gasPrice;
115− await this.ensureFundsFor(web3, point, inviteAddress, transferCost, [rawTransferStx]);
116−
117− // send transaction
118− this.updateProgress(PROGRESS_STATES.CLAIMING);
119− await this.sendAndAwaitConfirm(web3, [rawTransferStx]);
120−
121− //
122− // we're gonna be operating as the new wallet from here on out, so change
123− // the relevant values
124− //
125− this.props.setUrbitWallet(Just(newUrbitWallet));
126− const newAddress = newUrbitWallet.ownership.keys.address;
127−
128− // configure management proxy
129−
130− let managementTx = azimuth.ecliptic.setManagementProxy(contracts, point, newUrbitWallet.management.keys.address);
131− managementTx.gas = 200000;
132− managementTx.nonce = 0;
133−
134− // configure networking public keys
135− //TODO feels like more of this logic should live in a lib?
136− let networkSeed = await attemptSeedDerivation(true, {
137− walletType: WALLET_NAMES.TICKET,
138− urbitWallet: Just(newUrbitWallet),
139− pointCursor: Just(point),
140− pointCache: {[point]: {keyRevisionNumber: 0}}
141− });
142− if (Nothing.hasInstance(networkSeed)) {
143− throw new Error('wtf network seed not derived');
144− }
145− networkSeed = networkSeed.value;
146− const networkKeys = kg.deriveNetworkKeys(networkSeed);
147−
148− let keysTx = azimuth.ecliptic.configureKeys(
149− contracts,
150− point,
151− addHexPrefix(networkKeys.crypt.public),
152− addHexPrefix(networkKeys.auth.public),
153− 1,
154− false
155− );
156− keysTx.gas = 150000;
157− keysTx.nonce = 1;
158−
159− //TODO refactor this process into function
160− let totalCost = 0;
161− let txs = [managementTx, keysTx];
162− txs = txs.map(tx => {
163− tx.gasPrice = 20000000000;
164− tx.from = newAddress;
165− totalCost = totalCost + (tx.gasPrice * tx.gas);
166− return tx;
167− });
168−
169− let rawStxs = txs.map(tx => {
170− let stx = new Tx(tx);
171− stx.sign(Buffer.from(newUrbitWallet.ownership.keys.private, 'hex'));
172− return '0x'+stx.serialize().toString('hex');
173− });
174−
175− await this.ensureFundsFor(web3, point, newAddress, totalCost, rawStxs);
176−
177− this.updateProgress(PROGRESS_STATES.CONFIGURING);
178− await this.sendAndAwaitConfirm(web3, rawStxs);
179−
180− // if non-trivial eth left in invite wallet, transfer to new ownership
181− this.updateProgress(PROGRESS_STATES.CLEANING);
182− let balance = await web3.eth.getBalance(inviteAddress);
183− const gasPrice = 20000000000;
184− const gasLimit = 21000;
185− const sendEthCost = gasPrice * gasLimit;
186− if (balance > sendEthCost) {
187− const value = (balance - sendEthCost);
188− console.log('sending', value);
189− const tx = {
190− to: newAddress,
191− value: value,
192− gasPrice: gasPrice,
193− gas: gasLimit,
194− nonce: inviteNonce++
195− }
196− let stx = new Tx(tx);
197− stx.sign(inviteWallet.privateKey);
198− const rawTx = '0x'+stx.serialize().toString('hex');
199− web3.eth.sendSignedTransaction(rawTx).catch(err => {
200− console.log('error sending value tx, who cares', err);
201− });
202− console.log('sent old balance');
203− }
204−
205− // proceed without waiting for confirm
206− this.updateProgress(PROGRESS_STATES.DONE);
207− this.props.setUrbitWallet(Just(newUrbitWallet));
208− //TODO forward to "all done!" screen
209− }
210−
211− async ensureFundsFor(web3, point, address, cost, signedTxs) {
212− this.updateProgress(PROGRESS_STATES.FUNDING);
213− let balance = await web3.eth.getBalance(address);
214−
215− if (cost > balance) {
216−
217− try {
218−
219− const fundsRemaining = await tank.remainingTransactions(point);
220− if (fundsRemaining < signedTxs.length) {
221− throw new Error('request invalid');
222− }
223−
224− const res = await tank.fundTransactions(signedTxs);
225− if (!res.success) {
226− throw new Error('request rejected');
227− } else {
228− await waitForTransactionConfirm(web3, res.txHash);
229− let newBalance = await web3.eth.getBalance(address);
230− console.log('funds have confirmed', balance >= cost, balance, newBalance);
231− }
232−
233− } catch (e) {
234−
235− console.log('funding failed', e);
236− await this.waitForBalance(web3, address, cost);
237−
238− }
239−
240− } else {
241− console.log('already have sufficient funds');
242− }
243− }
244−
245− // resolves when address has at least minBalance
246− //
247− async waitForBalance(web3, address, minBalance) {
248− console.log('awaiting balance', address, minBalance);
249− return new Promise((resolve, reject) => {
250− let oldBalance = null;
251− const checkForBalance = async () => {
252− const balance = await web3.eth.getBalance(address);
253− if (balance >= minBalance) {
254− resolve();
255− } else {
256− if (balance !== oldBalance) {
257− this.askForFunding(address, minBalance, balance);
258− }
259− setTimeout(checkForBalance, 13000);
260− }
261− };
262− checkForBalance();
263− });
264− }
265−
266− async sendAndAwaitConfirm(web3, signedTxs) {
267− await Promise.all(signedTxs.map(tx => {
268− console.log('sending...');
269− return new Promise((resolve, reject) => {
270− web3.eth.sendSignedTransaction(tx).then(res => {
271− console.log('sent, now waiting for confirm!', res.transactionHash);
272− waitForTransactionConfirm(web3, res.transactionHash)
273− .then(resolve);
274− }).catch(async err => {
275− // if there's an error, check if it's because the transaction was
276− // already confirmed prior to sending.
277− // this is really only the case in local dev environments.
278− const txHash = web3.utils.keccak256(tx);
279− console.log(err.message);
280− if (err.message.slice(0,54) ===
281− "Returned error: the tx doesn't have the correct nonce.") {
282− console.log('nonce error, awaiting confirm', err.message.slice(55));
283− //TODO max wait time before assume failure?
284− let res = await waitForTransactionConfirm(web3, txHash);
285− if (res) resolve();
286− else reject(new Error('Unexpected tx failure'));
287− } else {
288− const confirmed = await isTransactionConfirmed(web3, txHash);
289− console.log('error, but maybe confirmed:', confirmed);
290− if (confirmed) resolve();
291− else reject(err);
292− }
293− });
294− });
295− }));
296− }
297−
298− awaitForResult(desired, retryTime, func) {
299− return new Promise((resolve, reject) => {
300− const retry = async () => {
301− const result = await func();
302− console.log('result vs desired', result, desired);
303− if (result === desired) resolve();
304− else setTimeout(retry, retryTime);
305− };
306− retry();
307− });
308− }
309−
31071 render() {
31172
31273 const { point } = this.state;