@@ -1,244 +1,426 @@
1−import { Nothing } from 'folktale/maybe'
1+import { Nothing, Just } from 'folktale/maybe'
22import React from 'react'
33
44import { Code, H3 } from './Base'
55import { Button } from './Base'
66import { CheckboxButton, Input, InnerLabel } from './Base'
7+import { Warning } from '../components/Base'
78
8−import { renderSignedTx, signTransaction } from '../lib/txn'
9+import { addressFromSecp256k1Public } from '../lib/wallet'
10+import { BRIDGE_ERROR } from '../lib/error'
11+import { ROUTE_NAMES } from '../lib/router'
912
10−const StatelessTransaction = props => {
11− const { web3, gasPrice, gasLimit, nonce, chainId } = props
12− const { setNonce, setChainId, setGasLimit, setGasPrice, showGasDetails, toggleGasDetails } = props
13− const { txn, stx, createUnsignedTxn } = props
14− const { canSign, canGenerate, canApprove } = props
15− const { setUserApproval, userApproval } = props
16− const { canSend, handleSubmit } = props
13+import {
14+ sendSignedTransaction,
15+ fromWei,
16+ renderSignedTx,
17+ signTransaction
18+ } from '../lib/txn'
19+
20+class StatelessTransaction extends React.Component {
21+
22+ constructor(props) {
23+ super(props)
24+
25+ this.state = {
26+ gasPrice: '5',
27+ gasLimit: '600000',
28+ showGasDetails: false,
29+ userApproval: false,
30+ chainId: '',
31+ nonce: '',
32+ stx: Nothing(),
33+ txn: Nothing(),
34+ txError: Nothing(),
35+ }
36+
37+ this.createUnsignedTxn = this.createUnsignedTxn.bind(this)
38+ this.submit = this.submit.bind(this)
39+ this.setUserApproval = this.setUserApproval.bind(this)
40+ this.setTxn = this.setTxn.bind(this)
41+ this.setStx = this.setStx.bind(this)
42+ this.sendTxn = this.sendTxn.bind(this)
43+ this.setNonce = this.setNonce.bind(this)
44+ this.setChainId = this.setChainId.bind(this)
45+ this.setGasPrice = this.setGasPrice.bind(this)
46+ this.setGasLimit = this.setGasLimit.bind(this)
47+ this.rangeChange = this.rangeChange.bind(this)
48+ this.toggleGasDetails = this.toggleGasDetails.bind(this)
49+ }
50+
51+ componentDidMount() {
52+ const { props } = this
53+
54+ const addr = props.wallet.matchWith({
55+ Just: wal => addressFromSecp256k1Public(wal.value.publicKey),
56+ Nothing: () => {
57+ throw BRIDGE_ERROR.MISSING_WALLET
58+ }
59+ })
60+
61+ props.web3.matchWith({
62+ Nothing: () => {},
63+ Just: (w3) => {
64+ const validWeb3 = w3.value
65+
66+ const getTxMetadata = [
67+ validWeb3.eth.getTransactionCount(addr),
68+ validWeb3.eth.net.getId(),
69+ validWeb3.eth.getGasPrice()
70+ ];
71+
72+ Promise.all(getTxMetadata).then(r => {
73+ const txMetadata = {
74+ nonce: r[0],
75+ chainId: r[1],
76+ gasPrice: fromWei(r[2], 'gwei'),
77+ };
78+
79+ this.setState({...txMetadata})
80+
81+ })
82+ }
83+ });
84+ }
85+
86+ submit(){
87+ const { props, state } = this
88+ sendSignedTransaction(props.web3.value, state.stx)
89+ .then(sent => {
90+ props.setTxnHashCursor(sent)
91+ props.popRoute()
92+
93+ if (props.networkSeed) {
94+ props.setNetworkSeedCache(props.networkSeed)
95+ props.pushRoute(ROUTE_NAMES.SENT_TRANSACTION, {promptKeyfile: true})
96+ } else {
97+ props.pushRoute(ROUTE_NAMES.SENT_TRANSACTION)
98+ }
99+ })
100+ .catch(err => {
101+ if (err.map) {
102+ this.setState({ txError: err.map(val => val.merge()) })
103+ } else {
104+ this.setState({ txError: err })
105+ }
106+ })
107+ }
108+
109+ setUserApproval(){
110+ const {state} = this
111+ this.setState({ userApproval: !state.userApproval })
112+ }
113+
114+ toggleGasDetails() {
115+ this.setState({
116+ showGasDetails: !this.state.showGasDetails
117+ })
118+ }
119+
120+ setStx(stx){
121+ this.setState({
122+ stx,
123+ userApproval: false,
124+ })
125+ }
126+
127+ setTxn(txn){
128+ this.setState({ txn })
129+ }
130+
131+ createUnsignedTxn() {
132+ const txn = this.props.createUnsignedTxn()
133+
134+ this.setState({ txn })
135+ }
136+
137+ clearTxn() {
138+ this.setState({
139+ userApproval: false,
140+ txn: Nothing(),
141+ stx: Nothing(),
142+ })
143+ }
144+
145+ setNonce(nonce){
146+ this.setState({ nonce })
147+ this.clearStx()
148+ }
149+
150+ setChainId(chainId){
151+ this.setState({ chainId })
152+ this.clearStx()
153+ }
154+
155+ setGasPrice(gasPrice) {
156+ this.setState({ gasPrice })
157+ this.clearStx()
158+ }
159+
160+ rangeChange(e) {
161+ this.setGasPrice(e.target.value);
162+ }
163+
164+ setGasLimit(gasLimit){
165+ this.setState({ gasLimit })
166+ this.clearStx()
167+ }
168+
169+ clearStx() {
170+ this.setState({
171+ userApproval: false,
172+ stx: Nothing(),
173+ })
174+ }
17175
18176 // TODO: Investigate
19− // setState doesn't seem to work in SetProxy/handleSubmit;
177+ // setState doesn't seem to work in SetProxy/submit;
20178 // - TypeError: Cannot read property 'updater' of undefined
21179 // so just modifying the DOM manually here. (https://imgur.com/a/i0Qsyq1)
22180
23− const handleRangeChange = (e) => {
24− setGasPrice(e.target.value);
25− }
26−
27− const sendTxn = (e) => {
181+ sendTxn(e) {
28182 e.target.setAttribute("disabled", true);
29183 let spinner = e.target.querySelectorAll('.btn-spinner')[0];
30184 spinner.classList.remove('hide');
31− handleSubmit()
32− }
33−
34− const generateButtonColor =
35− Nothing.hasInstance(txn)
36− ? 'blue'
37− : 'green'
38−
39− const signerButtonColor =
40− Nothing.hasInstance(stx)
41− ? 'blue'
42− : 'green'
43−
44− const generateTxnButton =
45− <Button
46− className={ 'mt-8' }
47− disabled={ !canGenerate }
48− prop-color={ generateButtonColor }
49− prop-size={ 'lg wide' }
50− onClick={ createUnsignedTxn }
51− >
52− { 'Generate Transaction' }
53− </Button>
54−
55− const unsignedTxnDisplay = txn.matchWith({
56− Nothing: _ => '',
57− Just: tx =>
58− <React.Fragment>
59− <H3 className={ 'mt-8' }>
60− { 'Unsigned Transaction' }
61− </H3>
62− <Code>
63− { JSON.stringify(tx.value, null, 2) }
64− </Code>
65− </React.Fragment>
66− })
185+ this.submit()
186+ }
67187
68− const gasPriceRangeDialogue = (
69− <React.Fragment>
70− <div className="mt-12 flex space-between align-baseline">
71− <div>
72− <span>Gas Price:</span>
73− <span className="ml-4 text-700 text-sm">{gasPrice} gwei</span>
188+ render() {
189+ const { web3, canGenerate } = this.props
190+ const { gasPrice, gasLimit, nonce, chainId,
191+ txn, stx, userApproval, showGasDetails } = this.state
192+
193+ const { setNonce, setChainId, setGasLimit, setGasPrice, toggleGasDetails,
194+ setUserApproval, sendTxn, createUnsignedTxn } = this
195+
196+ const { state } = this
197+
198+ const canSign = Just.hasInstance(txn)
199+ const canApprove = Just.hasInstance(stx)
200+ const canSend = Just.hasInstance(stx) && userApproval === true
201+
202+ const generateButtonColor =
203+ Nothing.hasInstance(txn)
204+ ? 'blue'
205+ : 'green'
206+
207+ const signerButtonColor =
208+ Nothing.hasInstance(stx)
209+ ? 'blue'
210+ : 'green'
211+
212+ const generateTxnButton =
213+ <Button
214+ className={ 'mt-8' }
215+ disabled={ !canGenerate }
216+ prop-color={ generateButtonColor }
217+ prop-size={ 'lg wide' }
218+ onClick={ createUnsignedTxn }
219+ >
220+ { 'Generate Transaction' }
221+ </Button>
222+
223+ const unsignedTxnDisplay = txn.matchWith({
224+ Nothing: _ => '',
225+ Just: tx =>
226+ <React.Fragment>
227+ <H3 className={ 'mt-8' }>
228+ { 'Unsigned Transaction' }
229+ </H3>
230+ <Code>
231+ { JSON.stringify(tx.value, null, 2) }
232+ </Code>
233+ </React.Fragment>
234+ })
235+
236+ const gasPriceRangeDialogue = (
237+ <React.Fragment>
238+ <div className="mt-12 flex space-between align-baseline">
239+ <div>
240+ <span>Gas Price:</span>
241+ <span className="ml-4 text-700 text-sm">{gasPrice} gwei</span>
242+ </div>
243+ <div className="text-sm">
244+ <span>Max transaction fee: </span>
245+ <span className="text-700">{(gasPrice * gasLimit) / 1000000000} eth</span>
246+ </div>
74247 </div>
75− <div className="text-sm">
76− <span>Max transaction fee: </span>
77− <span className="text-700">{(gasPrice * gasLimit) / 1000000000} eth</span>
248+
249+ <input
250+ className="mt-4"
251+ type="range"
252+ min="2"
253+ max="20"
254+ list="gweiVals"
255+ value={gasPrice}
256+ onChange={this.rangeChange}
257+ />
258+
259+ <div className="flex space-between text-sm mb-8">
260+ <div>Cheap</div>
261+ <div>Fast</div>
78262 </div>
79− </div>
80−
81− <input
82− className="mt-4"
83− type="range"
84− min="2"
85− max="20"
86− list="gweiVals"
87− value={gasPrice}
88− onChange={handleRangeChange}
89− />
90−
91− <div className="flex space-between text-sm mb-8">
92− <div>Cheap</div>
93− <div>Fast</div>
94− </div>
95− </React.Fragment>
96− )
97−
98− const toggleGasDetailsDialogue = (
99− <a href="javascript:void(0)" onClick={toggleGasDetails}>Gas Details</a>
100− )
101−
102− const gasPriceDialogue =
103− <Input
104− className={ 'mono mt-4' }
105− prop-size={ 'md' }
106− prop-format={ 'innerLabel' }
107− value={ gasPrice }
108− onChange={ setGasPrice }
109− >
110− <InnerLabel>
111− { 'Gas Price (gwei)' }
112− </InnerLabel>
113− </Input>
114−
115− const gasLimitDialogue =
116− <Input
117− className={ 'mono mt-4' }
118− prop-size={ 'md' }
119− prop-format={ 'innerLabel' }
120− value={ gasLimit }
121− onChange={ setGasLimit }>
122− <InnerLabel>
123− { 'Gas Limit' }
124− </InnerLabel>
125− </Input>
126−
127− const nonceDialogue =
128− <Input
129− className={ 'mono mt-4' }
130− prop-size={ 'md' }
131− prop-format={ 'innerLabel' }
132− value={ nonce }
133− onChange={ setNonce }
134− >
135− <InnerLabel>
136− { 'Nonce' }
137− </InnerLabel>
138− </Input>
139−
140− const chainDialogue =
141− <Input
142− className={ 'mono mt-4 mb-8' }
143− prop-size={ 'md' }
144− prop-format={ 'innerLabel' }
145− value={ chainId }
146− onChange={ setChainId }
147− >
148− <InnerLabel>
149− { 'Chain ID' }
150− </InnerLabel>
151− </Input>
152−
153− const onlineParamsDialogue = web3.matchWith({
154− Just: _ => <div />,
155− Nothing: _ =>
156− <React.Fragment>
157− { nonceDialogue }
158− { chainDialogue }
159263 </React.Fragment>
160− })
161−
162− const signTxnButton =
163− <Button
164− disabled={ !canSign }
165− className={ 'mt-8' }
166− prop-size={ 'lg wide' }
167− prop-color={ signerButtonColor }
168− onClick={ () => signTransaction(props) }
169− >
170− { 'Sign Transaction' }
171− </Button>
172−
173− const signedTxnDisplay = stx.matchWith({
174− Nothing: _ => '',
175− Just: tx =>
176− <React.Fragment>
177− <H3 className={ 'mt-8' }>
178− { 'Signed Transaction' }
179− </H3>
180− <Code>
181− { JSON.stringify(renderSignedTx(tx.value), null, 2) }
182− </Code>
183− </React.Fragment>
184− })
185−
186− const confirmButton =
187− <CheckboxButton
188− className={ 'mt-8' }
189− disabled={ !canApprove }
190− onClick={ setUserApproval }
191− state={ userApproval }
192− >
193− <div>
194− { `I approve this transaction and wish to send.` }
195− </div>
196− </CheckboxButton>
197−
198− const sendTxnButton =
199− <Button
200− prop-size={ 'xl wide' }
201− className={ 'mt-8' }
202− disabled={ !canSend }
203− onClick={ sendTxn }
204− >
205− <span className="relative">
206− <span className="btn-spinner hide"></span>
207− { 'Send Transaction' }
208− </span>
209− </Button>
210−
211− const sendDialogue = web3.matchWith({
212− Nothing: _ => '',
213− Just: _ =>
214− <React.Fragment>
215− { confirmButton }
216− { sendTxnButton }
217− </React.Fragment>
218− })
264+ )
265+
266+ const toggleGasDetailsDialogue = (
267+ <span className="text-link" onClick={toggleGasDetails}>Gas Details</span>
268+ )
269+
270+ const gasPriceDialogue =
271+ <Input
272+ className={ 'mono mt-4' }
273+ prop-size={ 'md' }
274+ prop-format={ 'innerLabel' }
275+ value={ gasPrice }
276+ onChange={ setGasPrice }
277+ >
278+ <InnerLabel>
279+ { 'Gas Price (gwei)' }
280+ </InnerLabel>
281+ </Input>
219282
220− return (
221− <React.Fragment>
222− { generateTxnButton }
223− { unsignedTxnDisplay }
283+ const gasLimitDialogue =
284+ <Input
285+ className={ 'mono mt-4' }
286+ prop-size={ 'md' }
287+ prop-format={ 'innerLabel' }
288+ value={ gasLimit }
289+ onChange={ setGasLimit }>
290+ <InnerLabel>
291+ { 'Gas Limit' }
292+ </InnerLabel>
293+ </Input>
224294
225− { gasPriceRangeDialogue }
226− { toggleGasDetailsDialogue }
295+ const nonceDialogue =
296+ <Input
297+ className={ 'mono mt-4' }
298+ prop-size={ 'md' }
299+ prop-format={ 'innerLabel' }
300+ value={ nonce }
301+ onChange={ setNonce }
302+ >
303+ <InnerLabel>
304+ { 'Nonce' }
305+ </InnerLabel>
306+ </Input>
227307
228− { showGasDetails &&
308+ const chainDialogue =
309+ <Input
310+ className={ 'mono mt-4 mb-8' }
311+ prop-size={ 'md' }
312+ prop-format={ 'innerLabel' }
313+ value={ chainId }
314+ onChange={ setChainId }
315+ >
316+ <InnerLabel>
317+ { 'Chain ID' }
318+ </InnerLabel>
319+ </Input>
320+
321+ const onlineParamsDialogue = web3.matchWith({
322+ Just: _ => <div />,
323+ Nothing: _ =>
324+ <React.Fragment>
325+ { nonceDialogue }
326+ { chainDialogue }
327+ </React.Fragment>
328+ })
329+
330+ const signTxnButton =
331+ <Button
332+ disabled={ !canSign }
333+ className={ 'mt-8' }
334+ prop-size={ 'lg wide' }
335+ prop-color={ signerButtonColor }
336+ onClick={ () => signTransaction({...this.props, ...this.state, setStx: this.setStx}) }
337+ >
338+ { 'Sign Transaction' }
339+ </Button>
340+
341+ const signedTxnDisplay = stx.matchWith({
342+ Nothing: _ => '',
343+ Just: tx =>
344+ <React.Fragment>
345+ <H3 className={ 'mt-8' }>
346+ { 'Signed Transaction' }
347+ </H3>
348+ <Code>
349+ { JSON.stringify(renderSignedTx(tx.value), null, 2) }
350+ </Code>
351+ </React.Fragment>
352+ })
353+
354+ const confirmButton =
355+ <CheckboxButton
356+ className={ 'mt-8' }
357+ disabled={ !canApprove }
358+ onClick={ setUserApproval }
359+ state={ userApproval }
360+ >
229361 <div>
230− { gasPriceDialogue }
231− { gasLimitDialogue }
362+ { `I approve this transaction and wish to send.` }
232363 </div>
233− }
234− { onlineParamsDialogue }
364+ </CheckboxButton>
235365
236− { signTxnButton }
366+ const sendTxnButton =
367+ <Button
368+ prop-size={ 'xl wide' }
369+ className={ 'mt-8' }
370+ disabled={ !canSend }
371+ onClick={ sendTxn }
372+ >
373+ <span className="relative">
374+ <span className="btn-spinner hide"></span>
375+ { 'Send Transaction' }
376+ </span>
377+ </Button>
237378
238− { signedTxnDisplay }
239− { sendDialogue }
240− </React.Fragment>
241− )
379+ const sendDialogue = web3.matchWith({
380+ Nothing: _ => '',
381+ Just: _ =>
382+ <React.Fragment>
383+ { confirmButton }
384+ { sendTxnButton }
385+ </React.Fragment>
386+ })
387+
388+ const txnErrorDialogue = Nothing.hasInstance(state.txError)
389+ ? ''
390+ : <Warning className={'mt-8'}>
391+ <H3 style={{marginTop: 0, paddingTop: 0}}>
392+ {
393+ 'There was an error sending your transaction.'
394+ }
395+ </H3>
396+ { state.txError.value }
397+ </Warning>
398+
399+ return (
400+ <React.Fragment>
401+ { generateTxnButton }
402+ { unsignedTxnDisplay }
403+
404+ { gasPriceRangeDialogue }
405+ { toggleGasDetailsDialogue }
406+
407+ { showGasDetails &&
408+ <div>
409+ { gasPriceDialogue }
410+ { gasLimitDialogue }
411+ </div>
412+ }
413+ { onlineParamsDialogue }
414+
415+ { signTxnButton }
416+
417+ { signedTxnDisplay }
418+ { sendDialogue }
419+
420+ { txnErrorDialogue }
421+ </React.Fragment>
422+ )
423+ }
242424}
243425
244426export default StatelessTransaction