on master help sign in
Automatically generate new paper wallet in invite flow
35ab548Fang
src/bridge/views/InviteClaim.js+214−71
@@ -1,4 +1,4 @@
1−import Maybe from 'folktale/maybe'
1+import { Just, Nothing } from 'folktale/maybe'
2import React from 'react'
2import React from 'react'
3import { pour } from 'sigil-js'
3import { pour } from 'sigil-js'
4import * as ob from 'urbit-ob'
4import * as ob from 'urbit-ob'
@@ -13,7 +13,22 @@ import Actions from './Point/Actions'
13import { BRIDGE_ERROR } from '../lib/error'
13import { BRIDGE_ERROR } from '../lib/error'
14import { Row, Col, Warning, Button, H1, H3 } from '../components/Base'
14import { Row, Col, Warning, Button, H1, H3 } from '../components/Base'
15
15
16+// for wallet generation
17+import * as kg from '../../../node_modules/urbit-key-generation/dist/index'
18+import * as more from 'more-entropy'
19+import lodash from 'lodash'
20+import PaperCollateralRenderer from 'PaperCollateralRenderer'
21+import JSZip from 'jszip'
22+import saveAs from 'file-saver'
23+import { MAX_GALAXY, MIN_STAR, MAX_STAR, MIN_PLANET,
24+ GALAXY_ENTROPY_BITS, STAR_ENTROPY_BITS, PLANET_ENTROPY_BITS,
25+ SEED_ENTROPY_BITS,
26+ GEN_STATES
27+ } from '../../walletgen/lib/constants'
28+const SEED_LENGTH_BYTES = SEED_ENTROPY_BITS / 8
29+const NEXT_STEP_NUM = 6;
16
30
31+//TODO more like InviteGenerate...
17class InviteClaim extends React.Component {
32class InviteClaim extends React.Component {
18
33
19 constructor(props) {
34 constructor(props) {
@@ -21,10 +36,14 @@ class InviteClaim extends React.Component {
21
36
22 this.state = {
37 this.state = {
23 loaded: false,
38 loaded: false,
24− point: Maybe.Nothing(),
39+ point: Nothing(),
25− newTicket: Maybe.Nothing(),
40+ wallet: Nothing(),
26− isGenerating: true
41+ paper: Nothing(),
42+ downloaded: false
43+ //TODO remove bools in favor of Nothings/Justs
27 }
44 }
45+
46+ this.download = this.download.bind(this)
28 }
47 }
29
48
30 componentDidMount() {
49 componentDidMount() {
@@ -38,14 +57,13 @@ class InviteClaim extends React.Component {
38 const incoming = await azimuth.azimuth.getTransferringFor(ctrcs, addr)
57 const incoming = await azimuth.azimuth.getTransferringFor(ctrcs, addr)
39 console.log('incoming', incoming)
58 console.log('incoming', incoming)
40
59
41− this.state.loaded = true;
60+ let point = Nothing();
42− if (0 === incoming.length) {
61+ if (incoming.length > 0) {
43− setPointCursor(Maybe.Nothing());
44− } else {
45− setPointCursor(Maybe.Just(incoming[0]));
62+ point = Just(incoming[0]);
46− this.generateWallet(incoming[0])
63+ this.generateWallet(incoming[0]);
47− if (1 < incoming.length) {
64+ this.setState({ loaded: true, point: point });
65+ if (incoming.length > 1) {
48− //TODO notify user "...and others"
66+ //TODO notify user "...and others / ticket reusable"
49 }
67 }
50 }
68 }
51 })))
69 })))
@@ -55,80 +73,205 @@ class InviteClaim extends React.Component {
55 //
73 //
56 }
74 }
57
75
58− generateWallet(point) {
59− //TODO combine wg/views/Generate and Download into single lib function?
76+ //TODO pulled from walletgen/views/Generate and Download, put into lib
77+ async generateWallet(point) {
78+ const makeTicket = point => {
79+
80+ const bits = point < MIN_STAR
81+ ? GALAXY_ENTROPY_BITS
82+ : point < MIN_PLANET
83+ ? STAR_ENTROPY_BITS
84+ : PLANET_ENTROPY_BITS
85+
86+ const bytes = bits / 8
87+ const some = new Uint8Array(bytes)
88+ window.crypto.getRandomValues(some)
89+
90+ const gen = new more.Generator()
91+
92+ return new Promise((resolve, reject) => {
93+ gen.generate(bits, result => {
94+ const chunked = lodash.chunk(result, 2)
95+ const desired = chunked.slice(0, bytes) // only take required entropy
96+ const more = lodash.flatMap(desired, arr => arr[0] ^ arr[1])
97+ const entropy = lodash.zipWith(some, more, (x, y) => x ^ y)
98+ const buf = Buffer.from(entropy)
99+ const patq = ob.hex2patq(buf.toString('hex'))
100+ resolve(patq)
101+ reject('Entropy generation failed')
102+ })
103+ })
104+ }
105+
106+ const genWallet = async (point, ticket, cb) => {
107+
108+ const boot = point < MIN_STAR || point > MAX_STAR;
109+
110+ const config = {
111+ ticket: ticket,
112+ seedSize: SEED_LENGTH_BYTES,
113+ ship: point,
114+ password: '',
115+ revisions: {},
116+ boot: boot
117+ };
118+
119+ const wallet = await kg.generateWallet(config);
120+
121+ // This is here to notify the anyone who opens console because the thread
122+ // hangs, blocking UI updates so this cannot be doen in the UI
123+ console.log('Generating Wallet for point address: ', point);
124+
125+ return wallet;
126+ }
127+
128+ const ticket = await makeTicket(point);
129+
130+ const wallet = await genWallet(point, ticket);
131+
132+ this.setState({ wallet: Just(wallet) });
133+ console.log('got wallet', wallet);
134+ console.log('should start rendering now');
135+ }
136+
137+ download() {
138+ const zip = new JSZip();
139+ //TODO the categories here aren't explained in bridge at all...
140+
141+ const bin0 = this.state.paper.filter(item => item.bin === '0');
142+ const bin1 = this.state.paper.filter(item => item.bin === '1');
143+ const bin2 = this.state.paper.filter(item => item.bin === '2');
144+ const bin3 = this.state.paper.filter(item => item.bin === '3');
145+ const bin4 = this.state.paper.filter(item => item.bin === '4');
146+
147+ const bin0Folder = zip.folder("0. Public");
148+ const bin1Folder = zip.folder("1. Very High Friction Custody");
149+ const bin2Folder = zip.folder("2. High Friction Custody");
150+ const bin3Folder = zip.folder("3. Medium Friction Custody");
151+ const bin4Folder = zip.folder("4. Low Friction Custody");
152+
153+ bin0.forEach(item => bin0Folder.file(`${item.pageTitle}.png`, item.png))
154+ bin1.forEach(item => bin1Folder.file(`${item.pageTitle}.png`, item.png));
155+ bin2.forEach(item => bin2Folder.file(`${item.pageTitle}.png`, item.png));
156+ bin3.forEach(item => bin3Folder.file(`${item.pageTitle}.png`, item.png));
157+ bin4.forEach(item => bin4Folder.file(`${item.pageTitle}.png`, item.png));
158+
159+ zip.generateAsync({type:"blob"}).then((content) => {
160+ saveAs(content, 'urbit-wallet.zip');
161+ this.setState({ downloaded: true });
162+ });
60 }
163 }
61
164
62 render() {
165 render() {
63
166
64− const { pointCursor } = this.props
167+ const { loaded, point, wallet, paper, downloaded } = this.state;
65
168
66− //TODO do something about DOM duplication below
169+ let loading;
170+ if (!loaded) {
171+ loading = ( <div>{ 'Loading...' }</div> );
172+ }
67
173
68− if (!this.state.loaded) {
174+ let balanceWarning;
69− return (
175+ if (loaded && Nothing.hasInstance(point)) {
70− <Row>
176+ balanceWarning = (
71− <Col>
177+ <Warning>
72− {'Loading...'}
178+ <h3 className={'mb-2'}>{'Warning'}</h3>
73− </Col>
179+ {
74− </Row>
180+ 'This invite has no claimable balance.'
181+ }
182+ </Warning>
75− )
183+ );
76 }
184 }
77
185
186+ let pointOverview;
78− if (!Maybe.Just.hasInstance(pointCursor)) {
187+ if (loaded && Just.hasInstance(point)) {
79− return (
188+ const name = ob.patp(point.value);
80− <Row>
189+ const sigil = pour({
81− <Col>
190+ patp: name,
82− <Warning>
191+ renderer: ReactSVGComponents,
192+ size: 256
193+ });
194+ pointOverview = (
195+ //TODO Passport display component
196+ <>
83− <h3 className={'mb-2'}>{'Warning'}</h3>
197+ <div className={'mt-12 pt-6'}>
84− {
198+ { sigil }
85− 'This invite has no claimable balance.'
199+ </div>
86− }
200+ <H3><code>{ name }</code></H3>
87− </Warning>
201+ </>
88− </Col>
89− </Row>
90− )
202+ );
91 }
203 }
92
204
205+ let downloadButtonContents = 'Download wallet';
206+ if ( Nothing.hasInstance(this.state.wallet) ||
207+ Nothing.hasInstance(this.state.paper) ) {
208+ downloadButtonContents = (
209+ <>
210+ <span className="btn-spinner"></span>
211+ {'Generating wallet...'}
212+ </>
213+ );
214+ }
93
215
94− const point = pointCursor.matchWith({
216+ let downloadButton;
95− Just: (cursor) => cursor.value,
217+ let nextButton;
96− Nothing: () => {
218+ if (loaded) {
97− throw BRIDGE_ERROR.MISSING_POINT
219+ downloadButton = (
98− }
220+ <Button
221+ className={'mt-8'}
222+ prop-size={'lg wide'}
223+ disabled={( Nothing.hasInstance(this.state.wallet) ||
224+ Nothing.hasInstance(this.state.paper) )}
225+ onClick={this.download}
226+ >
227+ <span className="relative">
228+ { downloadButtonContents }
229+ </span>
230+ </Button>
99− })
231+ );
100
232
101− const name = ob.patp(point)
233+ nextButton = (
234+ <Button
235+ className={'mt-8'}
236+ prop-size={'lg wide'}
237+ disabled={!this.state.downloaded}
238+ onClick={() => console.log('TODO route to next')}
239+ >
240+ {'Proceed'}
241+ </Button>
242+ );
243+ }
102
244
103− const sigil = pour({
245+ let paperRenderer;
104− patp: name,
246+ if ( Just.hasInstance(this.state.wallet) &&
105− renderer: ReactSVGComponents,
247+ Nothing.hasInstance(this.state.paper) ) {
106− size: 256
248+ console.log('rendering for', point.value)
249+ paperRenderer = (
250+ <PaperCollateralRenderer
251+ wallet={{[point.value]: wallet.value}}
252+ className={'extremely-hidden'}
253+ callback={data => {
254+ console.log('got paper', data);
255+ this.setState({isRendering: false, paper: data})
256+ }}
257+ mode={'REGISTRATION'} />
258+ );
107− })
259+ }
108
260
109 return (
261 return (
110− <Row>
262+ <Row>
111− <Col>
263+ <Col>
112− <div className={'mt-12 pt-6'}>
264+
113− { sigil }
265+ { loading }
114− </div>
266+ { balanceWarning }
115− <H1><code>{ name }</code></H1>
267+ { pointOverview }
116−
268+
117− <p>{'TODO copy'}</p>
269+ <p>{'TODO copy. you need a wallet to receive the point into. download it below.'}</p>
118−
270+
119− <Button
271+ { downloadButton }
120− className={'mt-8'}
272+ { nextButton }
121− prop-size={'lg wide'}
273+
122− disabled={this.state.isGenerating}
274+ { paperRenderer }
123− onClick={() => console.log('TODO download wallet')}>
124−
125− <span className="relative">
126− {this.state.isGenerating &&
127− <span className="btn-spinner"></span>
128− }
129− {'Generating wallet...'}
130− </span>
131− </Button>
132
275
133 </Col>
276 </Col>
134 </Row>
277 </Row>
src/bridge/views/InviteLogin.js+3−3
@@ -1,4 +1,4 @@
1−import Maybe from 'folktale/maybe'
1+import { Just, Nothing } from 'folktale/maybe'
2import React from 'react'
2import React from 'react'
3import { Button } from '../components/Base'
3import { Button } from '../components/Base'
4import { InnerLabel, ValidatedSigil, PointInput, TicketInput } from '../components/Base'
4import { InnerLabel, ValidatedSigil, PointInput, TicketInput } from '../components/Base'
@@ -49,7 +49,7 @@ class InviteLogin extends React.Component {
49 // buttonTriState = (wallet) => {
49 // buttonTriState = (wallet) => {
50 // if (wallet.isNothing()) return 'blue'
50 // if (wallet.isNothing()) return 'blue'
51 // if (wallet === false) return 'yellow'
51 // if (wallet === false) return 'yellow'
52− // if (Maybe.Nothing.hasInstance(wallet)) return 'green'
52+ // if (Nothing.hasInstance(wallet)) return 'green'
53 // }
53 // }
54
54
55 async walletFromTicket(ticket, pointName) {
55 async walletFromTicket(ticket, pointName) {
@@ -66,7 +66,7 @@ class InviteLogin extends React.Component {
66 const mnemonic = urbitWallet.ownership.seed
66 const mnemonic = urbitWallet.ownership.seed
67 const wallet = walletFromMnemonic(mnemonic, DEFAULT_HD_PATH)
67 const wallet = walletFromMnemonic(mnemonic, DEFAULT_HD_PATH)
68 setWallet(wallet)
68 setWallet(wallet)
69− setUrbitWallet(Maybe.Just(urbitWallet))
69+ setUrbitWallet(Just(urbitWallet))
70
70
71 this.setState({
71 this.setState({
72 isUnlocking: false
72 isUnlocking: false