on master help sign in
Merge pull request #61 from urbit/jt-issue-60
915baebJared Tobin
package.json+2−3
@@ -58,11 +58,10 @@
58 "pilot:cleanup": "pkill -f ganache-cli"
58 "pilot:cleanup": "pkill -f ganache-cli"
59 },
59 },
60 "devDependencies": {
60 "devDependencies": {
61− "@ledgerhq/hw-transport-node-hid": "^4.24.0",
62 "ganache-cli": "^6.2.1",
61 "ganache-cli": "^6.2.1",
63 "npm-run-all": "^4.1.3",
62 "npm-run-all": "^4.1.3",
63+ "serve": "10.1.1",
64− "truffle": "4.1.11",
64+ "truffle": "4.1.11"
65− "serve": "10.1.1"
66 },
65 },
67 "browserslist": [
66 "browserslist": [
68 ">0.2%",
67 ">0.2%",
src/components/StatelessTransaction.js+164−157
@@ -1,177 +1,184 @@
1−import Maybe from 'folktale/maybe'
1+import { Nothing } from 'folktale/maybe'
2import React from 'react'
2import React from 'react'
3
3
4import { Code, H3 } from './Base'
4import { Code, H3 } from './Base'
5import { Button } from './Base'
5import { Button } from './Base'
6−import { CheckboxButton, Input, InnerLabel } from './Base'
6+import { CheckboxButton, Input, InnerLabel } from './Base'
7−
8−import {
9− renderSignedTx,
10− // createUnsignedTxn,
11− signTransaction,
12−} from '../lib/txn';
13−
14
7
8+import { renderSignedTx, signTransaction } from '../lib/txn'
15
9
16const StatelessTransaction = props => {
10const StatelessTransaction = props => {
17−
11+ const { web3, gasPrice, gasLimit, nonce, chainId } = props
12+ const { setNonce, setChainId, setGasLimit, setGasPrice } = props
13+ const { txn, stx, createUnsignedTxn } = props
14+ const { canSign, canGenerate, canApprove } = props
15+ const { setUserApproval, userApproval } = props
16+ const { canSend, handleSubmit } = props
17+
18− const generateButtonColor = Maybe.Nothing.hasInstance(props.txn)
18+ const generateButtonColor =
19+ Nothing.hasInstance(txn)
19 ? 'blue'
20 ? 'blue'
20 : 'green'
21 : 'green'
21
22
22− const signerButtonColor = Maybe.Nothing.hasInstance(props.stx)
23+ const signerButtonColor =
24+ Nothing.hasInstance(stx)
23 ? 'blue'
25 ? 'blue'
24 : 'green'
26 : 'green'
25
27
26− return (
28+ const generateTxnButton =
29+ <Button
30+ className={ 'mt-8' }
31+ disabled={ !canGenerate }
32+ prop-color={ generateButtonColor }
33+ prop-size={ 'lg wide' }
34+ onClick={ createUnsignedTxn }
35+ >
36+ { 'Generate Transaction' }
37+ </Button>
38+
39+ const unsignedTxnDisplay = txn.matchWith({
40+ Nothing: _ => '',
41+ Just: tx =>
42+ <React.Fragment>
43+ <H3 className={ 'mt-8' }>
44+ { 'Unsigned Transaction' }
45+ </H3>
46+ <Code>
47+ { JSON.stringify(tx.value, null, 2) }
48+ </Code>
49+ </React.Fragment>
50+ })
51+
52+ const gasPriceDialogue =
53+ <Input
54+ className={ 'mono mt-4' }
55+ prop-size={ 'md' }
56+ prop-format={ 'innerLabel' }
57+ value={ gasPrice }
58+ onChange={ setGasPrice }
59+ >
60+ <InnerLabel>
61+ { 'Gas Price (gwei)' }
62+ </InnerLabel>
63+ </Input>
64+
65+ const gasLimitDialogue =
66+ <Input
67+ className={ 'mono mt-4' }
68+ prop-size={ 'md' }
69+ prop-format={ 'innerLabel' }
70+ value={ gasLimit }
71+ onChange={ setGasLimit }>
72+ <InnerLabel>
73+ { 'Gas Limit' }
74+ </InnerLabel>
75+ </Input>
76+
77+ const nonceDialogue =
78+ <Input
79+ className={ 'mono mt-4' }
80+ prop-size={ 'md' }
81+ prop-format={ 'innerLabel' }
82+ value={ nonce }
83+ onChange={ setNonce }
84+ >
85+ <InnerLabel>
86+ { 'Nonce' }
87+ </InnerLabel>
88+ </Input>
89+
90+ const chainDialogue =
91+ <Input
92+ className={ 'mono mt-4 mb-8' }
93+ prop-size={ 'md' }
94+ prop-format={ 'innerLabel' }
95+ value={ chainId }
96+ onChange={ setChainId }
97+ >
98+ <InnerLabel>
99+ { 'Chain ID' }
100+ </InnerLabel>
101+ </Input>
102+
103+ const onlineParamsDialogue = web3.matchWith({
104+ Just: _ => <div />,
105+ Nothing: _ =>
106+ <React.Fragment>
107+ { nonceDialogue }
108+ { chainDialogue }
109+ </React.Fragment>
110+ })
111+
112+ const signTxnButton =
113+ <Button
114+ disabled={ !canSign }
115+ className={ 'mt-8' }
116+ prop-size={ 'lg wide' }
117+ prop-color={ signerButtonColor }
118+ onClick={ () => signTransaction(props) }
119+ >
120+ { 'Sign Transaction' }
121+ </Button>
122+
123+ const signedTxnDisplay = stx.matchWith({
124+ Nothing: _ => '',
125+ Just: tx =>
126+ <React.Fragment>
127+ <H3 className={ 'mt-8' }>
128+ { 'Signed Transaction' }
129+ </H3>
130+ <Code>
131+ { JSON.stringify(renderSignedTx(tx.value), null, 2) }
132+ </Code>
133+ </React.Fragment>
134+ })
135+
136+ const confirmButton =
137+ <CheckboxButton
138+ className={ 'mt-8' }
139+ disabled={ !canApprove }
140+ onClick={ setUserApproval }
141+ state={ userApproval }
142+ >
143+ <div>
144+ { `I approve this transaction and wish to send.` }
145+ </div>
146+ </CheckboxButton>
147+
148+ const sendTxnButton =
149+ <Button
150+ prop-size={ 'xl wide' }
151+ className={ 'mt-8' }
152+ disabled={ !canSend }
153+ onClick={ handleSubmit }
154+ >
155+ { 'Send Transaction' }
156+ </Button>
157+
158+ const sendDialogue = web3.matchWith({
159+ Nothing: _ => '',
160+ Just: _ =>
27 <React.Fragment>
161 <React.Fragment>
162+ { confirmButton }
163+ { sendTxnButton }
164+ </React.Fragment>
165+ })
28
166
29− <Button
30− prop-size={'lg wide'}
31− prop-color={generateButtonColor}
32− className={'mt-8'}
33− disabled={ !props.canGenerate }
34− onClick={ () => props.createUnsignedTxn()}>
35− { 'Generate Transaction' }
36− </Button>
37−
38−
39− {
40− props.txn.matchWith({
41− Nothing: () => '',
42− Just: (tx) => {
43− console.log(tx.value)
44− const rendered = tx.value
45− return (
167+ return (
46− <React.Fragment>
47− <H3 className={'mt-8'}>{'Unsigned Transaction'}</H3>
48− <Code> {JSON.stringify(rendered, null, 2)} </Code>
49− </React.Fragment>
50− )
51− }
52− })
53− }
54−
55−
56− <Input
57− className='mono mt-4'
58− prop-size='md'
59− prop-format='innerLabel'
60− value={ props.gasPrice }
61− onChange={ v => props.setGasPrice(v) }>
62− <InnerLabel>
63− { 'Gas Price (gwei)' }
64− </InnerLabel>
65− </Input>
66−
67− <Input
68− className='mono mt-4'
69− prop-size='md'
70− prop-format='innerLabel'
71− value={ props.gasLimit }
72− onChange={ v => props.setGasLimit(v) }>
73− <InnerLabel>
74− { 'Gas Limit' }
75− </InnerLabel>
76− </Input>
77−
78− {
79− props.web3.matchWith({
80− Just: () => <div />,
81− Nothing: () =>
82− <React.Fragment>
83− <Input
84− className='mono mt-4'
85− prop-size='md'
86− prop-format='innerLabel'
87− value={ props.nonce }
88− onChange={ v => props.setNonce(v) }>
89− <InnerLabel>
90− { 'Nonce' }
91− </InnerLabel>
92− </Input>
93−
94− <Input
95− className='mono mt-4 mb-8'
96− prop-size='md'
97− prop-format='innerLabel'
98− value={ props.chainId }
99− onChange={ v => props.setChainId(v) }>
100− <InnerLabel>
101− { 'Chain ID' }
102− </InnerLabel>
103− </Input>
104− </React.Fragment>
105− })
106− }
107−
108− <Button
109− disabled={ !props.canSign }
110− className={'mt-8'}
111− prop-size={'lg wide'}
112− prop-color={signerButtonColor}
113− onClick={ () => signTransaction({
114− wallet: props.wallet,
115− walletType: props.walletType,
116− walletHdPath: props.walletHdPath,
117− networkType: props.networkType,
118− txn: props.txn,
119− setStx: props.setStx,
120− nonce: props.nonce,
121− chainId: props.chainId,
122− gasPrice: props.gasPrice,
123− gasLimit: props.gasLimit,
124− }) }>
125− { 'Sign Transaction' }
126− </Button>
127−
128−
129− {
130− props.stx.matchWith({
131− Nothing: () => '',
132− Just: (tx) => {
133− const rendered = renderSignedTx(tx.value)
134− return (
135− <React.Fragment>
136− <H3 className={'mt-8'}>{'Signed Transaction'}</H3>
137− <Code> {JSON.stringify(rendered, null, 2)} </Code>
138− </React.Fragment>
168+ <React.Fragment>
139− )
169+ { generateTxnButton }
140− }
170+ { unsignedTxnDisplay }
141− })
171+
142− }
172+ { gasPriceDialogue }
143−
173+ { gasLimitDialogue }
144− {
174+ { onlineParamsDialogue }
145− props.web3.matchWith({
175+
146− Nothing: () => '',
176+ { signTxnButton }
147− Just: () => {
177+
148− return (
178+ { signedTxnDisplay }
149− <React.Fragment>
179+ { sendDialogue }
150− <CheckboxButton
151− className={'mt-8'}
152− disabled={ !props.canApprove }
153− onClick={ () => props.setUserApproval() }
154− state={props.userApproval}>
155− <div>
156− { `I approve this transaction and wish to send.` }
157− </div>
158− </CheckboxButton>
159− <Button
160− prop-size={'xl wide'}
161− className={'mt-8'}
162− disabled={ !props.canSend }
163− onClick={ () => props.handleSubmit() }>
164− { 'Send Transaction' }
165− </Button>
166− </React.Fragment>
167− )
168− }
169− })
170− }
171 </React.Fragment>
180 </React.Fragment>
172 )
181 )
173}
182}
174
183
175−
176−
177export default StatelessTransaction
184export default StatelessTransaction
src/lib/ledger.js+1−1
@@ -11,8 +11,8 @@ const chopHdPrefix = str =>
11const ledgerSignTransaction = async (txn, hdpath) => {
11const ledgerSignTransaction = async (txn, hdpath) => {
12 const transport = await Transport.create()
12 const transport = await Transport.create()
13 const eth = new Eth(transport)
13 const eth = new Eth(transport)
14− const path = chopHdPrefix(hdpath)
15
14
15+ const path = chopHdPrefix(hdpath)
16 const serializedTx = txn.serialize().toString('hex')
16 const serializedTx = txn.serialize().toString('hex')
17 const sig = await eth.signTransaction(path, serializedTx)
17 const sig = await eth.signTransaction(path, serializedTx)
18
18
src/lib/txn.js+5−5
@@ -1,6 +1,6 @@
1import * as ob from 'urbit-ob'
1import * as ob from 'urbit-ob'
2−import Maybe from 'folktale/maybe'
2+import { Just } from 'folktale/maybe'
3−import Result from 'folktale/result'
3+import { Ok, Error } from 'folktale/result'
4import Tx from 'ethereumjs-tx'
4import Tx from 'ethereumjs-tx'
5import Web3 from 'web3'
5import Web3 from 'web3'
6
6
@@ -134,7 +134,7 @@ const signTransaction = async config => {
134 stx.sign(sec)
134 stx.sign(sec)
135 }
135 }
136
136
137− setStx(Maybe.Just(stx))
137+ setStx(Just(stx))
138}
138}
139
139
140const sendSignedTransaction = (web3, stx) => {
140const sendSignedTransaction = (web3, stx) => {
@@ -150,10 +150,10 @@ const sendSignedTransaction = (web3, stx) => {
150 return new Promise((resolve, reject) => {
150 return new Promise((resolve, reject) => {
151 web3.eth.sendSignedTransaction(serializedTx)
151 web3.eth.sendSignedTransaction(serializedTx)
152 .on('receipt', txn =>
152 .on('receipt', txn =>
153− resolve(Maybe.Just(Result.Ok(txn)))
153+ resolve(Just(Ok(txn)))
154 )
154 )
155 .on('error', err => {
155 .on('error', err => {
156− reject(Maybe.Just(Result.Error(err.message)))
156+ reject(Just(Error(err.message)))
157 })
157 })
158 })
158 })
159}
159}
src/views/AcceptTransfer.js+20−80
@@ -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 { Row, Col, H1, H3, P, Warning } from '../components/Base'
3import { Row, Col, H1, H3, P, Warning } from '../components/Base'
4import { InnerLabel, AddressInput, ShowBlockie, Anchor } from '../components/Base'
4import { InnerLabel, AddressInput, ShowBlockie, Anchor } from '../components/Base'
@@ -9,17 +9,8 @@ import StatelessTransaction from '../components/StatelessTransaction'
9import { BRIDGE_ERROR } from '../lib/error'
9import { BRIDGE_ERROR } from '../lib/error'
10import { NETWORK_NAMES } from '../lib/network'
10import { NETWORK_NAMES } from '../lib/network'
11import { ROUTE_NAMES } from '../lib/router'
11import { ROUTE_NAMES } from '../lib/router'
12−
13−import {
14− sendSignedTransaction,
12+import { sendSignedTransaction, fromWei } from '../lib/txn'
15− fromWei,
16− } from '../lib/txn'
17−
18−import {
19− isValidAddress,
20− addressFromSecp256k1Public,
13+import { isValidAddress, addressFromSecp256k1Public } from '../lib/wallet'
21−} from '../lib/wallet'
22−
23
14
24class AcceptTransfer extends React.Component {
15class AcceptTransfer extends React.Component {
25 constructor(props) {
16 constructor(props) {
@@ -41,18 +32,18 @@ class AcceptTransfer extends React.Component {
41
32
42 this.state = {
33 this.state = {
43 receivingAddress: receivingAddress,
34 receivingAddress: receivingAddress,
44− txn: Maybe.Nothing(),
35+ txn: Nothing(),
45− txError: Maybe.Nothing(),
36+ txError: Nothing(),
46 incomingPoint: incomingPoint,
37 incomingPoint: incomingPoint,
47 userApproval: false,
38 userApproval: false,
48 nonce: '',
39 nonce: '',
49 gasPrice: '5',
40 gasPrice: '5',
50 chainId: '',
41 chainId: '',
51 gasLimit: '600000',
42 gasLimit: '600000',
52− stx: Maybe.Nothing(),
43+ stx: Nothing(),
53 }
44 }
45+
54 this.handleAddressInput = this.handleAddressInput.bind(this)
46 this.handleAddressInput = this.handleAddressInput.bind(this)
55− // Transaction
56 this.handleCreateUnsignedTxn = this.handleCreateUnsignedTxn.bind(this)
47 this.handleCreateUnsignedTxn = this.handleCreateUnsignedTxn.bind(this)
57 this.handleSubmit = this.handleSubmit.bind(this)
48 this.handleSubmit = this.handleSubmit.bind(this)
58 this.handleSetUserApproval = this.handleSetUserApproval.bind(this)
49 this.handleSetUserApproval = this.handleSetUserApproval.bind(this)
@@ -97,57 +88,27 @@ class AcceptTransfer extends React.Component {
97 })
88 })
98 }
89 }
99 });
90 });
100−
101− // const validPoint = props.pointCursor.matchWith({
102− // Just: (shp) => shp.value,
103− // Nothing: () => {
104− // throw BRIDGE_ERROR.MISSING_POINT
105− // }
106− // })
107−
108− // NB (jtobin):
109− //
110− // We need to get the point's owner, so it's not possible to perform this
111− // transaction offline.
112− //
113− // Perhaps we could add an 'owner' input in the offline case, if necessary.
114−
115− // const owner = props.pointCache[validPoint].owner
116− //
117− // const txn = Maybe.Just(this.createUnsignedTxn(
118− // owner,
119− // state.receivingAddress))
120− //
121− // this.setState({ txn })
122 }
91 }
123
92
124−
125−
126 handleAddressInput(proxyAddress) {
93 handleAddressInput(proxyAddress) {
127 this.setState({ proxyAddress })
94 this.setState({ proxyAddress })
128 this.handleClearTxn()
95 this.handleClearTxn()
129 }
96 }
130
97
131−
132 handleCreateUnsignedTxn() {
98 handleCreateUnsignedTxn() {
133 const txn = this.createUnsignedTxn()
99 const txn = this.createUnsignedTxn()
134 this.setState({ txn })
100 this.setState({ txn })
135 }
101 }
136
102
137−
138 handleSetUserApproval(){
103 handleSetUserApproval(){
139 const {state} = this
104 const {state} = this
140 this.setState({ userApproval: !state.userApproval })
105 this.setState({ userApproval: !state.userApproval })
141 }
106 }
142
107
143−
144−
145 handleSetTxn(txn){
108 handleSetTxn(txn){
146 this.setState({ txn })
109 this.setState({ txn })
147 }
110 }
148
111
149−
150−
151 handleSetStx(stx){
112 handleSetStx(stx){
152 this.setState({
113 this.setState({
153 stx,
114 stx,
@@ -155,65 +116,41 @@ class AcceptTransfer extends React.Component {
155 })
116 })
156 }
117 }
157
118
158−
159−
160 handleSetNonce(nonce){
119 handleSetNonce(nonce){
161 this.setState({ nonce })
120 this.setState({ nonce })
162 this.handleClearStx()
121 this.handleClearStx()
163 }
122 }
164
123
165−
166−
167 handleSetChainId(chainId){
124 handleSetChainId(chainId){
168 this.setState({ chainId })
125 this.setState({ chainId })
169 this.handleClearStx()
126 this.handleClearStx()
170 }
127 }
171
128
172−
173−
174 handleSetGasPrice(gasPrice){
129 handleSetGasPrice(gasPrice){
175 this.setState({ gasPrice })
130 this.setState({ gasPrice })
176 this.handleClearStx()
131 this.handleClearStx()
177 }
132 }
178
133
179−
180−
181 handleSetGasLimit(gasLimit){
134 handleSetGasLimit(gasLimit){
182 this.setState({ gasLimit })
135 this.setState({ gasLimit })
183 this.handleClearStx()
136 this.handleClearStx()
184 }
137 }
185
138
186−
187−
188 handleClearStx() {
139 handleClearStx() {
189 this.setState({
140 this.setState({
190 userApproval: false,
141 userApproval: false,
191− stx: Maybe.Nothing(),
142+ stx: Nothing(),
192 })
143 })
193 }
144 }
194
145
195−
196−
197 handleClearTxn() {
146 handleClearTxn() {
198 this.setState({
147 this.setState({
199 userApproval: false,
148 userApproval: false,
200− txn: Maybe.Nothing(),
149+ txn: Nothing(),
201− stx: Maybe.Nothing(),
202− })
203− }
204−
205−
206−
207− handleClearTransaction() {
208− this.setState({
209− userApproval: false,
210− txn: Maybe.Nothing(),
211− stx: Maybe.Nothing(),
150+ stx: Nothing(),
212 })
151 })
213 }
152 }
214
153
215−
216−
217 handleSubmit() {
154 handleSubmit() {
218 const { props, state } = this
155 const { props, state } = this
219 sendSignedTransaction(props.web3.value, state.stx)
156 sendSignedTransaction(props.web3.value, state.stx)
@@ -223,8 +160,7 @@ class AcceptTransfer extends React.Component {
223 props.pushRoute(ROUTE_NAMES.SENT_TRANSACTION)
160 props.pushRoute(ROUTE_NAMES.SENT_TRANSACTION)
224 })
161 })
225 .catch(err => {
162 .catch(err => {
226− // Note that value.value is due to wrapped Maybe.Just + Result.Error
227− this.setState({ txError: Maybe.Just(err.value.value) })
163+ this.setState({ txError: err.map(val => val.merge()) })
228 })
164 })
229 }
165 }
230
166
@@ -247,7 +183,7 @@ class AcceptTransfer extends React.Component {
247
183
248 const owner = props.pointCache[validPoint].owner
184 const owner = props.pointCache[validPoint].owner
249
185
250− return Maybe.Just(azimuth.ecliptic.transferFrom(
186+ return Just(azimuth.ecliptic.transferFrom(
251 validContracts,
187 validContracts,
252 owner,
188 owner,
253 state.receivingAddress,
189 state.receivingAddress,
@@ -260,9 +196,11 @@ class AcceptTransfer extends React.Component {
260 const { state, props } = this
196 const { state, props } = this
261 const validAddress = isValidAddress(state.receivingAddress)
197 const validAddress = isValidAddress(state.receivingAddress)
262 const canGenerate = validAddress === true
198 const canGenerate = validAddress === true
263− const canSign = !Maybe.Nothing.hasInstance(state.txn)
199+ const canSign = Just.hasInstance(state.txn)
264− const canApprove = !Maybe.Nothing.hasInstance(state.stx)
200+ const canApprove = Just.hasInstance(state.stx)
201+ const canSend =
202+ Just.hasInstance(state.stx)
265− const canSend = !Maybe.Nothing.hasInstance(state.stx) && state.userApproval === true
203+ && state.userApproval === true
266
204
267 const esvisible =
205 const esvisible =
268 props.networkType === NETWORK_NAMES.ROPSTEN ||
206 props.networkType === NETWORK_NAMES.ROPSTEN ||
@@ -286,6 +224,7 @@ class AcceptTransfer extends React.Component {
286 "as. But you can transfer to any address you like."
224 "as. But you can transfer to any address you like."
287 }
225 }
288 </P>
226 </P>
227+
289 <AddressInput
228 <AddressInput
290 className='mono mt-8'
229 className='mono mt-8'
291 prop-size='lg'
230 prop-size='lg'
@@ -339,7 +278,7 @@ class AcceptTransfer extends React.Component {
339 handleSubmit={this.handleSubmit} />
278 handleSubmit={this.handleSubmit} />
340
279
341 {
280 {
342− Maybe.Nothing.hasInstance(state.txError)
281+ Nothing.hasInstance(state.txError)
343 ? ''
282 ? ''
344 : <Warning className={'mt-8'}>
283 : <Warning className={'mt-8'}>
345 <H3 style={{marginTop: 0, paddingTop: 0}}>
284 <H3 style={{marginTop: 0, paddingTop: 0}}>
@@ -350,6 +289,7 @@ class AcceptTransfer extends React.Component {
350 { state.txError.value }
289 { state.txError.value }
351 </Warning>
290 </Warning>
352 }
291 }
292+
353 </Col>
293 </Col>
354 </Row>
294 </Row>
355 )
295 )