on master help sign in
Incorporate gas tank into StatelessTransaction
commit062cc5153a6ee2163d7fb8278e657ffb6285c082 authorFang <git@fang.io> dateWed 8 May 2019, 01:11 (7 years ago) parente6846fa
2 files changed+144−61
src/bridge/components/StatelessTransaction.js+106−40 src/bridge/lib/txn.js+38−21
src/bridge/components/StatelessTransaction.js+106−40
@@ -1,4 +1,5 @@
11import { Nothing, Just } from 'folktale/maybe'
2+import { Ok } from 'folktale/result'
23import React from 'react'
34
45import { Code, H3 } from './Base'
@@ -10,14 +11,24 @@ import { Warning } from '../components/Base'
1011import { addressFromSecp256k1Public } from '../lib/wallet'
1112import { BRIDGE_ERROR } from '../lib/error'
1213import { ROUTE_NAMES } from '../lib/router'
14+import * as tank from '../lib/tank'
1315
1416import {
1517 sendSignedTransaction,
18+ waitForTransactionConfirm,
1619 fromWei,
20+ toWei,
21+ hexify,
1722 renderSignedTx,
1823 signTransaction
1924 } from '../lib/txn'
2025
26+const SUBMISSION_STATES = {
27+ PROMPT: 'Send transaction',
28+ FUNDING: 'Finding transaction funding...',
29+ SENDING: 'Sending transaction...'
30+};
31+
2132class StatelessTransaction extends React.Component {
2233
2334 constructor(props) {
@@ -33,11 +44,11 @@ class StatelessTransaction extends React.Component {
3344 nonce: '0',
3445 stx: Nothing(),
3546 txn: Nothing(),
47+ txStatus: SUBMISSION_STATES.PROMPT,
3648 txError: Nothing()
3749 }
3850
3951 this.createUnsignedTxn = this.createUnsignedTxn.bind(this)
40− this.submit = this.submit.bind(this)
4152 this.setUserApproval = this.setUserApproval.bind(this)
4253 this.setTxn = this.setTxn.bind(this)
4354 this.setStx = this.setStx.bind(this)
@@ -86,32 +97,6 @@ class StatelessTransaction extends React.Component {
8697 });
8798 }
8899
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−
115100 handleChainUpdate(chainId) {
116101 if (chainId === "custom") {
117102 this.setState({
@@ -194,16 +179,95 @@ class StatelessTransaction extends React.Component {
194179 })
195180 }
196181
197− // TODO: Investigate
198− // setState doesn't seem to work in SetProxy/submit;
199− // - TypeError: Cannot read property 'updater' of undefined
200− // so just modifying the DOM manually here. (https://imgur.com/a/i0Qsyq1)
182+ async sendTxn() {
183+ const { props, state } = this;
184+ const web3 = props.web3.value;
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'));
201190
202− sendTxn(e) {
203− e.target.setAttribute("disabled", true);
204− let spinner = e.target.querySelectorAll('.btn-spinner')[0];
205− spinner.classList.remove('hide');
206− this.submit()
191+ //TODO need a lib function or something for address=, it's everywhere
192+ const address = props.wallet.matchWith({
193+ Just: wal => addressFromSecp256k1Public(wal.value.publicKey),
194+ Nothing: () => {
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+ }
207271 }
208272
209273 getChainTitle(chainId) {
@@ -249,7 +313,7 @@ class StatelessTransaction extends React.Component {
249313 const { web3, canGenerate } = this.props
250314 const { gasPrice, gasLimit, nonce, chainId,
251315 txn, stx, userApproval, showGasDetails,
252− customChain } = this.state
316+ customChain, txStatus } = this.state
253317
254318 const { setNonce, setChainId, setGasLimit, setGasPrice, toggleGasDetails,
255319 setUserApproval, sendTxn, createUnsignedTxn, handleChainUpdate } = this
@@ -439,16 +503,18 @@ class StatelessTransaction extends React.Component {
439503 </div>
440504 </CheckboxButton>
441505
506+ const sending = (txStatus !== SUBMISSION_STATES.PROMPT);
507+ const sendTxnSpinner = sending ? '' : 'hide';
442508 const sendTxnButton =
443509 <Button
444510 prop-size={ 'xl wide' }
445511 className={ 'mt-8' }
446− disabled={ !canSend }
512+ disabled={ !canSend || sending }
447513 onClick={ sendTxn }
448514 >
449515 <span className="relative">
450− <span className="btn-spinner hide"></span>
451− { 'Send Transaction' }
516+ <span className={`btn-spinner ${sendTxnSpinner}`}></span>
517+ { txStatus }
452518 </span>
453519 </Button>
454520
src/bridge/lib/txn.js+38−21
@@ -138,32 +138,49 @@ const signTransaction = async config => {
138138 setStx(Just(stx))
139139}
140140
141−const sendSignedTransaction = (web3, stx, confirmationCb) => {
141+const sendSignedTransaction = (web3, stx, doubtNonceError, confirmationCb) => {
142142 const txn = stx.matchWith({
143143 Just: (tx) => tx.value,
144144 Nothing: () => {
145145 throw BRIDGE_ERROR.MISSING_TXN
146146 }
147− })
148−
149− const serializedTx = addHexPrefix(txn.serialize().toString('hex'))
147+ });
150148
151− return new Promise((resolve, reject) => {
152− web3.eth.sendSignedTransaction(serializedTx)
153− .on('transactionHash', hash =>
154− resolve(Just(Ok(hash)))
155− )
156− .on('receipt', txn => {
157− resolve(Just(Ok(txn.transactionHash)))
158− })
159− .on('confirmation', (confirmationNum, txn) => {
160− confirmationCb(txn.transactionHash, confirmationNum + 1)
161− resolve(Just(Ok(txn.transactionHash)))
162− })
163− .on('error', err => {
164− reject(Just(Error(err.message)))
165− })
166− })
149+ const rawTx = addHexPrefix(txn.serialize().toString('hex'));
150+
151+ return new Promise(async (resolve, reject) => {
152+ web3.eth.sendSignedTransaction(rawTx)
153+ .on('transactionHash', hash => {
154+ resolve(hash);
155+ })
156+ .on('confirmation', (confirmationNum, txn) => {
157+ confirmationCb(txn.transactionHash, confirmationNum + 1);
158+ resolve(txn.transactionHash);
159+ })
160+ .on('error', err => {
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);
181+ }
182+ });
183+ });
167184}
168185
169186// returns a Promise<bool>, where the bool indicates tx success/failure
@@ -174,7 +191,7 @@ const waitForTransactionConfirm = (web3, txHash) => {
174191 const receipt = await web3.eth.getTransactionReceipt(txHash);
175192 console.log('tried, got', receipt);
176193 let confirmed = (receipt !== null);
177− if (confirmed) resolve(receipt.status === '0x1');
194+ if (confirmed) resolve(receipt.status === true);
178195 else setTimeout(checkForConfirm, 13000);
179196 }
180197 checkForConfirm();