on master help sign in
Incorporate gas tank into StatelessTransaction
062cc51Fang
src/bridge/components/StatelessTransaction.js+106−40
@@ -1,4 +1,5 @@
1import { Nothing, Just } from 'folktale/maybe'
1import { Nothing, Just } from 'folktale/maybe'
2+import { Ok } from 'folktale/result'
2import React from 'react'
3import React from 'react'
3
4
4import { Code, H3 } from './Base'
5import { Code, H3 } from './Base'
@@ -10,14 +11,24 @@ import { Warning } from '../components/Base'
10import { addressFromSecp256k1Public } from '../lib/wallet'
11import { addressFromSecp256k1Public } from '../lib/wallet'
11import { BRIDGE_ERROR } from '../lib/error'
12import { BRIDGE_ERROR } from '../lib/error'
12import { ROUTE_NAMES } from '../lib/router'
13import { ROUTE_NAMES } from '../lib/router'
14+import * as tank from '../lib/tank'
13
15
14import {
16import {
15 sendSignedTransaction,
17 sendSignedTransaction,
18+ waitForTransactionConfirm,
16 fromWei,
19 fromWei,
20+ toWei,
21+ hexify,
17 renderSignedTx,
22 renderSignedTx,
18 signTransaction
23 signTransaction
19 } from '../lib/txn'
24 } from '../lib/txn'
20
25
26+const SUBMISSION_STATES = {
27+ PROMPT: 'Send transaction',
28+ FUNDING: 'Finding transaction funding...',
29+ SENDING: 'Sending transaction...'
30+};
31+
21class StatelessTransaction extends React.Component {
32class StatelessTransaction extends React.Component {
22
33
23 constructor(props) {
34 constructor(props) {
@@ -33,11 +44,11 @@ class StatelessTransaction extends React.Component {
33 nonce: '0',
44 nonce: '0',
34 stx: Nothing(),
45 stx: Nothing(),
35 txn: Nothing(),
46 txn: Nothing(),
47+ txStatus: SUBMISSION_STATES.PROMPT,
36 txError: Nothing()
48 txError: Nothing()
37 }
49 }
38
50
39 this.createUnsignedTxn = this.createUnsignedTxn.bind(this)
51 this.createUnsignedTxn = this.createUnsignedTxn.bind(this)
40− this.submit = this.submit.bind(this)
41 this.setUserApproval = this.setUserApproval.bind(this)
52 this.setUserApproval = this.setUserApproval.bind(this)
42 this.setTxn = this.setTxn.bind(this)
53 this.setTxn = this.setTxn.bind(this)
43 this.setStx = this.setStx.bind(this)
54 this.setStx = this.setStx.bind(this)
@@ -86,32 +97,6 @@ class StatelessTransaction extends React.Component {
86 });
97 });
87 }
98 }
88
99
89− submit(){
90− const { props, state } = this
91− sendSignedTransaction(props.web3.value, state.stx, props.setTxnConfirmations)
92− .then(sent => {
93− props.setTxnHashCursor(sent)
94− props.popRoute()
95−
96− let routeData = {};
97− if (props.networkSeed) {
98− props.setNetworkSeedCache(props.networkSeed);
99− routeData.promptKeyfile = true;
100− }
101− if (props.promptText) {
102− routeData.promptText = props.promptText;
103− }
104− props.pushRoute(ROUTE_NAMES.SENT_TRANSACTION, routeData);
105− })
106− .catch(err => {
107− if (err.map) {
108− this.setState({ txError: err.map(val => val.merge()) })
109− } else {
110− this.setState({ txError: err })
111− }
112− })
113− }
114−
115 handleChainUpdate(chainId) {
100 handleChainUpdate(chainId) {
116 if (chainId === "custom") {
101 if (chainId === "custom") {
117 this.setState({
102 this.setState({
@@ -194,16 +179,95 @@ class StatelessTransaction extends React.Component {
194 })
179 })
195 }
180 }
196
181
197− // TODO: Investigate
182+ async sendTxn() {
198− // setState doesn't seem to work in SetProxy/submit;
183+ const { props, state } = this;
199− // - TypeError: Cannot read property 'updater' of undefined
184+ const web3 = props.web3.value;
200− // so just modifying the DOM manually here. (https://imgur.com/a/i0Qsyq1)
185+
186+ this.setState({ txStatus: SUBMISSION_STATES.FUNDING });
187+
188+ const rawTx = hexify(state.stx.value.serialize());
189+ const cost = (state.gasLimit * toWei(state.gasPrice, 'gwei'));
201
190
202− sendTxn(e) {
191+ //TODO need a lib function or something for address=, it's everywhere
203− e.target.setAttribute("disabled", true);
192+ const address = props.wallet.matchWith({
204− let spinner = e.target.querySelectorAll('.btn-spinner')[0];
193+ Just: wal => addressFromSecp256k1Public(wal.value.publicKey),
205− spinner.classList.remove('hide');
194+ Nothing: () => {
206− this.submit()
195+ throw BRIDGE_ERROR.MISSING_WALLET
196+ }
197+ });
198+ let balance = await web3.eth.getBalance(address);
199+ let hasBalance = (balance >= cost);
200+
201+ let usedTank = false;
202+ // if we need to, try and fund the transaction
203+ if (!hasBalance) {
204+ hasBalance = await this.ensureFundsFor(web3, address, cost, [rawTx]);
205+ usedTank = hasBalance;
206+ }
207+
208+ // if we still don't have sufficient balance, fail and tell the user
209+ if (!hasBalance) {
210+ this.setState({
211+ txStatus: SUBMISSION_STATES.PROMPT,
212+ txError: Just(`Insufficient funds.
213+ Address ${address} needs at least ${fromWei(cost.toString())} ETH,
214+ currently has ${fromWei(balance.toString())} ETH.`
215+ )
216+ });
217+
218+ // if we have the balance, proceed with submission
219+ } else {
220+ this.setState({ txStatus: SUBMISSION_STATES.SENDING });
221+
222+ sendSignedTransaction(
223+ web3,
224+ state.stx,
225+ usedTank,
226+ props.setTxnConfirmations
227+ )
228+ .then(txHash => {
229+ props.setTxnHashCursor(Just(Ok(txHash)));
230+ props.popRoute();
231+
232+ let routeData = {};
233+ if (props.networkSeed) {
234+ props.setNetworkSeedCache(props.networkSeed);
235+ routeData.promptKeyfile = true;
236+ }
237+ if (props.promptText) {
238+ routeData.promptText = props.promptText;
239+ }
240+ props.pushRoute(ROUTE_NAMES.SENT_TRANSACTION, routeData);
241+ })
242+ .catch(err => {
243+ this.setState({
244+ txStatus: SUBMISSION_STATES.PROMPT,
245+ txError: err
246+ });
247+ });
248+ }
249+ }
250+
251+ //TODO partially copied from InviteTransactions, try to move into tank lib
252+ async ensureFundsFor(web3, address, cost, signedTxs) {
253+ let balance = await web3.eth.getBalance(address);
254+ if (cost > balance) {
255+ try {
256+
257+ const res = await tank.fundTransactions(signedTxs);
258+ if (!res.success) {
259+ return false;
260+ } else {
261+ await waitForTransactionConfirm(web3, res.txHash);
262+ return true;
263+ }
264+
265+ } catch (e) {
266+ return false;
267+ }
268+ } else {
269+ return true;
270+ }
207 }
271 }
208
272
209 getChainTitle(chainId) {
273 getChainTitle(chainId) {
@@ -249,7 +313,7 @@ class StatelessTransaction extends React.Component {
249 const { web3, canGenerate } = this.props
313 const { web3, canGenerate } = this.props
250 const { gasPrice, gasLimit, nonce, chainId,
314 const { gasPrice, gasLimit, nonce, chainId,
251 txn, stx, userApproval, showGasDetails,
315 txn, stx, userApproval, showGasDetails,
252− customChain } = this.state
316+ customChain, txStatus } = this.state
253
317
254 const { setNonce, setChainId, setGasLimit, setGasPrice, toggleGasDetails,
318 const { setNonce, setChainId, setGasLimit, setGasPrice, toggleGasDetails,
255 setUserApproval, sendTxn, createUnsignedTxn, handleChainUpdate } = this
319 setUserApproval, sendTxn, createUnsignedTxn, handleChainUpdate } = this
@@ -439,16 +503,18 @@ class StatelessTransaction extends React.Component {
439 </div>
503 </div>
440 </CheckboxButton>
504 </CheckboxButton>
441
505
506+ const sending = (txStatus !== SUBMISSION_STATES.PROMPT);
507+ const sendTxnSpinner = sending ? '' : 'hide';
442 const sendTxnButton =
508 const sendTxnButton =
443 <Button
509 <Button
444 prop-size={ 'xl wide' }
510 prop-size={ 'xl wide' }
445 className={ 'mt-8' }
511 className={ 'mt-8' }
446− disabled={ !canSend }
512+ disabled={ !canSend || sending }
447 onClick={ sendTxn }
513 onClick={ sendTxn }
448 >
514 >
449 <span className="relative">
515 <span className="relative">
450− <span className="btn-spinner hide"></span>
516+ <span className={`btn-spinner ${sendTxnSpinner}`}></span>
451− { 'Send Transaction' }
517+ { txStatus }
452 </span>
518 </span>
453 </Button>
519 </Button>
454
520
src/bridge/lib/txn.js+38−21
@@ -138,32 +138,49 @@ const signTransaction = async config => {
138 setStx(Just(stx))
138 setStx(Just(stx))
139}
139}
140
140
141−const sendSignedTransaction = (web3, stx, confirmationCb) => {
141+const sendSignedTransaction = (web3, stx, doubtNonceError, confirmationCb) => {
142 const txn = stx.matchWith({
142 const txn = stx.matchWith({
143 Just: (tx) => tx.value,
143 Just: (tx) => tx.value,
144 Nothing: () => {
144 Nothing: () => {
145 throw BRIDGE_ERROR.MISSING_TXN
145 throw BRIDGE_ERROR.MISSING_TXN
146 }
146 }
147− })
147+ });
148−
149− const serializedTx = addHexPrefix(txn.serialize().toString('hex'))
150
148
149+ const rawTx = addHexPrefix(txn.serialize().toString('hex'));
150+
151− return new Promise((resolve, reject) => {
151+ return new Promise(async (resolve, reject) => {
152− web3.eth.sendSignedTransaction(serializedTx)
152+ web3.eth.sendSignedTransaction(rawTx)
153− .on('transactionHash', hash =>
153+ .on('transactionHash', hash => {
154− resolve(Just(Ok(hash)))
154+ resolve(hash);
155− )
156− .on('receipt', txn => {
157− resolve(Just(Ok(txn.transactionHash)))
158− })
155+ })
159− .on('confirmation', (confirmationNum, txn) => {
156+ .on('confirmation', (confirmationNum, txn) => {
160− confirmationCb(txn.transactionHash, confirmationNum + 1)
157+ confirmationCb(txn.transactionHash, confirmationNum + 1);
161− resolve(Just(Ok(txn.transactionHash)))
158+ resolve(txn.transactionHash);
162− })
159+ })
163− .on('error', err => {
160+ .on('error', err => {
164− reject(Just(Error(err.message)))
161+ // if there's a nonce error, but we used the gas tank, it's likely
162+ // that it's because the tank already submitted our transaction.
163+ // we just wait for first confirmation here.
164+ if (doubtNonceError &&
165+ err.message.includes("the tx doesn't have the correct nonce.")) {
166+ console.log('nonce error, likely from gas tank submission, ignoring');
167+ const txHash = web3.utils.keccak256(rawTx);
168+ //TODO can we do does-exists check first?
169+ //TODO max wait time before assume fail?
170+ waitForTransactionConfirm(web3, txHash)
171+ .then(res => {
172+ if (res) {
173+ resolve(txHash);
174+ confirmationCb(txHash, 1);
175+ } else {
176+ reject('Unexpected tx failure');
177+ }
178+ });
179+ } else {
180+ reject(err.message);
165− })
181+ }
182+ });
166− })
183+ });
167}
184}
168
185
169// returns a Promise<bool>, where the bool indicates tx success/failure
186// returns a Promise<bool>, where the bool indicates tx success/failure
@@ -174,7 +191,7 @@ const waitForTransactionConfirm = (web3, txHash) => {
174 const receipt = await web3.eth.getTransactionReceipt(txHash);
191 const receipt = await web3.eth.getTransactionReceipt(txHash);
175 console.log('tried, got', receipt);
192 console.log('tried, got', receipt);
176 let confirmed = (receipt !== null);
193 let confirmed = (receipt !== null);
177− if (confirmed) resolve(receipt.status === '0x1');
194+ if (confirmed) resolve(receipt.status === true);
178 else setTimeout(checkForConfirm, 13000);
195 else setTimeout(checkForConfirm, 13000);
179 }
196 }
180 checkForConfirm();
197 checkForConfirm();