on master help sign in
Merge pull request #120 from urbit/mino-onboarding-styling

[WIP] Mino onboarding styling

commitb3b0ff4740238aecd13c26371aab61e3905ca08b authorChristopher Johnson <chris@cjohnson.io> dateTue 28 May 2019, 10:15 (7 years ago) parent6a8b3c14bd2eee
26 files changed+1254−908
src/Root.js+1−1 src/bridge/Bridge.js+16−2 src/bridge/components/Base.js+23 src/bridge/components/Header.js+17−17 src/bridge/components/Passport.js+240 src/bridge/components/Warning.js+1−1 src/bridge/components/advancedInput.js+1 src/bridge/lib/invite.js+438 src/bridge/lib/router.js+4−18 src/bridge/lib/txn.js−1 src/bridge/lib/validators.js+1−24 src/bridge/style/button.css+10 src/bridge/style/extend.css+118 src/bridge/views/InviteLogin.js−106 src/bridge/views/InviteTicket.js+374 src/bridge/views/InviteTransactions.js−346 src/bridge/views/InviteVerify.js−106 src/bridge/views/InviteWallet.js−226 src/bridge/views/InvitesManage.js+2−15 src/bridge/views/InvitesSend.js+2−27 src/bridge/views/Landing.js+1−1 src/bridge/views/Shards.js+1−1 src/bridge/views/Ticket.js+1−3 src/common/components/AppNavigation.js+1−1 src/walletgen/lib/lib.js+1−3 src/walletgen/views/Generate.js+1−9
src/Root.js+1−1
@@ -5,7 +5,7 @@ import _ from 'lodash'
55
66import Bridge from './bridge/Bridge'
77import Walletgen from './walletgen/Walletgen'
8−import { AppNavigation } from './common/components/AppNavigation'
8+// import { AppNavigation } from './common/components/AppNavigation'
99
1010import './common/style/index.css'
1111import './bridge/style/index.css'
src/bridge/Bridge.js+16−2
@@ -280,16 +280,30 @@ class Bridge extends React.Component {
280280
281281 const View = router(routeCrumbs.peek())
282282
283+ // For the invite acceptance flow, widen the screen to use the full
284+ // container, and hide the breadcrumbs
285+ const colClass = routeCrumbs.contains(ROUTE_NAMES.INVITE_TICKET)
286+ ? 'col-md-12'
287+ : 'col-md-offset-1 col-md-10'
288+
289+ const colStyle = routeCrumbs.contains(ROUTE_NAMES.INVITE_TICKET)
290+ ? {}
291+ : {maxWidth: '620px'}
292+
293+ const showCrumbs = routeCrumbs.contains(ROUTE_NAMES.INVITE_TICKET)
294+ ? false
295+ : true
296+
283297 return (
284298 <Container>
285299 <Row>
286− <Col className='col-md-1' />
287− <Col className='col-md-10' style={{maxWidth: '620px'}}>
300+ <Col className={colClass} style={colStyle}>
288301 <Header
289302 routeCrumbs={ routeCrumbs }
290303 skipRoute={ this.skipRoute }
291304 networkType={ networkType }
292305 wallet={ wallet }
306+ showCrumbs={ showCrumbs }
293307 pointCursor={ pointCursor } />
294308
295309 <Row className={ 'row wrapper' }>
src/bridge/components/Base.js+23
@@ -1,5 +1,6 @@
11// Helpers
22import {
3+ simpleValidatorWrapper,
34 validateMnemonic,
45 validateNotEmpty,
56 validateEthereumAddress,
@@ -66,6 +67,7 @@ import ValidatedSigil from './ValidatedSigil';
6667import ValidatedBlockie from './ValidatedBlockie';
6768import Chevron from './icons/Chevron'
6869import ShowBlockie from './ShowBlockie'
70+import Passport from './Passport'
6971
7072// HOC validated form components
7173
@@ -145,6 +147,25 @@ const TicketInput = advancedInput({
145147 ]
146148});
147149
150+const VerifyTicketInput = matchingTicket => advancedInput({
151+ WrappedComponent: Input,
152+ validators: [
153+ validateTicket,
154+ validateNotEmpty,
155+
156+ // TODO: This should be in /lib/validators, but we'd need a way for
157+ // validators to compare against dynamic values
158+ m => simpleValidatorWrapper({
159+ prevMessage: m,
160+ validator: d => d === matchingTicket,
161+ errorMessage: 'This does not match the provided master ticket'
162+ })
163+ ],
164+ transformers: [
165+ prependSig,
166+ ]
167+});
168+
148169const ShardInput = advancedInput({
149170 WrappedComponent: Input,
150171 validators: [
@@ -171,6 +192,7 @@ export {
171192 GalaxyInput,
172193 PointInput,
173194 TicketInput,
195+ VerifyTicketInput,
174196 ShardInput,
175197 RequiredInput,
176198 NetworkKeyInput,
@@ -207,4 +229,5 @@ export {
207229 Warning,
208230
209231 Chevron,
232+ Passport
210233};
src/bridge/components/Header.js+17−17
@@ -11,13 +11,8 @@ const Crumbs = (props) => {
1111 // FIXME probably more straightforward to render them normally and just
1212 // return the reversed array of renders
1313 const rendered = history.map((route, idx) => {
14−
1514 return (
1615 <div className={'flex items-center'} key={ `history-${idx}` }>
17−
18−
19−
20−
2116 <Button
2217 prop-type={'link'}
2318 prop-size={'sm'}
@@ -31,8 +26,6 @@ const Crumbs = (props) => {
3126 ? <div />
3227 : <Chevron className={'h-4 mh-2'} />
3328 }
34−
35−
3629 </div>
3730 )
3831 })
@@ -40,15 +33,22 @@ const Crumbs = (props) => {
4033 return rendered
4134}
4235
43−const Header = (props) =>
44− <div className={'flex items-center h-10'}>
45− <Crumbs
46− routeCrumbs={ props.routeCrumbs }
47− skipRoute={ props.skipRoute }
48− networkType={ props.networkType }
49− wallet={ props.wallet }
50− pointCursor={ props.pointCursor }
51− />
52− </div>
36+const Header = (props) => {
37+ const crumbs = (
38+ <div className={'flex items-center h-10'}>
39+ <Crumbs
40+ routeCrumbs={ props.routeCrumbs }
41+ skipRoute={ props.skipRoute }
42+ networkType={ props.networkType }
43+ wallet={ props.wallet }
44+ pointCursor={ props.pointCursor }
45+ />
46+ </div>
47+ )
48+
49+ const noCrumbs = <h2 className="mt-9 clickable" onClick={() => props.skipRoute(1)}>Bridge</h2>
50+
51+ return props.showCrumbs ? crumbs : noCrumbs
52+}
5353
5454export default Header
src/bridge/components/Passport.jsnew file+240
@@ -0,0 +1,240 @@
1+import React from 'react'
2+import { pour } from 'sigil-js'
3+import * as ob from 'urbit-ob'
4+import ReactSVGComponents from './ReactSVGComponents'
5+import PropTypes from 'prop-types'
6+import PaperCollateralRenderer from 'PaperCollateralRenderer'
7+import { Button } from './Base'
8+
9+import { WALLET_STATES } from '../lib/invite'
10+import { downloadWallet } from '../lib/invite'
11+
12+class Passport extends React.Component {
13+
14+ constructor(props) {
15+ super(props)
16+
17+ this.state = {
18+ paper: null
19+ }
20+
21+ this.masterCensor = this.generateCensor()
22+ this.managementCensor = this.generateCensor()
23+
24+ this.download = this.download.bind(this)
25+ }
26+
27+ download() {
28+ downloadWallet(this.state.paper)
29+ this.props.pushWalletState(WALLET_STATES.DOWNLOADED)
30+ }
31+
32+ generateCensor() {
33+ let blocks = [];
34+
35+ (new Uint32Array(86)).forEach((_, i) => {
36+ let rand = Math.round(Math.random())
37+ let filled = (rand || i === 0 || i === 43)
38+
39+ let className = filled ? "passport-block bg-gray-30" : "passport-block"
40+
41+ blocks.push(<div key={i} data-key={i}className={className}></div>)
42+ })
43+
44+ return (
45+ <div className="flex flex-wrap passport-censor">
46+ {blocks}
47+ </div>
48+ )
49+ }
50+
51+ getCurrentDate() {
52+ let date = new Date()
53+ const fil = n => n >= 10 ? n : `0${n}`
54+ let fmtDate = `${date.getUTCFullYear()}.` +
55+   `${(date.getUTCMonth() + 1)}.` +
56+   `${fil(date.getUTCDate())}`
57+
58+ return fmtDate
59+ }
60+
61+ getWalletProgress() {
62+ let { walletStates } = this.props
63+
64+ if ( !walletStates.includes(WALLET_STATES.UNLOCKING) ) return null
65+ if ( walletStates.includes(WALLET_STATES.TRANSACTIONS) ) return null
66+
67+ let progressTimes = {
68+ [WALLET_STATES.UNLOCKING]: {ms: "2813", pct: "44%"},
69+ [WALLET_STATES.GENERATING]: {ms: "2433", pct: "38%"},
70+ [WALLET_STATES.RENDERING]: {ms: "986", pct: "15%"}
71+ }
72+
73+ const progressBars = Object.keys(progressTimes).map(state => {
74+ let progressIndex = walletStates.indexOf(state)
75+ let progressDone = progressIndex !== -1 && progressIndex < walletStates.length -1
76+
77+ let progressStyle = {
78+ width: progressTimes[state].pct
79+ }
80+
81+ return (
82+ <div className="passport-wallet-progress" style={progressStyle}>
83+ {progressDone &&
84+ <div className="passport-wallet-progress-fill"></div>
85+ }
86+ </div>
87+ )
88+ })
89+
90+ return (
91+ <div className="flex space-between mt-3">
92+ {progressBars}
93+ </div>
94+ )
95+ }
96+
97+ getWalletStates() {
98+ let { walletStates } = this.props
99+
100+ if ( walletStates.includes(WALLET_STATES.TRANSACTIONS) ) return null
101+
102+ let stateElems = walletStates.map(state => {
103+ if (state === WALLET_STATES.DOWNLOADED) return null
104+
105+ const lastIndex = walletStates.indexOf(state) === walletStates.length - 1
106+ const checkmark = lastIndex
107+ ? null
108+ : <div className="text-700 text-sm lh-6 green-dark">✓</div>
109+
110+ return (
111+ <div className="flex justify-between">
112+ <div className="text-mono text-sm lh-6 green-dark">{state}</div>
113+ { checkmark }
114+ </div>
115+ )
116+ })
117+
118+ return (
119+ <div className="mt-3">
120+ {stateElems}
121+ </div>
122+ )
123+ }
124+
125+ render() {
126+ let pointVal = this.props.point.matchWith({
127+ Just: p => p.value,
128+ Nothing: () => ''
129+ })
130+
131+ let patp = pointVal === '' ? '' : ob.patp(pointVal)
132+
133+ let wallet = this.props.wallet.matchWith({
134+ Just: w => w.value,
135+ Nothing: () => null
136+ })
137+
138+ let ownershipAddress = this.props.wallet.matchWith({
139+ Just: w => w.value.ownership.keys.address,
140+ Nothing: () => null
141+ })
142+
143+ const sigil = patp !== ''
144+ ? pour({
145+ patp: patp,
146+ renderer: ReactSVGComponents,
147+ size: 128
148+ })
149+ : <div className="passport-sigil-blank"></div>
150+
151+ let currentDate = this.getCurrentDate()
152+ let walletStates = this.getWalletStates()
153+ let walletProgress = this.getWalletProgress()
154+ let paperReady = this.state.paper && this.props.walletStates.includes(WALLET_STATES.PAPER_READY)
155+ let downloadColor = paperReady ? 'green' : 'black'
156+
157+ let downloadBtn = (
158+ <Button
159+ disabled={ !paperReady }
160+ prop-size={ 'lg wide' }
161+ prop-color={ downloadColor }
162+ onClick={ this.download }
163+ >
164+ { 'Download' }
165+ </Button>
166+ )
167+
168+ let paperCollateral = wallet
169+ ? (
170+ <PaperCollateralRenderer
171+ wallet={{[pointVal]: wallet}}
172+ className={'extremely-hidden'}
173+ callback={data => {
174+ this.setState({paper: data})
175+ this.props.pushWalletState(WALLET_STATES.PAPER_READY)
176+ }}
177+ mode={'REGISTRATION'} />
178+ )
179+ : <div />
180+
181+ let walletAddresses = (
182+ <div className="mt-8">
183+ <div className="passport-label text-mono">Ethereum Address</div>
184+ <div className="passport-value">{ownershipAddress || 'Ungenerated'}</div>
185+
186+ <div className="passport-label gray-50 mt-3">Master Ticket</div>
187+ <div className="passport-value passport-censor overflow-hidden">{this.masterCensor}</div>
188+
189+ <div className="passport-label gray-50 mt-5">Management Seed</div>
190+ <div className="passport-value passport-censor overflow-hidden">{this.managementCensor}</div>
191+ </div>
192+ )
193+
194+ let walletMetadata = (
195+ <div className="flex">
196+ <div className="passport-sigil">
197+ {sigil}
198+ </div>
199+ <div className="passport-metadata ml-4">
200+ <div className="passport-label text-mono">Point</div>
201+ <div className="text-mono">{patp || '-'}</div>
202+
203+ <div className="passport-label gray-50 mt-6">Created on</div>
204+ <div className="passport-value">{patp ? currentDate : '-' }</div>
205+
206+ <div className="passport-label gray-50 mt-2">Invited by</div>
207+ <div className="passport-value">{this.props.invitedBy || '-'}</div>
208+ </div>
209+ </div>
210+ )
211+
212+ return (
213+ <div>
214+ <div className="passport-border">
215+ <div className={`passport ${paperReady && 'passport-filled'}`}>
216+ { walletMetadata}
217+ { walletAddresses }
218+ { paperCollateral }
219+
220+ </div>
221+ { downloadBtn }
222+ </div>
223+
224+ { walletProgress }
225+ { walletStates }
226+
227+ </div>
228+ )
229+ }
230+}
231+
232+Passport.propTypes = {
233+ wallet: PropTypes.object
234+};
235+
236+Passport.defaultProps = {
237+ wallet: null
238+};
239+
240+export default Passport
src/bridge/components/Warning.js+1−1
@@ -5,7 +5,7 @@ const Warning = ({ className, children, style, measure }) => {
55
66
77 return (
8− <div className={`mb-7 p-4 bg-yellow ${className}`} style={style}>
8+ <div className={`mb-7 p-4 bg-yellow break-word ${className}`} style={style}>
99 { children }
1010 </div>
1111 )
src/bridge/components/advancedInput.js+1
@@ -75,6 +75,7 @@ const advancedInput = ({
7575
7676 // Run validation chain on the transformed value. Validators are executed
7777 // from right to left and exit on the first validation failure.
78+
7879 const report = compose(...validators, initMessage)(props.value)
7980
8081 // Makes a uniform data structure to pass to display triggers. This
src/bridge/lib/invite.jsnew file+438
@@ -0,0 +1,438 @@
1+import lodash from 'lodash'
2+import { Just, Nothing } from 'folktale/maybe'
3+import Tx from 'ethereumjs-tx'
4+
5+import * as azimuth from 'azimuth-js'
6+import * as more from 'more-entropy'
7+import * as ob from 'urbit-ob'
8+import * as kg from '../../../node_modules/urbit-key-generation/dist/index'
9+import * as tank from './tank'
10+import { MIN_STAR, MIN_PLANET, SEED_ENTROPY_BITS,
11+ GALAXY_ENTROPY_BITS, STAR_ENTROPY_BITS, PLANET_ENTROPY_BITS,
12+ } from '../../walletgen/lib/constants'
13+
14+import JSZip from 'jszip'
15+import saveAs from 'file-saver'
16+
17+import {
18+ waitForTransactionConfirm,
19+ isTransactionConfirmed
20+} from './txn'
21+import { BRIDGE_ERROR } from './error'
22+import { attemptSeedDerivation } from './keys'
23+import {
24+ addressFromSecp256k1Public,
25+ addHexPrefix,
26+ WALLET_NAMES
27+} from './wallet'
28+
29+const SEED_LENGTH_BYTES = SEED_ENTROPY_BITS / 8
30+
31+const INVITE_STAGES = {
32+ INVITE_LOGIN: "invite login",
33+ INVITE_WALLET: "invite wallet",
34+ INVITE_VERIFY: "invite verify",
35+ INVITE_TRANSACTIONS: "invite transactions"
36+}
37+
38+const WALLET_STATES = {
39+ UNLOCKING: "Unlocking invite wallet",
40+ GENERATING: "Generating your wallet",
41+ RENDERING: "Creating paper collateral",
42+ PAPER_READY: "Download your wallet",
43+ DOWNLOADED: "Wallet downloaded",
44+ TRANSACTIONS: "Sending transactions"
45+}
46+
47+const TRANSACTION_STATES = {
48+ GENERATING: {
49+ label: 'Generating transactions',
50+ pct: "0%"
51+ },
52+ SIGNING: {
53+ label: 'Signing transactions',
54+ pct: "15%"
55+ },
56+ FUNDING_INVITE: {
57+ label: 'Funding invite wallet',
58+ pct: "30%"
59+ },
60+ CLAIMING: {
61+ label: 'Claiming invite',
62+ pct: "55%"
63+ },
64+ FUNDING_RECIPIENT: {
65+ label: 'Funding recipient wallet',
66+ pct: "65%"
67+ },
68+ CONFIGURING: {
69+ label: 'Configuring planet',
70+ pct: "85%"
71+ },
72+ CLEANING: {
73+ label: 'Cleaning up',
74+ pct: "95%"
75+ },
76+ DONE: {
77+ label: 'Done',
78+ pct: "100%"
79+ },
80+}
81+
82+//TODO should be moved to lib/walletgen
83+async function generateWallet(point) {
84+ const makeTicket = point => {
85+
86+ const bits = point < MIN_STAR
87+ ? GALAXY_ENTROPY_BITS
88+ : point < MIN_PLANET
89+ ? STAR_ENTROPY_BITS
90+ : PLANET_ENTROPY_BITS
91+
92+ const bytes = bits / 8
93+ const some = new Uint8Array(bytes)
94+ window.crypto.getRandomValues(some)
95+
96+ const gen = new more.Generator()
97+
98+ return new Promise((resolve, reject) => {
99+ gen.generate(bits, result => {
100+ const chunked = lodash.chunk(result, 2)
101+ const desired = chunked.slice(0, bytes) // only take required entropy
102+ const more = lodash.flatMap(desired, arr => arr[0] ^ arr[1])
103+ const entropy = lodash.zipWith(some, more, (x, y) => x ^ y)
104+ const buf = Buffer.from(entropy)
105+ const patq = ob.hex2patq(buf.toString('hex'))
106+ resolve(patq)
107+ reject('Entropy generation failed')
108+ })
109+ })
110+ }
111+
112+ const genWallet = async (point, ticket, cb) => {
113+
114+ const config = {
115+ ticket: ticket,
116+ seedSize: SEED_LENGTH_BYTES,
117+ ship: point,
118+ password: '',
119+ revisions: {},
120+ boot: false //TODO should this generate networking keys here already?
121+ };
122+
123+ const wallet = await kg.generateWallet(config);
124+
125+ // This is here to notify the anyone who opens console because the thread
126+ // hangs, blocking UI updates so this cannot be doen in the UI
127+ console.log('Generating Wallet for point address: ', point);
128+
129+ return wallet;
130+ }
131+
132+ const ticket = await makeTicket(point);
133+ const wallet = await genWallet(point, ticket);
134+ return wallet;
135+}
136+
137+//TODO should be moved to lib/walletgen
138+async function downloadWallet(paper) {
139+ return new Promise((resolve, reject) => {
140+ const zip = new JSZip();
141+ //TODO the categories here aren't explained in bridge at all...
142+
143+ const bin0 = paper.filter(item => item.bin === '0');
144+ const bin1 = paper.filter(item => item.bin === '1');
145+ const bin2 = paper.filter(item => item.bin === '2');
146+ const bin3 = paper.filter(item => item.bin === '3');
147+ const bin4 = paper.filter(item => item.bin === '4');
148+
149+ const bin0Folder = zip.folder("0. Public");
150+ const bin1Folder = zip.folder("1. Very High Friction Custody");
151+ const bin2Folder = zip.folder("2. High Friction Custody");
152+ const bin3Folder = zip.folder("3. Medium Friction Custody");
153+ const bin4Folder = zip.folder("4. Low Friction Custody");
154+
155+ bin0.forEach(item => bin0Folder.file(`${item.pageTitle}.png`, item.png))
156+ bin1.forEach(item => bin1Folder.file(`${item.pageTitle}.png`, item.png));
157+ bin2.forEach(item => bin2Folder.file(`${item.pageTitle}.png`, item.png));
158+ bin3.forEach(item => bin3Folder.file(`${item.pageTitle}.png`, item.png));
159+ bin4.forEach(item => bin4Folder.file(`${item.pageTitle}.png`, item.png));
160+
161+ zip.generateAsync({type:"blob"}).then((content) => {
162+ saveAs(content, 'urbit-wallet.zip');
163+ resolve(true)
164+ });
165+ })
166+}
167+
168+async function startTransactions(args) {
169+ let { realPointM, web3M, contractsM,
170+ inviteWalletM, realWalletM, setUrbitWallet, updateProgress } = args
171+
172+ if (Nothing.hasInstance(web3M)) {
173+ throw BRIDGE_ERROR.MISSING_WEB3;
174+ }
175+ const web3 = web3M.value;
176+
177+ if (Nothing.hasInstance(contractsM)) {
178+ throw BRIDGE_ERROR.MISSING_CONTRACTS;
179+ }
180+ const contracts = contractsM.value;
181+
182+ if (Nothing.hasInstance(inviteWalletM)) {
183+ throw BRIDGE_ERROR.MISSING_WALLET;
184+ }
185+ const inviteWallet = inviteWalletM.value;
186+
187+ if (Nothing.hasInstance(realWalletM)) {
188+ throw BRIDGE_ERROR.MISSING_WALLET;
189+ }
190+ const realWallet = realWalletM.value;
191+
192+ // const realWallet = await urbitWalletFromTicket(args.realTicket, point);
193+
194+ if (Nothing.hasInstance(realPointM)) {
195+ throw BRIDGE_ERROR.MISSING_POINT;
196+ }
197+ const point = realPointM.value;
198+
199+ const inviteAddress = addressFromSecp256k1Public(inviteWallet.publicKey);
200+ console.log('working as', inviteAddress);
201+
202+ // transfer from invite wallet to new wallet
203+
204+ let transferTx = azimuth.ecliptic.transferPoint(contracts, point, realWallet.ownership.keys.address);
205+ transferTx.gas = 500000; //TODO can maybe be lower?
206+
207+ // ping gas tank with txs if needed
208+
209+ let inviteNonce = await web3.eth.getTransactionCount(inviteAddress);
210+ transferTx.nonce = inviteNonce++;
211+ transferTx.gasPrice = 20000000000; //NOTE we pay the premium for faster ux
212+ transferTx.from = inviteAddress;
213+
214+ //NOTE using web3.eth.accounts.signTransaction is broken (1.0.0-beta51)
215+ let transferStx = new Tx(transferTx);
216+ transferStx.sign(inviteWallet.privateKey);
217+ let rawTransferStx = '0x'+transferStx.serialize().toString('hex');
218+
219+ const transferCost = transferTx.gas * transferTx.gasPrice;
220+ await ensureFundsFor(web3, point, inviteAddress, transferCost, [rawTransferStx], updateProgress, true);
221+
222+ updateProgress({
223+ type: "progress",
224+ value: TRANSACTION_STATES.CLAIMING
225+ });
226+
227+ // send transaction
228+ await sendAndAwaitConfirm(web3, [rawTransferStx]);
229+
230+ //
231+ // we're gonna be operating as the new wallet from here on out, so change
232+ // the relevant values
233+ //
234+ setUrbitWallet(Just(realWallet));
235+ const newAddress = realWallet.ownership.keys.address;
236+
237+ // configure management proxy
238+
239+ let managementTx = azimuth.ecliptic.setManagementProxy(contracts, point, realWallet.management.keys.address);
240+ managementTx.gas = 200000;
241+ managementTx.nonce = 0;
242+
243+ // configure networking public keys
244+ //TODO feels like more of this logic should live in a lib?
245+ let networkSeed = await attemptSeedDerivation(true, {
246+ walletType: WALLET_NAMES.TICKET,
247+ urbitWallet: Just(realWallet),
248+ pointCursor: Just(point),
249+ pointCache: {[point]: {keyRevisionNumber: 0}}
250+ });
251+ if (Nothing.hasInstance(networkSeed)) {
252+ throw new Error('wtf network seed not derived');
253+ }
254+ networkSeed = networkSeed.value;
255+ const networkKeys = kg.deriveNetworkKeys(networkSeed);
256+
257+ let keysTx = azimuth.ecliptic.configureKeys(
258+ contracts,
259+ point,
260+ addHexPrefix(networkKeys.crypt.public),
261+ addHexPrefix(networkKeys.auth.public),
262+ 1,
263+ false
264+ );
265+ keysTx.gas = 150000;
266+ keysTx.nonce = 1;
267+
268+ //TODO refactor this process into function
269+ let totalCost = 0;
270+ let txs = [managementTx, keysTx];
271+ txs = txs.map(tx => {
272+ tx.gasPrice = 20000000000;
273+ tx.from = newAddress;
274+ totalCost = totalCost + (tx.gasPrice * tx.gas);
275+ return tx;
276+ });
277+
278+ let rawStxs = txs.map(tx => {
279+ let stx = new Tx(tx);
280+ stx.sign(Buffer.from(realWallet.ownership.keys.private, 'hex'));
281+ return '0x'+stx.serialize().toString('hex');
282+ });
283+
284+ await ensureFundsFor(web3, point, newAddress, totalCost, rawStxs, updateProgress, false);
285+
286+ updateProgress({
287+ type: "progress",
288+ value: TRANSACTION_STATES.CONFIGURING
289+ });
290+
291+ await sendAndAwaitConfirm(web3, rawStxs);
292+
293+ updateProgress({
294+ type: "progress",
295+ value: TRANSACTION_STATES.CLEANING
296+ });
297+
298+ // if non-trivial eth left in invite wallet, transfer to new ownership
299+ let balance = await web3.eth.getBalance(inviteAddress);
300+ const gasPrice = 20000000000;
301+ const gasLimit = 21000;
302+ const sendEthCost = gasPrice * gasLimit;
303+ if (balance > sendEthCost) {
304+ const value = (balance - sendEthCost);
305+ console.log('sending', value);
306+ const tx = {
307+ to: newAddress,
308+ value: value,
309+ gasPrice: gasPrice,
310+ gas: gasLimit,
311+ nonce: inviteNonce++
312+ }
313+ let stx = new Tx(tx);
314+ stx.sign(inviteWallet.privateKey);
315+ const rawTx = '0x'+stx.serialize().toString('hex');
316+ web3.eth.sendSignedTransaction(rawTx).catch(err => {
317+ console.log('error sending value tx, who cares', err);
318+ });
319+ console.log('sent old balance');
320+ }
321+
322+ updateProgress({
323+ type: "progress",
324+ value: TRANSACTION_STATES.DONE
325+ });
326+
327+ // proceed without waiting for confirm
328+ setUrbitWallet(Just(realWallet));
329+}
330+
331+async function ensureFundsFor(web3, point, address, cost, signedTxs, updateProgress, firstTx) {
332+ let fundingState = firstTx? TRANSACTION_STATES.FUNDING_INVITE : TRANSACTION_STATES.FUNDING_RECIPIENT
333+
334+ updateProgress({
335+ type: "progress",
336+ value: fundingState
337+ });
338+
339+ let balance = await web3.eth.getBalance(address);
340+
341+ if (cost > balance) {
342+
343+ try {
344+
345+ const fundsRemaining = await tank.remainingTransactions(point);
346+ if (fundsRemaining < signedTxs.length) {
347+ throw new Error('request invalid');
348+ }
349+
350+ const res = await tank.fundTransactions(signedTxs);
351+ if (!res.success) {
352+ throw new Error('request rejected');
353+ } else {
354+ await waitForTransactionConfirm(web3, res.txHash);
355+ let newBalance = await web3.eth.getBalance(address);
356+ console.log('funds have confirmed', balance >= cost, balance, newBalance);
357+ }
358+
359+ } catch (e) {
360+
361+ console.log('funding failed', e);
362+ await waitForBalance(web3, address, cost, updateProgress);
363+
364+ }
365+
366+ } else {
367+ console.log('already have sufficient funds');
368+ }
369+}
370+
371+// resolves when address has at least minBalance
372+//
373+async function waitForBalance(web3, address, minBalance, updateProgress) {
374+ console.log('awaiting balance', address, minBalance);
375+ return new Promise((resolve, reject) => {
376+ let oldBalance = null;
377+ const checkForBalance = async () => {
378+ const balance = await web3.eth.getBalance(address);
379+ if (balance >= minBalance) {
380+ resolve();
381+ } else {
382+ if (balance !== oldBalance) {
383+ askForFunding(address, minBalance, balance, updateProgress);
384+ }
385+ setTimeout(checkForBalance, 13000);
386+ }
387+ };
388+ checkForBalance();
389+ });
390+}
391+
392+async function sendAndAwaitConfirm(web3, signedTxs) {
393+ await Promise.all(signedTxs.map(tx => {
394+ console.log('sending...');
395+ return new Promise((resolve, reject) => {
396+ web3.eth.sendSignedTransaction(tx).then(res => {
397+ console.log('sent, now waiting for confirm!', res.transactionHash);
398+ waitForTransactionConfirm(web3, res.transactionHash)
399+ .then(resolve);
400+ }).catch(async err => {
401+ // if there's an error, check if it's because the transaction was
402+ // already confirmed prior to sending.
403+ // this is really only the case in local dev environments.
404+ const txHash = web3.utils.keccak256(tx);
405+ console.log(err.message);
406+ if (err.message.slice(0,54) ===
407+ "Returned error: the tx doesn't have the correct nonce.") {
408+ console.log('nonce error, awaiting confirm', err.message.slice(55));
409+ //TODO max wait time before assume failure?
410+ let res = await waitForTransactionConfirm(web3, txHash);
411+ if (res) resolve();
412+ else reject(new Error('Unexpected tx failure'));
413+ } else {
414+ const confirmed = await isTransactionConfirmed(web3, txHash);
415+ console.log('error, but maybe confirmed:', confirmed);
416+ if (confirmed) resolve();
417+ else reject(err);
418+ }
419+ });
420+ });
421+ }));
422+}
423+
424+function askForFunding(address, amount, current, updateProgress) {
425+ updateProgress({
426+ type: "notify",
427+ value: `Please make sure ${address} has at least ${amount} wei, we'll continue once that's true. Current balance: ${current}. Waiting`
428+ });
429+}
430+
431+export {
432+ generateWallet,
433+ downloadWallet,
434+ startTransactions,
435+ INVITE_STAGES,
436+ WALLET_STATES,
437+ TRANSACTION_STATES
438+}
src/bridge/lib/router.js+4−18
@@ -4,10 +4,7 @@ import React from 'react';
44
55import Landing from '../views/Landing'
66
7−import InviteLogin from '../views/InviteLogin'
8−import InviteWallet from '../views/InviteWallet'
9−import InviteVerify from '../views/InviteVerify.js'
10−import InviteTransactions from '../views/InviteTransactions.js'
7+import InviteTicket from '../views/InviteTicket'
118
129import InvitesSend from '../views/InvitesSend.js'
1310import InvitesManage from '../views/InvitesManage.js'
@@ -45,9 +42,7 @@ import { BRIDGE_ERROR } from './error'
4542const ROUTE_NAMES = {
4643 DEFAULT: Symbol('DEFAULT'),
4744 //
48− INVITE_LOGIN: Symbol('INVITE_LOGIN'),
49− INVITE_WALLET: Symbol('INVITE_WALLET'),
50− INVITE_VERIFY: Symbol('INVITE_VERIFY'),
45+ INVITE_TICKET: Symbol('INVITE_TICKET'),
5146 INVITE_TRANSACTIONS: Symbol('INVITE_TRANSACTIONS'),
5247 INVITES_SEND: Symbol('INVITES_SEND'),
5348 INVITES_MANAGE: Symbol('INVITES_MANAGE'),
@@ -82,10 +77,7 @@ const createRoutes = () => {
8277 const routes = {}
8378 routes[ROUTE_NAMES.DEFAULT] = Landing
8479 //
85− routes[ROUTE_NAMES.INVITE_LOGIN] = InviteLogin
86− routes[ROUTE_NAMES.INVITE_WALLET] = InviteWallet
87− routes[ROUTE_NAMES.INVITE_VERIFY] = InviteVerify
88− routes[ROUTE_NAMES.INVITE_TRANSACTIONS] = InviteTransactions
80+ routes[ROUTE_NAMES.INVITE_TICKET] = InviteTicket
8981 routes[ROUTE_NAMES.INVITES_SEND] = InvitesSend
9082 routes[ROUTE_NAMES.INVITES_MANAGE] = InvitesManage
9183 //
@@ -124,15 +116,9 @@ const renderRoute = (props, route) => {
124116 route === ROUTE_NAMES.LANDING
125117 ? 'Bridge'
126118
127− : route === ROUTE_NAMES.INVITE_LOGIN
119+ : route === ROUTE_NAMES.INVITE_TICKET
128120 ? 'Invite code'
129121
130− : route === ROUTE_NAMES.INVITE_WALLET
131− ? 'Claim invite'
132−
133− : route === ROUTE_NAMES.INVITE_VERIFY
134− ? 'Verify new wallet'
135−
136122 : route === ROUTE_NAMES.INVITE_TRANSACTIONS
137123 ? 'Setting up new wallet'
138124
src/bridge/lib/txn.js−1
@@ -2,7 +2,6 @@ import * as ob from 'urbit-ob'
22import { Just } from 'folktale/maybe'
33import { Ok, Error } from 'folktale/result'
44import Tx from 'ethereumjs-tx'
5−import Web3 from 'web3'
65import { toWei, fromWei, toHex } from 'web3-utils'
76
87import { BRIDGE_ERROR } from '../lib/error'
src/bridge/lib/validators.js+1−24
@@ -5,7 +5,6 @@ import { isValidAddress } from './wallet'
55
66const hexRegExp = /[0-9A-Fa-f]{64}/g
77
8−
98// // Note that this validator also transforms data
109// const validateBase64 = ({ data, pass, error }) => {
1110// if (pass !== true) return { data, pass, error }
@@ -30,10 +29,6 @@ const hexRegExp = /[0-9A-Fa-f]{64}/g
3029// }
3130// };
3231
33−
34−
35−
36−
3732// Validates a bip39 mnemonic
3833const validateMnemonic = m => simpleValidatorWrapper({
3934 prevMessage: m,
@@ -41,7 +36,6 @@ const validateMnemonic = m => simpleValidatorWrapper({
4136 errorMessage: 'This is not a valid mnemonic.'
4237});
4338
44−
4539// Checks an empty field
4640const validateNotEmpty = m => simpleValidatorWrapper({
4741 prevMessage: m,
@@ -49,8 +43,6 @@ const validateNotEmpty = m => simpleValidatorWrapper({
4943 errorMessage: 'This field is required.'
5044});
5145
52−
53−
5446// Checks if a patp is a valid galaxy
5547const validateGalaxy = m => simpleValidatorWrapper({
5648 prevMessage: m,
@@ -66,8 +58,6 @@ const validateGalaxy = m => simpleValidatorWrapper({
6658 errorMessage: 'This is not a valid galaxy'
6759});
6860
69−
70−
7161const validatePoint = m => simpleValidatorWrapper({
7262 prevMessage: m,
7363 validator: d => {
@@ -80,8 +70,6 @@ const validatePoint = m => simpleValidatorWrapper({
8070 errorMessage: 'This is not a valid point'
8171});
8272
83−
84−
8573const validateTicket = m => simpleValidatorWrapper({
8674 prevMessage: m,
8775 validator: d => {
@@ -94,7 +82,6 @@ const validateTicket = m => simpleValidatorWrapper({
9482 errorMessage: 'This is not a valid ticket'
9583});
9684
97−
9885const validateShard = m => simpleValidatorWrapper({
9986 prevMessage: m,
10087 validator: d => {
@@ -107,29 +94,24 @@ const validateShard = m => simpleValidatorWrapper({
10794 errorMessage: 'This is not a valid shard'
10895});
10996
110−
11197const validateLength = (m, l) => simpleValidatorWrapper({
11298 prevMessage: m,
11399 validator: d => d.length === l,
114100 errorMessage: 'This is of an invalid length'
115101});
116102
117−
118103const validateNetworkKey = m => simpleValidatorWrapper({
119104 prevMessage: m,
120105 validator: d => hexRegExp.test(d),
121106 errorMessage: 'This is not a valid network key'
122107});
123108
124−
125109const validateNetworkSeed = m => simpleValidatorWrapper({
126110 prevMessage: m,
127111 validator: d => hexRegExp.test(d),
128112 errorMessage: 'This is not a valid network seed'
129113});
130114
131−
132−
133115// Checks if a string is a valid ethereum address
134116const validateEthereumAddress = m => simpleValidatorWrapper({
135117 prevMessage: m,
@@ -137,10 +119,6 @@ const validateEthereumAddress = m => simpleValidatorWrapper({
137119 errorMessage: 'This is not a valid Ethereum address'
138120});
139121
140−
141−
142−
143−
144122// Wraps single validation functions in a controlled and predictable way.
145123const simpleValidatorWrapper = config => {
146124
@@ -172,11 +150,10 @@ const newMessage = (data, pass, error) => ({
172150 error,
173151})
174152
175−
176−
177153export {
178154 // validateBase64,
179155 // validateJSON,
156+ simpleValidatorWrapper,
180157 validateMnemonic,
181158 validateNotEmpty,
182159 validateGalaxy,
src/bridge/style/button.css+10
@@ -54,6 +54,10 @@ button[prop-type='solid'][prop-color='green']:focus {
5454 box-shadow: 0 0 0 3px #C5FBE7;
5555}
5656
57+button[prop-type='solid'][prop-color='green']:disabled {
58+ background-color: #C5FBE7;
59+}
60+
5761button[prop-type='solid'][prop-color='yellow'] {
5862 background-color: #F8C134;
5963 border-color: #F8C134;
@@ -64,6 +68,12 @@ button[prop-type='solid'][prop-color='yellow']:focus {
6468 box-shadow: 0 0 0 3px #FFEFC5;
6569}
6670
71+button[prop-type='solid'][prop-color='black'] {
72+ background-color: black;
73+ border-color: black;
74+ color: white;
75+}
76+
6777button[prop-type='solid'][prop-size*='sm'] {
6878 height: 6rem;
6979 padding: 0 2rem 0 2rem;
src/bridge/style/extend.css+118
@@ -212,6 +212,14 @@ footer,
212212 font-size: 8rem;
213213}
214214
215+.lh-6 {
216+ line-height: 6rem;
217+}
218+
219+.lh-8 {
220+ line-height: 8rem;
221+}
222+
215223.ls-1 > * {
216224 padding-bottom: 1rem !important;
217225}
@@ -478,10 +486,120 @@ code {
478486 margin-bottom: 4rem;
479487}
480488
489+.block {
490+ display: block;
491+}
492+
493+.flex-wrap {
494+ flex-wrap: wrap;
495+}
496+
481497.full-width {
482498 width: 100%;
483499}
484500
501+.overflow-hidden {
502+ overflow: hidden;
503+}
504+
485505.z-1 {
486506 z-index: 1
487507}
508+
509+.break-word {
510+ word-wrap: break-word;
511+}
512+
513+/**
514+ Passport styles
515+**/
516+
517+.passport {
518+ padding: 24px;
519+}
520+
521+.passport .text-sm {
522+ font-size: 3rem;
523+ line-height: 6rem;
524+}
525+
526+.passport-filled {
527+ background-color: #C5FBE7;
528+}
529+
530+.passport-border {
531+ border: 1px solid #C5FBE7;
532+}
533+
534+.passport-block {
535+ width: 8px;
536+ height: 8px;
537+}
538+
539+.passport-label {
540+ font-size: 12px;
541+ line-height: 24px;
542+ font-weight: 700;
543+ font-family: "Inter UI";
544+ color: #7f7f7f;
545+}
546+
547+.passport-value {
548+ font-size: 12px;
549+ line-height: 24px;
550+ font-family: "Source Code Pro";
551+ color: #7f7f7f;
552+}
553+
554+.passport-censor {
555+ /* min-width: 350px; */
556+ font-size: 48px;
557+}
558+
559+.passport-metadata .passport-label {
560+ line-height: 12px;
561+}
562+
563+.passport-point {
564+ font-size: 16px;
565+ line-height: 24px;
566+}
567+
568+.passport-sigil-blank {
569+ width: 128px;
570+ height: 128px;
571+
572+ background-color: black;
573+}
574+
575+.passport-progress {
576+ position: relative;
577+ height: 4px;
578+ width: 100%;
579+
580+ background-color: #E6E6E6;
581+}
582+
583+.passport-progress-filled {
584+ position: absolute;
585+ height: 4px;
586+ width: 0%;
587+
588+ z-index: 1;
589+ top: 0;
590+ left: 0;
591+
592+ background-color: #2AA779;
593+ transition: width 3s ease;
594+}
595+
596+.passport-wallet-progress {
597+ height: 4px;
598+ background-color: #C5FBE7;
599+}
600+
601+.passport-wallet-progress-fill {
602+ height: 4px;
603+ width: 100%;
604+ background-color: #2AA779;
605+}
src/bridge/views/InviteLogin.jsdeleted−106
@@ -1,106 +0,0 @@
1−import { Just, Nothing } from 'folktale/maybe'
2−import React from 'react'
3−import { Button } from '../components/Base'
4−import { InnerLabel, ValidatedSigil, PointInput, TicketInput } from '../components/Base'
5−import { Row, Col, H1, P } from '../components/Base'
6−import * as kg from '../../../node_modules/urbit-key-generation/dist/index'
7−import * as ob from 'urbit-ob'
8−
9−import { ROUTE_NAMES } from '../lib/router'
10−import { DEFAULT_HD_PATH, urbitWalletFromTicket } from '../lib/wallet'
11−
12−const placeholder = (len) => {
13− let bytes = window.crypto.getRandomValues(new Uint8Array(len))
14− let hex = bytes.reduce((acc, byt) =>
15− acc + byt.toString(16).padStart(2, '0'),
16− ''
17− )
18− return ob.hex2patq(hex)
19−}
20−
21−class InviteLogin extends React.Component {
22−
23− constructor(props) {
24− super(props)
25−
26− this.state = {
27− ticket: '',
28− pointName: '',
29− isUnlocking: false
30− }
31−
32− this.pointPlaceholder = placeholder(4)
33− this.ticketPlaceholder = placeholder(12)
34−
35− this.handleTicketInput = this.handleTicketInput.bind(this)
36− }
37−
38− handleTicketInput(ticket) {
39− this.setState({ ticket })
40− }
41−
42− async walletFromTicket(ticket, pointName) {
43− this.setState({
44− isUnlocking: true
45− });
46−
47− //NOTE ~zod because tmp wallet
48− const uhdw = await urbitWalletFromTicket(ticket, '~zod');
49− this.props.setUrbitWallet(Just(uhdw));
50−
51− this.setState({
52− isUnlocking: false
53− });
54− this.props.pushRoute(ROUTE_NAMES.INVITE_WALLET);
55− }
56−
57− render() {
58− const { wallet } = this.props
59− const { ticket, pointName } = this.state
60−
61− const phTick = this.ticketPlaceholder
62−
63− return (
64− <Row>
65− <Col>
66− <H1>{ 'Onboard' }</H1>
67−
68− <P>
69− { `Please enter your invite code.` }
70− </P>
71−
72− <TicketInput
73− className='mono mt-8'
74− prop-size='lg'
75− prop-format='innerLabel'
76− type='text'
77− autoFocus
78− name='invite'
79− placeholder={ `e.g. ${phTick}` }
80− value={ ticket }
81− onChange={ this.handleTicketInput }>
82− <InnerLabel>{ 'Invite code' }</InnerLabel>
83− </TicketInput>
84−
85− <Button
86− className={'mt-8'}
87− prop-size={'lg wide'}
88− disabled={this.state.isUnlocking}
89− // prop-color={this.buttonTriState(wallet)}
90− onClick={() => this.walletFromTicket(ticket, pointName)}>
91−
92− <span className="relative">
93− {this.state.isUnlocking &&
94− <span className="btn-spinner"></span>
95− }
96− {'Continue →'}
97− </span>
98− </Button>
99−
100− </Col>
101− </Row>
102− )
103− }
104−}
105−
106−export default InviteLogin
src/bridge/views/InviteTicket.jsnew file+374
@@ -0,0 +1,374 @@
1+import { Just, Nothing } from 'folktale/maybe'
2+import React from 'react'
3+import { InnerLabel, Warning, TicketInput,
4+ VerifyTicketInput, Button, Row, Col, Passport } from '../components/Base'
5+import * as azimuth from 'azimuth-js'
6+
7+import { randomPatq } from '../lib/lib'
8+import { ROUTE_NAMES } from '../lib/router'
9+import { DEFAULT_HD_PATH, urbitWalletFromTicket,
10+ walletFromMnemonic, addressFromSecp256k1Public } from '../lib/wallet'
11+import { BRIDGE_ERROR } from '../lib/error'
12+import { INVITE_STAGES, WALLET_STATES, TRANSACTION_STATES } from '../lib/invite'
13+import { generateWallet, startTransactions } from '../lib/invite'
14+
15+class InviteTicket extends React.Component {
16+
17+ constructor(props) {
18+ super(props)
19+
20+ this.state = {
21+ inviteTicket: '',
22+ inviteWallet: Nothing(),
23+ verifyTicket: '',
24+ realTicket: '',
25+ realPoint: Nothing(),
26+ realWallet: Nothing(),
27+ walletReady: false,
28+ walletStates: [],
29+ errors: [],
30+ stage: INVITE_STAGES.INVITE_LOGIN,
31+ transactionProgress: TRANSACTION_STATES.GENERATING
32+ }
33+
34+ this.pointPlaceholder = randomPatq(4)
35+ this.ticketPlaceholder = randomPatq(12)
36+
37+ this.handleInviteTicketInput = this.handleInviteTicketInput.bind(this)
38+ this.handleVerifyTicketInput = this.handleVerifyTicketInput.bind(this)
39+ this.updateProgress = this.updateProgress.bind(this)
40+ this.pushWalletState = this.pushWalletState.bind(this)
41+ this.navigateLogin = this.navigateLogin.bind(this)
42+ }
43+
44+ pushWalletState(walletState) {
45+ this.setState({
46+ walletStates: this.state.walletStates.concat(walletState)
47+ })
48+ }
49+
50+ navigateLogin() {
51+ this.props.popRoute()
52+ this.props.pushRoute(ROUTE_NAMES.NETWORK)
53+ }
54+
55+ handleInviteTicketInput(inviteTicket) {
56+ this.setState({ inviteTicket })
57+ }
58+
59+ handleVerifyTicketInput(verifyTicket) {
60+ this.setState({ verifyTicket })
61+ }
62+
63+ updateTransactionProgress(transactionProgress) {
64+ this.setState({ transactionProgress })
65+ }
66+
67+ async unlockInviteWallet(inviteTicket) {
68+ this.setState({
69+ walletStates: this.state.walletStates.concat(WALLET_STATES.UNLOCKING),
70+ stage: INVITE_STAGES.INVITE_WALLET
71+ });
72+
73+ //NOTE ~zod because tmp wallet
74+ const uhdw = await urbitWalletFromTicket(inviteTicket, '~zod');
75+ const mnemonic = uhdw.ownership.seed;
76+ const inviteWallet = walletFromMnemonic(
77+ mnemonic,
78+ DEFAULT_HD_PATH,
79+ uhdw.meta.passphrase
80+ ).matchWith({
81+ Just: w => w.value,
82+ Nothing: () => {
83+ throw BRIDGE_ERROR.MISSING_WALLET
84+ }
85+ });
86+
87+ const addr = addressFromSecp256k1Public(inviteWallet.publicKey);
88+
89+ const ctrcs = this.props.contracts.matchWith({
90+ Just: ctrcs => ctrcs.value,
91+ Nothing: () => {
92+ throw BRIDGE_ERROR.MISSING_CONTRACTS
93+ }
94+ })
95+
96+ const incoming = await azimuth.azimuth.getTransferringFor(ctrcs, addr)
97+
98+ this.setState({
99+ walletStates: this.state.walletStates.concat(WALLET_STATES.GENERATING)
100+ })
101+
102+ let realPoint = Nothing()
103+ let realWallet = Nothing()
104+ let errors = []
105+
106+ if (incoming.length > 0) {
107+ let pointNum = parseInt(incoming[0], 10)
108+ realPoint = Just(pointNum)
109+
110+ this.setState({
111+ realPoint
112+ })
113+
114+ let genWallet = await generateWallet(pointNum)
115+
116+ realWallet = Just(genWallet)
117+ if (incoming.length > 1) {
118+ //TODO notify user "...and others / ticket reusable"
119+ }
120+ } else {
121+ errors = ["Invite code has no claimable ship. Check your invite code and try again."]
122+ }
123+
124+ this.setState({
125+ realWallet,
126+ inviteWallet: Just(inviteWallet),
127+ // dev mode: uncomment this out to autofill verified ticket
128+ verifyTicket: !Nothing.hasInstance(realWallet) ? realWallet.value.ticket : '',
129+ walletStates: this.state.walletStates.concat(WALLET_STATES.RENDERING),
130+ errors
131+ })
132+ }
133+
134+ getTicketElement() {
135+ const phTick = this.ticketPlaceholder
136+
137+ let isLogin = this.state.stage === INVITE_STAGES.INVITE_LOGIN
138+ let isVerify = this.state.stage === INVITE_STAGES.INVITE_VERIFY
139+
140+ if (isLogin || isVerify) {
141+
142+ let value = isLogin ? this.state.inviteTicket : this.state.verifyTicket
143+ let handler = isLogin ? this.handleInviteTicketInput : this.handleVerifyTicketInput
144+ let label = isLogin ? 'Activation code' : 'ticket'
145+
146+ // if realWallet is Just, then VerifyTicketInput appropriate contents; if Nothing, remain Nothing
147+ const Verified = this.state.realWallet.map(realWallet =>
148+ VerifyTicketInput(realWallet.ticket)
149+ )
150+
151+ // Verified.getOrElse(TicketInput) falls back to TicketInput if Verified is Nothing
152+ const TicketElem =
153+ isVerify
154+ ? Verified.getOrElse(TicketInput)
155+ : TicketInput
156+
157+ return (
158+ <TicketElem
159+ className='mono mt-8'
160+ prop-size='md'
161+ prop-format='innerLabel'
162+ type='text'
163+ autoFocus
164+ name='invite'
165+ placeholder={ `e.g. ${phTick}` }
166+ value={ value }
167+ onChange={ handler }>
168+ <InnerLabel>{ label }</InnerLabel>
169+ </TicketElem>
170+ )
171+ } else {
172+ return null
173+ }
174+ }
175+
176+ getStageText() {
177+ const { stage } = this.state
178+
179+ let progressWidth = this.state.transactionProgress.pct;
180+
181+ const stageText = stage === INVITE_STAGES.INVITE_LOGIN
182+ ? <h1 className="text-700 mt-15">Welcome</h1>
183+ : stage === INVITE_STAGES.INVITE_WALLET
184+ ? (
185+ <div>
186+ <h1 className="fs-6 lh-8 mb-3">Passport</h1>
187+ <p>A passport is your digital identity. You will use your passport to access your true computer, send payments, and administer your identity. So naturally, you must keep this secure.</p>
188+ <p>After you’ve downloaded your passport, back up the ticket manually or store on a trusted device.</p>
189+ </div>
190+ )
191+ : stage === INVITE_STAGES.INVITE_VERIFY
192+ ? (
193+ <div>
194+ <h1 className="fs-6 lh-8 mb-3">Verify Passport</h1>
195+ <p>Afer you download your passport, verify your custody. Your passport should be a folder of image files. One of them is your Master Ticket. Open it and enter the 4 word phrase below (with hyphens).</p>
196+ </div>
197+ ) : INVITE_STAGES.INVITE_TRANSACTIONS
198+ ? this.state.transactionProgress.label === TRANSACTION_STATES.DONE.label
199+ ? (
200+ <div>
201+ <h1 className="fs-6 lh-8 mb-3">
202+ <span>Success</span>
203+ <span className="ml-4 green">✓</span>
204+ </h1>
205+ <p>Transferring to Bridge...</p>
206+ </div>
207+ )
208+ : (
209+ <div>
210+ <h1 className="fs-6 lh-8 mb-3">Submitting</h1>
211+ <p className="mt-4 mb-4">This step can take up to five minutes. Please do not leave this page until the transactions are complete.</p>
212+ <div className="passport-progress mb-2">
213+ <div className="passport-progress-filled" style={{width: progressWidth}}></div>
214+ </div>
215+ <div className="flex justify-between">
216+ <div className="text-mono text-sm lh-6 green-dark">{this.state.transactionProgress.label}</div>
217+ </div>
218+ </div>
219+ )
220+ : null
221+
222+ return stageText
223+ }
224+
225+ updateProgress(notification) {
226+ let newState = notification.type === "progress"
227+ ? { transactionProgress: notification.value }
228+ : notification.type === "notify"
229+ ? { errors: [notification.value] }
230+ : {}
231+
232+ this.setState(newState)
233+ }
234+
235+ getContinueButton() {
236+ const { realPoint, realWallet, inviteWallet, stage, walletStates,
237+ verifyTicket, inviteTicket } = this.state
238+ const { web3, contracts, setUrbitWallet } = this.props
239+
240+ const btnColor = this.state.walletStates.includes(WALLET_STATES.PAPER_READY)
241+ ? 'green'
242+ : 'black'
243+
244+ const clickHandler = stage === INVITE_STAGES.INVITE_LOGIN
245+ ? () => {
246+ this.unlockInviteWallet(inviteTicket)
247+ }
248+ : stage === INVITE_STAGES.INVITE_WALLET
249+ ? () => {
250+ this.setState({
251+ stage: INVITE_STAGES.INVITE_VERIFY
252+ })
253+ }
254+ : stage === INVITE_STAGES.INVITE_VERIFY
255+ ? () => {
256+ this.setState({
257+ stage: INVITE_STAGES.INVITE_TRANSACTIONS,
258+ walletStates: this.state.walletStates.concat(WALLET_STATES.TRANSACTIONS)
259+ })
260+
261+ startTransactions({
262+ inviteWalletM: inviteWallet,
263+ realWalletM: realWallet,
264+ realPointM: realPoint,
265+ realTicket: verifyTicket,
266+ web3M: web3,
267+ contractsM: contracts,
268+ setUrbitWallet,
269+ updateProgress: this.updateProgress
270+ })
271+ .then(() => {
272+ setTimeout(() => {
273+ this.props.popRoute()
274+ }, 5000)
275+ })
276+ }
277+ : null
278+
279+ const ticketMatches = this.state.realWallet.matchWith({
280+ Just: wal => wal.value.ticket === this.state.verifyTicket,
281+ Nothing: () => false
282+ })
283+
284+ const continueReady = stage === INVITE_STAGES.INVITE_WALLET
285+ ? walletStates.includes(WALLET_STATES.DOWNLOADED)
286+ : stage === INVITE_STAGES.INVITE_VERIFY
287+ ? ticketMatches
288+ : true
289+
290+ const buttonElem = stage === INVITE_STAGES.INVITE_TRANSACTIONS
291+ ? null
292+ : (
293+ <Button
294+ className={'mt-4'}
295+ prop-size={'lg'}
296+ prop-color={btnColor}
297+ disabled={!continueReady}
298+ onClick={clickHandler}
299+ >
300+ { 'Continue →' }
301+ </Button>
302+ )
303+
304+ return buttonElem
305+ }
306+
307+ getStageDisplay() {
308+ const { stage } = this.state
309+
310+ if (stage === INVITE_STAGES.INVITE_LOGIN) return null
311+
312+ return (
313+ <div className="mt-6 mb-8">
314+ <span className={`fs-35 mr-3 ${stage !== INVITE_STAGES.INVITE_WALLET && 'gray-20'}`}>1 Passport</span>
315+ <span className={`fs-35 mr-3 ${stage !== INVITE_STAGES.INVITE_VERIFY && 'gray-20'}`}>2 Verify</span>
316+ <span className={`fs-35 mr-3 ${stage !== INVITE_STAGES.INVITE_TRANSACTIONS && 'gray-20'}`}>3 Invite</span>
317+ </div>
318+ )
319+ }
320+
321+ getErrors() {
322+ if (this.state.errors.length === 0) return null
323+
324+ let errorElems = this.state.errors.map(e => <span>{e}</span>)
325+
326+ return (
327+ <Warning>
328+ <h3 className={'mb-2'}>{'Warning'}</h3>
329+ { errorElems }
330+ </Warning>
331+ )
332+ }
333+
334+ render() {
335+ let stageDisplay = this.getStageDisplay()
336+ let stageText = this.getStageText()
337+ let ticketElem = this.getTicketElement()
338+ let continueButton = this.getContinueButton()
339+ let errorDisplay = this.getErrors()
340+
341+ return (
342+ <Row>
343+ <Col className="col-md-4 mt-">
344+ {stageDisplay}
345+ {errorDisplay}
346+ {stageText}
347+ {ticketElem}
348+ {continueButton}
349+
350+ { this.state.stage === INVITE_STAGES.INVITE_LOGIN &&
351+ <Button
352+ prop-type={`link`}
353+ prop-size={`sm`}
354+ className={`block mt-4`}
355+ onClick={this.navigateLogin} >
356+ {`Log in`}
357+ </Button>
358+ }
359+
360+ </Col>
361+ <Col className="col-md-offset-3 col-md-5 mt-18">
362+ <Passport
363+ point={this.state.realPoint}
364+ wallet={this.state.realWallet}
365+ walletStates={this.state.walletStates}
366+ pushWalletState={this.pushWalletState}
367+ />
368+ </Col>
369+ </Row>
370+ )
371+ }
372+}
373+
374+export default InviteTicket
src/bridge/views/InviteTransactions.jsdeleted−346
@@ -1,346 +0,0 @@
1−import { Just, Nothing } from 'folktale/maybe'
2−import React from 'react'
3−import { pour } from 'sigil-js'
4−import * as ob from 'urbit-ob'
5−import * as azimuth from 'azimuth-js'
6−
7−import {
8− EthereumWallet,
9− addressFromSecp256k1Public,
10− urbitWalletFromTicket,
11− addHexPrefix,
12− WALLET_NAMES
13−} from '../lib/wallet'
14−
15−import ReactSVGComponents from '../components/ReactSVGComponents'
16−import KeysAndMetadata from './Point/KeysAndMetadata'
17−import Actions from './Point/Actions'
18−import { BRIDGE_ERROR } from '../lib/error'
19−import { Row, Col, Warning, Button, H1, H3 } from '../components/Base'
20−
21−// for transaction generation and signing
22−import {
23− signTransaction,
24− sendSignedTransaction,
25− waitForTransactionConfirm,
26− isTransactionConfirmed
27−} from '../lib/txn'
28−import { attemptSeedDerivation, genKey } from '../lib/keys'
29−import * as kg from '../../../node_modules/urbit-key-generation/dist/index'
30−import * as tank from '../lib/tank'
31−import Tx from 'ethereumjs-tx'
32−
33−const PROGRESS_STATES = {
34− GENERATING: 'Generating transactions',
35− SIGNING: 'Signing transactions',
36− FUNDING: 'Funding transactions',
37− CLAIMING: 'Claiming invite',
38− CONFIGURING: 'Configuring planet',
39− CLEANING: 'Cleaning up',
40− DONE: 'Done'
41−}
42−
43−class InviteVerify extends React.Component {
44−
45− constructor(props) {
46− super(props)
47−
48− this.state = {
49− point: this.props.routeData.point,
50− ticket: this.props.routeData.ticket,
51− progress: 'Starting'
52− }
53−
54− this.startTransactions = this.startTransactions.bind(this);
55− }
56−
57− componentDidMount() {
58− //NOTE delayed so that we hang *after* rendering the page
59− this.updateProgress('Generating transactions');
60− setTimeout(this.startTransactions, 100);
61− }
62−
63− componentDidUpdate(prevProps) {
64− //
65− }
66−
67− updateProgress(msg) {
68− this.setState({progress: msg+'...'});
69− }
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() {
311−
312− const { point } = this.state;
313−
314− const name = ob.patp(point);
315− const sigil = pour({
316− patp: name,
317− renderer: ReactSVGComponents,
318− size: 256
319− });
320− let pointOverview = (
321− //TODO Passport display component
322− <>
323− <div className={'mt-12 pt-6'}>
324− { sigil }
325− </div>
326− <H3><code>{ name }</code></H3>
327− </>
328− );
329−
330− return (
331− <Row>
332− <Col>
333−
334− { pointOverview }
335−
336− <p>{ 'Please wait while your wallet and point are prepared for use. This may take up to five minutes.' }</p>
337−
338− <p>{ this.state.progress }</p>
339−
340− </Col>
341− </Row>
342− )
343− }
344−}
345−
346−export default InviteVerify
src/bridge/views/InviteVerify.jsdeleted−106
@@ -1,106 +0,0 @@
1−import { Just, Nothing } from 'folktale/maybe'
2−import React from 'react'
3−import { pour } from 'sigil-js'
4−import * as ob from 'urbit-ob'
5−import * as azimuth from 'azimuth-js'
6−
7−import { randomPatq } from '../lib/lib'
8−import { ROUTE_NAMES } from '../lib/router'
9−
10−import { InnerLabel, TicketInput } from '../components/Base'
11−import ReactSVGComponents from '../components/ReactSVGComponents'
12−import KeysAndMetadata from './Point/KeysAndMetadata'
13−import Actions from './Point/Actions'
14−import { BRIDGE_ERROR } from '../lib/error'
15−import { Row, Col, Warning, Button, H1, H3 } from '../components/Base'
16−
17−class InviteVerify extends React.Component {
18−
19− constructor(props) {
20− super(props)
21−
22− this.state = {
23− point: this.props.routeData.point,
24− realTicket: this.props.routeData.ticket
25− }
26−
27− this.ticketPlaceholder = randomPatq(8);
28− this.handleTicketInput = this.handleTicketInput.bind(this);
29− this.proceed = this.proceed.bind(this);
30− }
31−
32− componentDidMount() {
33− //
34− }
35−
36− componentDidUpdate(prevProps) {
37− //
38− }
39−
40− handleTicketInput(ticket) {
41− this.setState({ ticket })
42− }
43−
44− proceed() {
45− this.props.pushRoute(ROUTE_NAMES.INVITE_TRANSACTIONS, {
46− ticket: this.state.realTicket,
47− point: this.state.point
48− });
49− }
50−
51− render() {
52−
53− const { point } = this.state;
54−
55− const name = ob.patp(point);
56− const sigil = pour({
57− patp: name,
58− renderer: ReactSVGComponents,
59− size: 256
60− });
61− let pointOverview = (
62− //TODO Passport display component
63− <>
64− <div className={'mt-12 pt-6'}>
65− { sigil }
66− </div>
67− <H3><code>{ name }</code></H3>
68− </>
69− );
70−
71− return (
72− <Row>
73− <Col>
74−
75− { pointOverview }
76−
77− <p>{`Please input the master ticket for ${point} that you downloaded in the previous step.`}</p>
78−
79− <TicketInput
80− className='mono mt-8'
81− prop-size='lg'
82− prop-format='innerLabel'
83− type='password'
84− name='ticket'
85− placeholder={ `e.g. ${this.ticketPlaceholder}` }
86− value={ this.state.ticket }
87− onChange={ this.handleTicketInput }>
88− <InnerLabel>{ 'Ticket' }</InnerLabel>
89− </TicketInput>
90−
91− <Button
92− className={'mt-4'}
93− prop-size={'xl wide'}
94− disabled={(this.state.ticket !== this.state.realTicket)}
95− onClick={this.proceed}
96− >
97− { 'Continue →' }
98− </Button>
99−
100− </Col>
101− </Row>
102− )
103− }
104−}
105−
106−export default InviteVerify
src/bridge/views/InviteWallet.jsdeleted−226
@@ -1,226 +0,0 @@
1−import { Just, Nothing } from 'folktale/maybe'
2−import React from 'react'
3−import { pour } from 'sigil-js'
4−import * as ob from 'urbit-ob'
5−import * as azimuth from 'azimuth-js'
6−
7−import { addressFromSecp256k1Public, EthereumWallet } from '../lib/wallet'
8−import { ROUTE_NAMES } from '../lib/router'
9−
10−import PointList from '../components/PointList'
11−import ReactSVGComponents from '../components/ReactSVGComponents'
12−import KeysAndMetadata from './Point/KeysAndMetadata'
13−import Actions from './Point/Actions'
14−import { BRIDGE_ERROR } from '../lib/error'
15−import { Row, Col, Warning, Button, H1, H3 } from '../components/Base'
16−
17−// for wallet generation
18−import * as wg from '../../walletgen/lib/lib'
19−import PaperCollateralRenderer from 'PaperCollateralRenderer'
20−import JSZip from 'jszip'
21−import saveAs from 'file-saver'
22−
23−class InviteWallet extends React.Component {
24−
25− constructor(props) {
26− super(props)
27−
28− this.state = {
29− loaded: false,
30− point: Nothing(),
31− wallet: Nothing(),
32− paper: Nothing(),
33− downloaded: false
34− }
35−
36− this.download = this.download.bind(this)
37− this.proceed = this.proceed.bind(this)
38− }
39−
40− componentDidMount() {
41− const { web3, wallet, contracts, setPointCursor } = this.props
42−
43− web3.chain(_ =>
44− contracts.chain(ctrcs =>
45− wallet.chain(async wal => {
46− const addr = addressFromSecp256k1Public(wal.publicKey);
47− console.log('fetching incoming for', addr)
48− const incoming = await azimuth.azimuth.getTransferringFor(ctrcs, addr)
49− console.log('incoming', incoming)
50−
51− let point = Nothing();
52− if (incoming.length > 0) {
53− let pointNum = parseInt(incoming[0]);
54− point = Just(pointNum);
55− this.generateWallet(pointNum);
56− if (incoming.length > 1) {
57− //TODO notify user "...and others / ticket reusable"
58− }
59− }
60− this.setState({loaded: true, point: point})
61− })))
62− }
63−
64− componentDidUpdate(prevProps) {
65− //
66− }
67−
68− async generateWallet(point) {
69− const ticket = await wg.makeTicket(point);
70− const wallet = await wg.generateWallet(point, ticket);
71− this.setState({ wallet: Just(wallet) });
72− }
73−
74− download() {
75− const zip = new JSZip();
76− //TODO the categories here aren't explained in bridge at all...
77−
78− const bin0 = this.state.paper.filter(item => item.bin === '0');
79− const bin1 = this.state.paper.filter(item => item.bin === '1');
80− const bin2 = this.state.paper.filter(item => item.bin === '2');
81− const bin3 = this.state.paper.filter(item => item.bin === '3');
82− const bin4 = this.state.paper.filter(item => item.bin === '4');
83−
84− const bin0Folder = zip.folder("0. Public");
85− const bin1Folder = zip.folder("1. Very High Friction Custody");
86− const bin2Folder = zip.folder("2. High Friction Custody");
87− const bin3Folder = zip.folder("3. Medium Friction Custody");
88− const bin4Folder = zip.folder("4. Low Friction Custody");
89−
90− bin0.forEach(item => bin0Folder.file(`${item.pageTitle}.png`, item.png))
91− bin1.forEach(item => bin1Folder.file(`${item.pageTitle}.png`, item.png));
92− bin2.forEach(item => bin2Folder.file(`${item.pageTitle}.png`, item.png));
93− bin3.forEach(item => bin3Folder.file(`${item.pageTitle}.png`, item.png));
94− bin4.forEach(item => bin4Folder.file(`${item.pageTitle}.png`, item.png));
95−
96− zip.generateAsync({type:"blob"}).then((content) => {
97− saveAs(content, 'urbit-wallet.zip');
98− this.setState({ downloaded: true });
99− });
100− }
101−
102− proceed() {
103− this.props.pushRoute(ROUTE_NAMES.INVITE_VERIFY, {
104− ticket: this.state.wallet.value.ticket,
105− point: parseInt(this.state.point.value)
106− });
107− }
108−
109− render() {
110−
111− const { loaded, point, wallet, paper, downloaded } = this.state;
112−
113− let loading;
114− if (!loaded) {
115− loading = ( <div>{ 'Loading...' }</div> );
116− }
117−
118− let balanceWarning;
119− if (loaded && Nothing.hasInstance(point)) {
120− balanceWarning = (
121− <Warning>
122− <h3 className={'mb-2'}>{'Warning'}</h3>
123− {
124− 'This invite has no claimable balance.'
125− }
126− </Warning>
127− );
128− }
129−
130− let pointOverview;
131− if (loaded && Just.hasInstance(point)) {
132− const name = ob.patp(point.value);
133− const sigil = pour({
134− patp: name,
135− renderer: ReactSVGComponents,
136− size: 256
137− });
138− pointOverview = (
139− //TODO Passport display component
140− <>
141− <div className={'mt-12 pt-6'}>
142− { sigil }
143− </div>
144− <H3><code>{ name }</code></H3>
145− </>
146− );
147− }
148−
149− let downloadButtonContents = 'Download wallet';
150− if ( Nothing.hasInstance(this.state.wallet) ||
151− Nothing.hasInstance(this.state.paper) ) {
152− downloadButtonContents = (
153− <>
154− <span className="btn-spinner"></span>
155− {'Generating wallet...'}
156− </>
157− );
158− }
159−
160− let downloadButton;
161− let nextButton;
162− if (loaded) {
163− downloadButton = (
164− <Button
165− className={'mt-8'}
166− prop-size={'lg wide'}
167− disabled={( Nothing.hasInstance(this.state.wallet) ||
168− Nothing.hasInstance(this.state.paper) )}
169− onClick={this.download}
170− >
171− <span className="relative">
172− { downloadButtonContents }
173− </span>
174− </Button>
175− );
176−
177− nextButton = (
178− <Button
179− className={'mt-8'}
180− prop-size={'lg wide'}
181− disabled={!this.state.downloaded}
182− onClick={this.proceed}
183− >
184− {'Proceed'}
185− </Button>
186− );
187− }
188−
189− let paperRenderer;
190− if ( Just.hasInstance(this.state.wallet) &&
191− Nothing.hasInstance(this.state.paper) ) {
192− console.log('rendering for', point.value)
193− paperRenderer = (
194− <PaperCollateralRenderer
195− wallet={{[point.value]: wallet.value}}
196− className={'extremely-hidden'}
197− callback={data => {
198− console.log('got paper', data);
199− this.setState({isRendering: false, paper: data})
200− }}
201− mode={'REGISTRATION'} />
202− );
203− }
204−
205− return (
206− <Row>
207− <Col>
208−
209− { loading }
210− { balanceWarning }
211− { pointOverview }
212−
213− <p>{'TODO copy. you need a wallet to receive the point into. download it below. also explanation of wallet files/storage'}</p>
214−
215− { downloadButton }
216− { nextButton }
217−
218− { paperRenderer }
219−
220− </Col>
221− </Row>
222− )
223− }
224−}
225−
226−export default InviteWallet
src/bridge/views/InvitesManage.js+2−15
@@ -1,24 +1,11 @@
11import { Just, Nothing } from 'folktale/maybe'
22import React from 'react'
3−import { pour } from 'sigil-js'
43import * as ob from 'urbit-ob'
54import * as azimuth from 'azimuth-js'
65
7−import {
8− EthereumWallet,
9− addressFromSecp256k1Public,
10− urbitWalletFromTicket,
11− addHexPrefix,
12− WALLET_NAMES
13−} from '../lib/wallet'
14−
15−import ReactSVGComponents from '../components/ReactSVGComponents'
16−import KeysAndMetadata from './Point/KeysAndMetadata'
17−import Actions from './Point/Actions'
186import { BRIDGE_ERROR } from '../lib/error'
19−import { Row, Col, Warning, Button, H1, H3, Input,
20− PointInput, InnerLabel, ValidatedSigil
21−} from '../components/Base'
7+import { Row, Col, Warning, Input,
8+ PointInput, InnerLabel, ValidatedSigil } from '../components/Base'
229import StatelessTransaction from '../components/StatelessTransaction'
2310
2411
src/bridge/views/InvitesSend.js+2−27
@@ -1,40 +1,16 @@
11import { Just, Nothing } from 'folktale/maybe'
22import React from 'react'
3−import { pour } from 'sigil-js'
43import * as ob from 'urbit-ob'
54import * as azimuth from 'azimuth-js'
65
7−import {
8− EthereumWallet,
9− addressFromSecp256k1Public,
10− urbitWalletFromTicket,
11− addHexPrefix,
12− WALLET_NAMES
13−} from '../lib/wallet'
14−
15−import ReactSVGComponents from '../components/ReactSVGComponents'
16−import KeysAndMetadata from './Point/KeysAndMetadata'
17−import Actions from './Point/Actions'
186import { BRIDGE_ERROR } from '../lib/error'
197import { sendMail } from '../lib/inviteMail'
20−import { Row, Col, Warning, Button, H1, H3,
21− Input, InnerLabel
22−} from '../components/Base'
8+import { Row, Col, Input, InnerLabel } from '../components/Base'
239import StatelessTransaction from '../components/StatelessTransaction'
2410
2511// for wallet generation
2612import * as wg from '../../walletgen/lib/lib'
2713
28−// for transaction generation and signing
29−import {
30− signTransaction,
31− sendSignedTransaction,
32− waitForTransactionConfirm,
33− isTransactionConfirmed
34−} from '../lib/txn'
35−import * as tank from '../lib/tank'
36−import Tx from 'ethereumjs-tx'
37−
3814class InvitesSend extends React.Component {
3915
4016 constructor(props) {
@@ -163,14 +139,13 @@ class InvitesSend extends React.Component {
163139 }
164140
165141 render() {
166−
167142 const inviteStatus = this.state.invitesAvailable.matchWith({
168143 Nothing: () => 'Loading...',
169144 Just: (invites) => {
170145 let res = `${invites.value} invites available`;
171146 return res + this.state.fromPool.matchWith({
172147 Nothing: () => '',
173− Just: (pool) => (pool.value == this.point)
148+ Just: (pool) => (pool.value === this.point)
174149 ? ''
175150 : ` (from ${ob.patp(pool.value)})`
176151 });
src/bridge/views/Landing.js+1−1
@@ -22,7 +22,7 @@ const Landing = (props) =>
2222 prop-type='link'
2323 prop-size='lg'
2424 className={'mb-4 mt-8'}
25− onClick={ () => props.pushRoute(ROUTE_NAMES.INVITE_LOGIN) }
25+ onClick={ () => props.pushRoute(ROUTE_NAMES.INVITE_TICKET) }
2626 >
2727 { 'Claim an invite →' }
2828 </Button>
src/bridge/views/Shards.js+1−1
@@ -14,7 +14,7 @@ import * as kg from '../../../node_modules/urbit-key-generation/dist/index'
1414import * as ob from 'urbit-ob'
1515
1616import { ROUTE_NAMES } from '../lib/router'
17−import { DEFAULT_HD_PATH, urbitWalletFromTicket } from '../lib/wallet'
17+import { urbitWalletFromTicket } from '../lib/wallet'
1818
1919const placeholder = (len) => {
2020 let bytes = window.crypto.getRandomValues(new Uint8Array(len))
src/bridge/views/Ticket.js+1−3
@@ -10,12 +10,10 @@ import {
1010 InputCaption
1111 } from '../components/Base'
1212import { Row, Col, H1, P } from '../components/Base'
13−import * as kg from '../../../node_modules/urbit-key-generation/dist/index'
14−import * as ob from 'urbit-ob'
1513
1614import { randomPatq } from '../lib/lib'
1715import { ROUTE_NAMES } from '../lib/router'
18−import { DEFAULT_HD_PATH, urbitWalletFromTicket } from '../lib/wallet'
16+import { urbitWalletFromTicket } from '../lib/wallet'
1917
2018class Ticket extends React.Component {
2119
src/common/components/AppNavigation.js+1−1
@@ -24,7 +24,7 @@ class AppNavigation extends React.Component {
2424 position: "fixed",
2525 bottom: "10px",
2626 right: "10px",
27− "text-decoration": "underline",
27+ textDecoration: "underline",
2828 cursor: "pointer"
2929 }
3030
src/walletgen/lib/lib.js+1−3
@@ -4,10 +4,8 @@ import * as kg from '../../../node_modules/urbit-key-generation/dist/index'
44import * as more from 'more-entropy'
55import lodash from 'lodash'
66import {
7− MAX_GALAXY, MIN_STAR, MAX_STAR, MIN_PLANET,
7+ MIN_STAR, MIN_PLANET, SEED_ENTROPY_BITS,
88 GALAXY_ENTROPY_BITS, STAR_ENTROPY_BITS, PLANET_ENTROPY_BITS,
9− SEED_ENTROPY_BITS,
10− GEN_STATES
119} from './constants'
1210
1311const SEED_LENGTH_BYTES = SEED_ENTROPY_BITS / 8
src/walletgen/views/Generate.js+1−9
@@ -1,6 +1,4 @@
11import React from 'react'
2−import * as more from 'more-entropy'
3−import lodash from 'lodash'
42import * as sigil from 'sigil-js'
53import ob from 'urbit-ob'
64import classnames from 'classnames'
@@ -11,16 +9,10 @@ import Button from '../components/Button'
119import ReactSVGComponents from '../components/ReactSVGComponents'
1210import PaperCollateralRenderer from 'PaperCollateralRenderer'
1311
14−import { MAX_GALAXY, MIN_STAR, MAX_STAR, MIN_PLANET,
15− GALAXY_ENTROPY_BITS, STAR_ENTROPY_BITS, PLANET_ENTROPY_BITS,
16− SEED_ENTROPY_BITS,
17− GEN_STATES
18− } from '../lib/constants'
12+import { MAX_GALAXY, MIN_STAR, MAX_STAR, GEN_STATES } from '../lib/constants'
1913
20−const SEED_LENGTH_BYTES = SEED_ENTROPY_BITS / 8
2114const NEXT_STEP_NUM = 6;
2215
23−
2416const shipInfo = ship => {
2517 const name = ob.patp(ship)
2618 const sig = sigil.pour({