@@ -1,4 +1,4 @@
1−import Maybe from 'folktale/maybe'
1+import { Just, Nothing } from 'folktale/maybe'
22import React from 'react'
33import { pour } from 'sigil-js'
44import * as ob from 'urbit-ob'
@@ -13,7 +13,22 @@ import Actions from './Point/Actions'
1313import { BRIDGE_ERROR } from '../lib/error'
1414import { Row, Col, Warning, Button, H1, H3 } from '../components/Base'
1515
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;
1630
31+//TODO more like InviteGenerate...
1732class InviteClaim extends React.Component {
1833
1934 constructor(props) {
@@ -21,10 +36,14 @@ class InviteClaim extends React.Component {
2136
2237 this.state = {
2338 loaded: false,
24− point: Maybe.Nothing(),
25− newTicket: Maybe.Nothing(),
26− isGenerating: true
39+ point: Nothing(),
40+ wallet: Nothing(),
41+ paper: Nothing(),
42+ downloaded: false
43+ //TODO remove bools in favor of Nothings/Justs
2744 }
45+
46+ this.download = this.download.bind(this)
2847 }
2948
3049 componentDidMount() {
@@ -38,14 +57,13 @@ class InviteClaim extends React.Component {
3857 const incoming = await azimuth.azimuth.getTransferringFor(ctrcs, addr)
3958 console.log('incoming', incoming)
4059
41− this.state.loaded = true;
42− if (0 === incoming.length) {
43− setPointCursor(Maybe.Nothing());
44− } else {
45− setPointCursor(Maybe.Just(incoming[0]));
46− this.generateWallet(incoming[0])
47− if (1 < incoming.length) {
48− //TODO notify user "...and others"
60+ let point = Nothing();
61+ if (incoming.length > 0) {
62+ point = Just(incoming[0]);
63+ this.generateWallet(incoming[0]);
64+ this.setState({ loaded: true, point: point });
65+ if (incoming.length > 1) {
66+ //TODO notify user "...and others / ticket reusable"
4967 }
5068 }
5169 })))
@@ -55,80 +73,205 @@ class InviteClaim extends React.Component {
5573 //
5674 }
5775
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+ });
60163 }
61164
62165 render() {
63166
64− const { pointCursor } = this.props
167+ const { loaded, point, wallet, paper, downloaded } = this.state;
65168
66− //TODO do something about DOM duplication below
169+ let loading;
170+ if (!loaded) {
171+ loading = ( <div>{ 'Loading...' }</div> );
172+ }
67173
68− if (!this.state.loaded) {
69− return (
70− <Row>
71− <Col>
72− {'Loading...'}
73− </Col>
74− </Row>
75− )
174+ let balanceWarning;
175+ if (loaded && Nothing.hasInstance(point)) {
176+ balanceWarning = (
177+ <Warning>
178+ <h3 className={'mb-2'}>{'Warning'}</h3>
179+ {
180+ 'This invite has no claimable balance.'
181+ }
182+ </Warning>
183+ );
76184 }
77185
78− if (!Maybe.Just.hasInstance(pointCursor)) {
79− return (
80− <Row>
81− <Col>
82− <Warning>
83− <h3 className={'mb-2'}>{'Warning'}</h3>
84− {
85− 'This invite has no claimable balance.'
86− }
87− </Warning>
88− </Col>
89− </Row>
90− )
186+ let pointOverview;
187+ if (loaded && Just.hasInstance(point)) {
188+ const name = ob.patp(point.value);
189+ const sigil = pour({
190+ patp: name,
191+ renderer: ReactSVGComponents,
192+ size: 256
193+ });
194+ pointOverview = (
195+ //TODO Passport display component
196+ <>
197+ <div className={'mt-12 pt-6'}>
198+ { sigil }
199+ </div>
200+ <H3><code>{ name }</code></H3>
201+ </>
202+ );
91203 }
92204
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+ }
93215
94− const point = pointCursor.matchWith({
95− Just: (cursor) => cursor.value,
96− Nothing: () => {
97− throw BRIDGE_ERROR.MISSING_POINT
98− }
99− })
216+ let downloadButton;
217+ let nextButton;
218+ if (loaded) {
219+ downloadButton = (
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>
231+ );
100232
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+ }
102244
103− const sigil = pour({
104− patp: name,
105− renderer: ReactSVGComponents,
106− size: 256
107− })
245+ let paperRenderer;
246+ if ( Just.hasInstance(this.state.wallet) &&
247+ Nothing.hasInstance(this.state.paper) ) {
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+ );
259+ }
108260
109261 return (
110− <Row>
111− <Col>
112− <div className={'mt-12 pt-6'}>
113− { sigil }
114− </div>
115− <H1><code>{ name }</code></H1>
116−
117− <p>{'TODO copy'}</p>
118−
119− <Button
120− className={'mt-8'}
121− prop-size={'lg wide'}
122− disabled={this.state.isGenerating}
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>
262+ <Row>
263+ <Col>
264+
265+ { loading }
266+ { balanceWarning }
267+ { pointOverview }
268+
269+ <p>{'TODO copy. you need a wallet to receive the point into. download it below.'}</p>
270+
271+ { downloadButton }
272+ { nextButton }
273+
274+ { paperRenderer }
132275
133276 </Col>
134277 </Row>