on master help sign in
Refactor transaction funding & awaiting logic
e38a0b2Fang
src/bridge/lib/txn.js+21
@@ -161,6 +161,25 @@ const sendSignedTransaction = (web3, stx) => {
161 })
161 })
162}
162}
163
163
164+// returns a Promise
165+const waitForTransactionConfirm = (web3, txHash) => {
166+ return new Promise((resolve, reject) => {
167+ const checkForConfirm = async () => {
168+ console.log('checking for confirm', txHash);
169+ let confirmed = isTransactionConfirmed(web3, txHash);
170+ if (confirmed) resolve();
171+ else setTimeout(checkForConfirm, 13000);
172+ }
173+ checkForConfirm();
174+ });
175+}
176+
177+const isTransactionConfirmed = async (web3, txHash) => {
178+ const receipt = await web3.eth.getTransactionReceipt(txHash);
179+ console.log('got confirm state', receipt !== null, receipt.confirmations);
180+ return (receipt !== null);
181+}
182+
164const hexify = val => addHexPrefix(val.toString('hex'))
183const hexify = val => addHexPrefix(val.toString('hex'))
165
184
166const renderSignedTx = stx => ({
185const renderSignedTx = stx => ({
@@ -199,6 +218,8 @@ const canDecodePatp = p => {
199export {
218export {
200 signTransaction,
219 signTransaction,
201 sendSignedTransaction,
220 sendSignedTransaction,
221+ waitForTransactionConfirm,
222+ isTransactionConfirmed,
202 TXN_PURPOSE,
223 TXN_PURPOSE,
203 getTxnInfo,
224 getTxnInfo,
204 renderTxnPurpose,
225 renderTxnPurpose,
src/bridge/views/InviteTransactions.js+63−86
@@ -19,7 +19,12 @@ import { BRIDGE_ERROR } from '../lib/error'
19import { Row, Col, Warning, Button, H1, H3 } from '../components/Base'
19import { Row, Col, Warning, Button, H1, H3 } from '../components/Base'
20
20
21// for transaction generation and signing
21// for transaction generation and signing
22+import {
23+ signTransaction,
22−import { signTransaction, sendSignedTransaction } from '../lib/txn'
24+ sendSignedTransaction,
25+ waitForTransactionConfirm,
26+ isTransactionConfirmed
27+} from '../lib/txn'
23import { attemptSeedDerivation, genKey } from '../lib/keys'
28import { attemptSeedDerivation, genKey } from '../lib/keys'
24import * as kg from '../../../node_modules/urbit-key-generation/dist/index'
29import * as kg from '../../../node_modules/urbit-key-generation/dist/index'
25import * as tank from '../lib/tank'
30import * as tank from '../lib/tank'
@@ -43,10 +48,13 @@ class InviteVerify extends React.Component {
43 point: this.props.routeData.point,
48 point: this.props.routeData.point,
44 ticket: this.props.routeData.ticket
49 ticket: this.props.routeData.ticket
45 }
50 }
51+
52+ this.startTransactions = this.startTransactions.bind(this);
46 }
53 }
47
54
48 componentDidMount() {
55 componentDidMount() {
56+ //NOTE delayed so that we hang *after* rendering the page
49− this.startTransactions();
57+ setTimeout(this.startTransactions, 100);
50 }
58 }
51
59
52 componentDidUpdate(prevProps) {
60 componentDidUpdate(prevProps) {
@@ -82,8 +90,8 @@ class InviteVerify extends React.Component {
82
90
83 // ping gas tank with txs if needed
91 // ping gas tank with txs if needed
84
92
85− let nonce = await web3.eth.getTransactionCount(inviteAddress);
93+ let inviteNonce = await web3.eth.getTransactionCount(inviteAddress);
86− transferTx.nonce = nonce++;
94+ transferTx.nonce = inviteNonce++;
87 transferTx.gasPrice = 20000000000; //NOTE we pay the premium for faster ux
95 transferTx.gasPrice = 20000000000; //NOTE we pay the premium for faster ux
88 transferTx.from = inviteAddress;
96 transferTx.from = inviteAddress;
89
97
@@ -92,53 +100,11 @@ class InviteVerify extends React.Component {
92 transferStx.sign(inviteWallet.privateKey);
100 transferStx.sign(inviteWallet.privateKey);
93 let rawTransferStx = '0x'+transferStx.serialize().toString('hex');
101 let rawTransferStx = '0x'+transferStx.serialize().toString('hex');
94
102
95− let transferCost = (transferTx.gasPrice * transferTx.gas);
103+ const transferCost = transferTx.gas * transferTx.gasPrice;
96− let balance = await web3.eth.getBalance(inviteAddress);
104+ await this.ensureFundsFor(web3, point, inviteAddress, transferCost, [rawTransferStx]);
97− console.log('balance', balance, 'transfercost', transferCost, 'need funds', transferCost > balance);
98− if (transferCost > balance) {
99−
100− //TODO want to do this earlier, maybe? but we need costs for all
101− // transactions, and all balances, known before we can display useful
102− // prompts to the user.
103− const fundsRemaining = await tank.remainingTransactions(point);
104− console.log('freebies remaining', fundsRemaining);
105− if (fundsRemaining < 1) {
106− throw new Error(`TODO present user with "please fund $(inviteAddress) with $(transferCost), then click to retry"`);
107− }
108−
109− const res = await this.pingGasTank([rawTransferStx]);
110− if (!res) {
111− //TODO show pls fund msg + try again button
112− console.log('tank request failed');
113− }
114−
115− //TODO await confirm of res.txHash
116− console.log('funded transfer tx');
117− } else {
118− console.log('paying for ourselves');
119− }
120
105
121 // send transaction
106 // send transaction
122− await web3.eth.sendSignedTransaction(rawTransferStx);
107+ await this.sendAndAwaitConfirm(web3, [rawTransferStx]);
123−
124− const transferTxHash = web3.utils.keccak256(rawTransferStx);
125− //TODO refactor into waitForConfirm function in /lib/txn
126− let confirmed = false;
127− let success = false;
128− while (!confirmed) {
129− let receipt = await web3.eth.getTransactionReceipt(transferTxHash);
130− confirmed = (receipt !== null);
131− if (confirmed) {
132− console.log('confirmed!', receipt);
133− success = receipt.status;
134− }
135− }
136−
137− if (!success) {
138− throw new Error('transaction failed');
139− }
140−
141− console.log('sent transfer tx');
142
108
143 //
109 //
144 // we're gonna be operating as the new wallet from here on out, so change
110 // we're gonna be operating as the new wallet from here on out, so change
@@ -194,37 +160,14 @@ class InviteVerify extends React.Component {
194 return '0x'+stx.serialize().toString('hex');
160 return '0x'+stx.serialize().toString('hex');
195 });
161 });
196
162
197− //TODO copy-pasted from above, refactor
163+ await this.ensureFundsFor(web3, point, newAddress, totalCost, rawStxs);
198− balance = await web3.eth.getBalance(newAddress);
199− console.log('balance', balance, 'totalcost', totalCost, 'need funds', totalCost > balance);
200− if (totalCost > balance) {
201−
202− const fundsRemaining = await tank.remainingTransactions(point);
203− console.log('freebies remaining', fundsRemaining);
204− if (fundsRemaining < 2) {
205− throw new Error(`TODO present user with "please fund $(newAddress) with at least $(totalCost), then click to retry"`);
206− }
207−
208− const res = await this.pingGasTank(rawStxs);
209− if (!res) {
210− //TODO show pls fund msg + try again button
211− console.log('tank request failed');
212− }
213
164
214− //TODO await confirm of res.txHash
165+ await this.sendAndAwaitConfirm(web3, rawStxs);
215− console.log('funded transfer tx');
216− } else {
217− console.log('paying for ourselves');
218− }
219−
220− await web3.eth.sendSignedTransaction(rawStxs[0]);
221− await web3.eth.sendSignedTransaction(rawStxs[1]);
222 console.log('sent transactions');
166 console.log('sent transactions');
223− //TODO use PromiEvent functions to wait for confirm
224
167
225 // if non-trivial eth left in invite wallet, transfer to new ownership
168 // if non-trivial eth left in invite wallet, transfer to new ownership
226 //TODO
169 //TODO
227− balance = await web3.eth.getBalance(inviteAddress);
170+ let balance = await web3.eth.getBalance(inviteAddress);
228 const gasPrice = 20000000000;
171 const gasPrice = 20000000000;
229 const gasLimit = 21000;
172 const gasLimit = 21000;
230 const sendEthCost = gasPrice * gasLimit;
173 const sendEthCost = gasPrice * gasLimit;
@@ -236,12 +179,14 @@ class InviteVerify extends React.Component {
236 value: value,
179 value: value,
237 gasPrice: gasPrice,
180 gasPrice: gasPrice,
238 gas: gasLimit,
181 gas: gasLimit,
239− nonce: nonce++
182+ nonce: inviteNonce++
240 }
183 }
241 let stx = new Tx(tx);
184 let stx = new Tx(tx);
242 stx.sign(inviteWallet.privateKey);
185 stx.sign(inviteWallet.privateKey);
243 const rawTx = '0x'+stx.serialize().toString('hex');
186 const rawTx = '0x'+stx.serialize().toString('hex');
244− await web3.eth.sendSignedTransaction(rawTx);
187+ await web3.eth.sendSignedTransaction(rawTx).catch(err => {
188+ console.log('error sending value tx, who cares', err);
189+ });
245 console.log('sent old balance');
190 console.log('sent old balance');
246 }
191 }
247
192
@@ -250,22 +195,54 @@ class InviteVerify extends React.Component {
250 //TODO forward to "all done!" screen
195 //TODO forward to "all done!" screen
251 }
196 }
252
197
253− //TODO move into tank.js?
198+ async ensureFundsFor(web3, point, address, cost, signedTxs) {
254− async pingGasTank(signedTxs) {
199+ let balance = await web3.eth.getBalance(address);
255− //TODO use folktale/either folktale/result more
256− const fundRes = await tank.fundTransactions(signedTxs);
257
200
201+ if (cost > balance) {
202+
203+ const fundsRemaining = await tank.remainingTransactions(point);
204+ if (fundsRemaining < signedTxs.length) {
205+ throw new Error(`TODO present user with "please fund $(address) with at least $(cost), then click to retry"`);
206+ }
207+
208+ const res = await tank.fundTransactions(signedTxs);
258− let success = false;
209+ if (!res.success) {
210+ //TODO show pls fund msg
211+ console.log('tank request failed');
212+ } else {
213+ await waitForTransactionConfirm(web3, res.txHash);
214+ let newBalance = await web3.eth.getBalance(address);
215+ console.log('funds have confirmed', balance >= cost, balance, newBalance);
216+ }
259
217
260− if (fundRes.success === true) {
261− //TODO await confirmation on fundRes.txHash
262 } else {
218 } else {
263− console.log(fundRes.reason);
219+ console.log('already have sufficient funds');
264 }
220 }
221+ }
265
222
266− return success;
223+ async sendAndAwaitConfirm(web3, signedTxs) {
224+ let promises = signedTxs.map(tx => {
225+ console.log('sending...');
226+ return new Promise((resolve, reject) => {
227+ web3.eth.sendSignedTransaction(tx).then(res => {
228+ //TODO figure out how to handle the doesn't-resolve-immediately case
229+ //res.transactionHash;
230+ resolve();
231+ }).catch(async err => {
232+ // if there's an error, check if it's because the transaction was
233+ // already confirmed prior to sending.
234+ // this is really only the case in local dev environments.
235+ const txHash = web3.utils.keccak256(tx);
236+ const confirmed = await isTransactionConfirmed(web3, txHash);
237+ if (confirmed) resolve();
238+ else reject(err);
239+ });
240+ });
241+ });
267 }
242 }
268
243
244+ //TODO waitForBalance(address, minBalance)
245+
269 render() {
246 render() {
270
247
271 const { point } = this.state;
248 const { point } = this.state;