on master help sign in
- [WIP] move transaction logic to lib/invite, almost complete flow
af5c226Chris Johnson
package-lock.json+82−29
@@ -1,6 +1,6 @@
1{
1{
2 "name": "bridge",
2 "name": "bridge",
3− "version": "1.2.1",
3+ "version": "1.3.1",
4 "lockfileVersion": 1,
4 "lockfileVersion": 1,
5 "requires": true,
5 "requires": true,
6 "dependencies": {
6 "dependencies": {
@@ -1058,6 +1058,19 @@
1058 "text-encoding": "^0.6.1"
1058 "text-encoding": "^0.6.1"
1059 }
1059 }
1060 },
1060 },
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+ },
1061 "@types/q": {
1074 "@types/q": {
1062 "version": "1.5.1",
1075 "version": "1.5.1",
1063 "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.1.tgz",
1076 "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.1.tgz",
@@ -1981,19 +1994,18 @@
1981 }
1994 }
1982 },
1995 },
1983 "azimuth-js": {
1996 "azimuth-js": {
1984− "version": "0.10.0",
1997+ "version": "0.13.2",
1985− "resolved": "https://registry.npmjs.org/azimuth-js/-/azimuth-js-0.10.0.tgz",
1998+ "resolved": "https://registry.npmjs.org/azimuth-js/-/azimuth-js-0.13.2.tgz",
1986− "integrity": "sha512-VDg60BYSpTjsv1K/DX7VY5F0HbxHV0X1cSoG3c1iZYH5Lxy8TErYq4CJ+D6er0xtI7JI3hmKOZS+p+ru42sT3A==",
1999+ "integrity": "sha512-DiteSHZTqst5Ikf5yoOetnU7MIE/El5JIuO9FjyaQHWPYA8ECB4XUaZJCl4VfScnAzzP1YCnORXz/mtVn2D9ng==",
1987 "requires": {
2000 "requires": {
1988− "azimuth-solidity": "1.0.2",
2001+ "azimuth-solidity": "1.1.0",
1989− "ethereumjs-util": "^5.2.0",
2002+ "ethereumjs-util": "^5.2.0"
1990− "web3": "1.0.0-beta.33"
1991 }
2003 }
1992 },
2004 },
1993 "azimuth-solidity": {
2005 "azimuth-solidity": {
1994− "version": "1.0.2",
2006+ "version": "1.1.0",
1995− "resolved": "https://registry.npmjs.org/azimuth-solidity/-/azimuth-solidity-1.0.2.tgz",
2007+ "resolved": "https://registry.npmjs.org/azimuth-solidity/-/azimuth-solidity-1.1.0.tgz",
1996− "integrity": "sha512-Yj+iAz/o7ladrRmjlWnnn7zqq5aVTQ1iKKprUBcbhxWnTfSfKvHwcF99Nyx6jNN7UjdiQ+gWt+4lJTlg1iewPg==",
2008+ "integrity": "sha512-YhVWKql1ZZ9Zae96nYA7QLwQlTzGspkNOErq170sl/UJwncQbvAyibSv+heMaS18n4soPDiRSpa1vHFoGoIX1A==",
1997 "requires": {
2009 "requires": {
1998 "babel-polyfill": "^6.26.0",
2010 "babel-polyfill": "^6.26.0",
1999 "babel-register": "^6.26.0",
2011 "babel-register": "^6.26.0",
@@ -18775,14 +18787,6 @@
18775 "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
18787 "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
18776 "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
18788 "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
18777 },
18789 },
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− },
18786 "typeforce": {
18790 "typeforce": {
18787 "version": "1.18.0",
18791 "version": "1.18.0",
18788 "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz",
18792 "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz",
@@ -19591,18 +19595,32 @@
19591 "integrity": "sha1-j93qQuGbvyUh7IeVRkV6Yjqdye8=",
19595 "integrity": "sha1-j93qQuGbvyUh7IeVRkV6Yjqdye8=",
19592 "requires": {
19596 "requires": {
19593 "underscore": "1.8.3",
19597 "underscore": "1.8.3",
19594− "web3-core-helpers": "1.0.0-beta.33",
19598+ "web3-core-helpers": "1.0.0-beta.33"
19595− "websocket": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2"
19596 },
19599 },
19597 "dependencies": {
19600 "dependencies": {
19598 "websocket": {
19601 "websocket": {
19599 "version": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2",
19602 "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",
19601 "requires": {
19604 "requires": {
19602 "debug": "^2.2.0",
19605 "debug": "^2.2.0",
19603 "nan": "^2.3.3",
19606 "nan": "^2.3.3",
19604 "typedarray-to-buffer": "^3.1.2",
19607 "typedarray-to-buffer": "^3.1.2",
19605 "yaeti": "^0.0.6"
19608 "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+ }
19606 }
19624 }
19607 }
19625 }
19608 }
19626 }
@@ -19637,10 +19655,50 @@
19637 "from": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2",
19655 "from": "git://github.com/frozeman/WebSocket-Node.git#6c72925e3f8aaaea8dc8450f97627e85263999f2",
19638 "requires": {
19656 "requires": {
19639 "debug": "^2.2.0",
19657 "debug": "^2.2.0",
19640− "nan": "^2.3.3",
19658+ "nan": "^2.3.3"
19641− "typedarray-to-buffer": "^3.1.2",
19659+ }
19642− "yaeti": "^0.0.6"
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"
19643 }
19686 }
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=="
19644 }
19702 }
19645 }
19703 }
19646 },
19704 },
@@ -20705,11 +20763,6 @@
20705 "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
20763 "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz",
20706 "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE="
20764 "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE="
20707 },
20765 },
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− },
20713 "yallist": {
20766 "yallist": {
20714 "version": "2.1.2",
20767 "version": "2.1.2",
20715 "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
20768 "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
src/bridge/lib/invite.js+290−1
@@ -1,7 +1,12 @@
1import lodash from 'lodash'
1import lodash from 'lodash'
2+import { Just, Nothing } from 'folktale/maybe'
3+import Tx from 'ethereumjs-tx'
4+
5+import * as azimuth from 'azimuth-js'
2import * as more from 'more-entropy'
6import * as more from 'more-entropy'
3import * as ob from 'urbit-ob'
7import * as ob from 'urbit-ob'
4import * as kg from '../../../node_modules/urbit-key-generation/dist/index'
8import * as kg from '../../../node_modules/urbit-key-generation/dist/index'
9+import * as tank from './tank'
5import { MAX_GALAXY, MIN_STAR, MAX_STAR, MIN_PLANET,
10import { MAX_GALAXY, MIN_STAR, MAX_STAR, MIN_PLANET,
6 GALAXY_ENTROPY_BITS, STAR_ENTROPY_BITS, PLANET_ENTROPY_BITS,
11 GALAXY_ENTROPY_BITS, STAR_ENTROPY_BITS, PLANET_ENTROPY_BITS,
7 SEED_ENTROPY_BITS,
12 SEED_ENTROPY_BITS,
@@ -11,6 +16,22 @@ import { MAX_GALAXY, MIN_STAR, MAX_STAR, MIN_PLANET,
11import JSZip from 'jszip'
16import JSZip from 'jszip'
12import saveAs from 'file-saver'
17import saveAs from 'file-saver'
13
18
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+
14const NEXT_STEP_NUM = 6;
35const NEXT_STEP_NUM = 6;
15const SEED_LENGTH_BYTES = SEED_ENTROPY_BITS / 8
36const SEED_LENGTH_BYTES = SEED_ENTROPY_BITS / 8
16
37
@@ -30,6 +51,16 @@ const WALLET_STATES = {
30 TRANSACTIONS: "Sending transactions"
51 TRANSACTIONS: "Sending transactions"
31}
52}
32
53
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+
33async function generateWallet(point) {
64async function generateWallet(point) {
34 const makeTicket = point => {
65 const makeTicket = point => {
35
66
@@ -115,9 +146,267 @@ async function downloadWallet(paper) {
115 })
146 })
116}
147}
117
148
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+
118export {
405export {
119 generateWallet,
406 generateWallet,
120 downloadWallet,
407 downloadWallet,
408+ startTransactions,
121 INVITE_STAGES,
409 INVITE_STAGES,
122− WALLET_STATES
410+ WALLET_STATES,
411+ TRANSACTION_STATES
123}
412}
src/bridge/views/InviteTicket.js+77−13
@@ -1,6 +1,6 @@
1import { Just, Nothing } from 'folktale/maybe'
1import { Just, Nothing } from 'folktale/maybe'
2import React from 'react'
2import React from 'react'
3−import { InnerLabel, ValidatedSigil, PointInput,
3+import { InnerLabel, ValidatedSigil, PointInput, Warning,
4 TicketInput, Button, Row, Col, H1, P, Passport } from '../components/Base'
4 TicketInput, Button, Row, Col, H1, P, Passport } from '../components/Base'
5import * as kg from '../../../node_modules/urbit-key-generation/dist/index'
5import * as kg from '../../../node_modules/urbit-key-generation/dist/index'
6import * as ob from 'urbit-ob'
6import * as ob from 'urbit-ob'
@@ -10,8 +10,8 @@ import { ROUTE_NAMES } from '../lib/router'
10import { DEFAULT_HD_PATH, urbitWalletFromTicket,
10import { DEFAULT_HD_PATH, urbitWalletFromTicket,
11 walletFromMnemonic, addressFromSecp256k1Public } from '../lib/wallet'
11 walletFromMnemonic, addressFromSecp256k1Public } from '../lib/wallet'
12import { BRIDGE_ERROR } from '../lib/error'
12import { BRIDGE_ERROR } from '../lib/error'
13−import { INVITE_STAGES, WALLET_STATES } from '../lib/invite'
13+import { INVITE_STAGES, WALLET_STATES, TRANSACTION_STATES } from '../lib/invite'
14−import { generateWallet } from '../lib/invite'
14+import { generateWallet, startTransactions } from '../lib/invite'
15
15
16const placeholder = (len) => {
16const placeholder = (len) => {
17 let bytes = window.crypto.getRandomValues(new Uint8Array(len))
17 let bytes = window.crypto.getRandomValues(new Uint8Array(len))
@@ -29,12 +29,16 @@ class InviteTicket extends React.Component {
29
29
30 this.state = {
30 this.state = {
31 inviteTicket: '',
31 inviteTicket: '',
32+ inviteWallet: Nothing(),
32 verifyTicket: '',
33 verifyTicket: '',
34+ realTicket: '',
33 realPoint: Nothing(),
35 realPoint: Nothing(),
34 realWallet: Nothing(),
36 realWallet: Nothing(),
35 walletReady: false,
37 walletReady: false,
38+ walletStates: [],
39+ errors: [],
36 stage: INVITE_STAGES.INVITE_LOGIN,
40 stage: INVITE_STAGES.INVITE_LOGIN,
37− walletStates: []
41+ transactionProgress: TRANSACTION_STATES.GENERATING
38 }
42 }
39
43
40 this.pointPlaceholder = placeholder(4)
44 this.pointPlaceholder = placeholder(4)
@@ -42,6 +46,7 @@ class InviteTicket extends React.Component {
42
46
43 this.handleInviteTicketInput = this.handleInviteTicketInput.bind(this)
47 this.handleInviteTicketInput = this.handleInviteTicketInput.bind(this)
44 this.handleVerifyTicketInput = this.handleVerifyTicketInput.bind(this)
48 this.handleVerifyTicketInput = this.handleVerifyTicketInput.bind(this)
49+ this.updateProgress = this.updateProgress.bind(this)
45 this.pushWalletState = this.pushWalletState.bind(this)
50 this.pushWalletState = this.pushWalletState.bind(this)
46 this.navigateLogin = this.navigateLogin.bind(this)
51 this.navigateLogin = this.navigateLogin.bind(this)
47 }
52 }
@@ -65,6 +70,10 @@ class InviteTicket extends React.Component {
65 this.setState({ verifyTicket })
70 this.setState({ verifyTicket })
66 }
71 }
67
72
73+ updateTransactionProgress(transactionProgress) {
74+ this.setState({ transactionProgress })
75+ }
76+
68 async unlockInviteWallet(inviteTicket) {
77 async unlockInviteWallet(inviteTicket) {
69 this.setState({
78 this.setState({
70 walletStates: this.state.walletStates.concat(WALLET_STATES.UNLOCKING),
79 walletStates: this.state.walletStates.concat(WALLET_STATES.UNLOCKING),
@@ -97,22 +106,29 @@ class InviteTicket extends React.Component {
97 walletStates: this.state.walletStates.concat(WALLET_STATES.GENERATING)
106 walletStates: this.state.walletStates.concat(WALLET_STATES.GENERATING)
98 })
107 })
99
108
100− let point
109+ let realPoint = Nothing()
101− let wallet
110+ let realWallet = Nothing()
111+ let errors = []
102
112
103 if (incoming.length > 0) {
113 if (incoming.length > 0) {
104 let pointNum = parseInt(incoming[0]);
114 let pointNum = parseInt(incoming[0]);
105− point = Just(pointNum);
115+ realPoint = Just(pointNum);
106− wallet = await generateWallet(pointNum);
116+ let genWallet = await generateWallet(pointNum);
117+
118+ realWallet = Just(genWallet)
107 if (incoming.length > 1) {
119 if (incoming.length > 1) {
108 //TODO notify user "...and others / ticket reusable"
120 //TODO notify user "...and others / ticket reusable"
109 }
121 }
122+ } else {
123+ errors = ["Invite code has no claimable ship. Check your invite code and try again."]
110 }
124 }
111
125
112 this.setState({
126 this.setState({
113− realPoint: point,
127+ realPoint,
114− realWallet: Just(wallet),
128+ realWallet,
129+ inviteWallet: Just(inviteWallet),
115− walletStates: this.state.walletStates.concat(WALLET_STATES.RENDERING)
130+ walletStates: this.state.walletStates.concat(WALLET_STATES.RENDERING),
131+ errors
116 })
132 })
117 }
133 }
118
134
@@ -179,7 +195,7 @@ class InviteTicket extends React.Component {
179 <div className="passport-progress-filled"></div>
195 <div className="passport-progress-filled"></div>
180 </div>
196 </div>
181 <div className="flex justify-between">
197 <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>
183 <div className="text-mono text-sm lh-6">3 minutes</div>
199 <div className="text-mono text-sm lh-6">3 minutes</div>
184 </div>
200 </div>
185 </div>
201 </div>
@@ -188,9 +204,18 @@ class InviteTicket extends React.Component {
188 }
204 }
189 }
205 }
190
206
207+ updateProgress(transactionProgress) {
208+ this.setState({
209+ transactionProgress
210+ })
211+ }
212+
191 getContinueButton() {
213 getContinueButton() {
192 if (this.state.stage === INVITE_STAGES.INVITE_TRANSACTIONS) return null
214 if (this.state.stage === INVITE_STAGES.INVITE_TRANSACTIONS) return null
193
215
216+ const { realPoint, realWallet, inviteWallet, verifyTicket, inviteTicket } = this.state
217+ const { web3, contracts, setUrbitWallet } = this.props
218+
194 let clickHandler
219 let clickHandler
195 let btnColor = this.state.walletStates.includes(WALLET_STATES.PAPER_READY)
220 let btnColor = this.state.walletStates.includes(WALLET_STATES.PAPER_READY)
196 ? 'green'
221 ? 'green'
@@ -200,7 +225,7 @@ class InviteTicket extends React.Component {
200 switch(this.state.stage) {
225 switch(this.state.stage) {
201 case INVITE_STAGES.INVITE_LOGIN:
226 case INVITE_STAGES.INVITE_LOGIN:
202 clickHandler = () => {
227 clickHandler = () => {
203− this.unlockInviteWallet(this.state.inviteTicket)
228+ this.unlockInviteWallet(inviteTicket)
204 }
229 }
205 continueReady = true
230 continueReady = true
206 break
231 break
@@ -214,10 +239,34 @@ class InviteTicket extends React.Component {
214 break
239 break
215 case INVITE_STAGES.INVITE_VERIFY:
240 case INVITE_STAGES.INVITE_VERIFY:
216 clickHandler = () => {
241 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 {
217 this.setState({
254 this.setState({
218 stage: INVITE_STAGES.INVITE_TRANSACTIONS,
255 stage: INVITE_STAGES.INVITE_TRANSACTIONS,
219 walletStates: this.state.walletStates.concat(WALLET_STATES.TRANSACTIONS)
256 walletStates: this.state.walletStates.concat(WALLET_STATES.TRANSACTIONS)
220 })
257 })
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+ // }
221 }
270 }
222 continueReady = true
271 continueReady = true
223 break
272 break
@@ -250,6 +299,19 @@ class InviteTicket extends React.Component {
250 )
299 )
251 }
300 }
252
301
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+
253 render() {
315 render() {
254 const { wallet } = this.props
316 const { wallet } = this.props
255 const { inviteTicket } = this.state
317 const { inviteTicket } = this.state
@@ -258,11 +320,13 @@ class InviteTicket extends React.Component {
258 let stageText = this.getStageText()
320 let stageText = this.getStageText()
259 let ticketElem = this.getTicketElement()
321 let ticketElem = this.getTicketElement()
260 let continueButton = this.getContinueButton()
322 let continueButton = this.getContinueButton()
323+ let errorDisplay = this.getErrors()
261
324
262 return (
325 return (
263 <Row>
326 <Row>
264 <Col className="col-md-4 mt-">
327 <Col className="col-md-4 mt-">
265 {stageDisplay}
328 {stageDisplay}
329+ {errorDisplay}
266 {stageText}
330 {stageText}
267 {ticketElem}
331 {ticketElem}
268 {continueButton}
332 {continueButton}
src/bridge/views/InviteTransactions.js−239
@@ -68,245 +68,6 @@ class InviteTransactions extends React.Component {
68 this.setState({progress: msg+'...'});
68 this.setState({progress: msg+'...'});
69 }
69 }
70
70
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−
310 render() {
71 render() {
311
72
312 const { point } = this.state;
73 const { point } = this.state;