-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathTransaction.ts
397 lines (343 loc) · 14.3 KB
/
Transaction.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
import BN from "bn.js";
import { EventEmitter } from "events";
import { PromiEvent } from "web3-core";
import {
NonPayableTransactionObject,
PayableTransactionObject,
NonPayableTx,
PayableTx
} from "../generated/types/types";
import { TransactionError, TransactionSendError } from "./Errors";
import { EthereumConnection } from "./EthereumConnection";
interface ISendOptions {
from?: string;
to?: string;
gasLimit?: number;
gasPrice?: number;
nonce?: number;
value?: BN;
}
interface ITxToSign extends ISendOptions {
data: string;
}
/** result from web3.eth.accounts.signTransaction */
interface ISignedTransaction {
rawTransaction: string;
tx: {
nonce: string;
gasPrice: string;
gas: string;
to: string;
value: string;
input: string;
v: string;
r: string;
s: string;
hash: string;
};
}
type IWeb3Tx = PromiEvent<any>;
type ITransactionReceipt = any; // TODO: use Web3's type
/**
* Transaction class to manage Ethereum transactions thourgh it's lifecycle.
*
* Recommended use:
* @example
* const ethereumConnection = new EthereumConnection(config);
* const rates = new Rates();
* await rates.connect()
* rates.setRate("USD", 121.12)
* .[sign(privatekey, {from: acc, to:rates.address})] // optionally you can sign
* .send([{from: acc}]) // from only needed if it's not signed
* .onceTxHash( txHash => {.. })
* .onceReceipt( receipt => { ...})
* .onConfirmation( (confirmationNumber, receipt) => {...}
* .onceReceiptConfirmed(5, receipt => {...})
* .onceTxRevert( (error, receipt) => { ....})
*
* // To catch errors you need to use txHash / confirmation / receipt getters:
* try {
* const txHash = await tx.getTxHash()
* const txReceipt = await tx.getReceipt() // receipt as soon as got it (even with 0 confirmation)
* const confirmedReceipt = await tx.getConfirmedReceipt(12) // receipt after x confirmation.
* // receipt you need to check for receipt.status if tx was Reverted or not.
* if (confirmedReceipt.status) {
* // all good
* } else {
* // this tx was reverted
* }
* } catch (error) {
* // These Promises are rejecting with sending errors or when txhash / receipt times out.
* }
*
* // Deprecated and discouraged but kept for backward compatibility with web3js style events:
* tx.on[ce]("transactionHash" | "receipt" | "confirmation" | "error")
* // This way it can be easily plugged into dapps which are handling web3js tx objects:
* // augmint-js Transaction object can be a drop in as an almost direct replacement of webjs transactioObject
*
* // To construct a transaction:
* const web3TxObject = rates.instance.methods.setRate(CCY, 100)
* const augmintRatesTx = new Transaction(ethereumConnection, web3TxObject, {gasLimit: 200000}); // you can set the gaslimit here or later at send() too
* augmintRatesTx.send(...).onTxHash(...) // or sign().send() etc.
*
*
* @export
* @fires transactionHash
* @fires receipt fired as soon as a receipt received
* @fires confirmation fired for each confirmation
* @fires error fired in case of any error. kept for backward compatibility
* @fires txRevert fired when tx was mined but with REVERT opcode. error also fired in this case for backward compatibility
* @class Transaction
* @extends {EventEmitter}
*/
export class Transaction extends EventEmitter {
public ethereumConnection: EthereumConnection;
public confirmationCount?: number;
public txHash?: string;
public txReceipt?: ITransactionReceipt;
public tx:
| PayableTransactionObject<any>
| NonPayableTransactionObject<any>; /** web3.js TransactionObject result from .methods.<methodname> */
public sendOptions: ISendOptions;
public isTxSent: boolean = false; /** indicate if .send was already called */
public sentTx?: IWeb3Tx;
public signedTx?: ISignedTransaction; /** set if signed */
public sendError?: any; /** if send returned error or tx REVERT error */
private signedTxPromise?: Promise<ISignedTransaction>;
private txReceiptPromise?: Promise<ITransactionReceipt>;
private txHashPromise?: Promise<string>;
private txToSign?: ITxToSign; /** Signature data when signing */
/**
* Creates an instance of Transaction.
*
* @param {EthereumConnection} ethereumConnection
* @param {TransactionObject<any>} tx the web3.js transaction object
* @param {ISendOptions} [sendOptions] optionally specify any of the send options here or later at sign or send
* @memberof Transaction
*/
constructor(
ethereumConnection: EthereumConnection,
tx: PayableTransactionObject<any> | NonPayableTransactionObject<any>,
sendOptions?: ISendOptions
) {
super();
if (!ethereumConnection || !tx) {
throw new TransactionError("Both ethereumConnection and tx must be provided for Transaction constructor");
}
this.tx = tx;
this.sendOptions = Object.assign({}, sendOptions);
this.ethereumConnection = ethereumConnection;
}
/**
* Sign the transaction with the provided private key
* and with the from, to, gas in ISendOptions.
* ISendOptions can be set in [Transaction] constructor too.
* make sure you set at least gasLimit and from.
* @param {string} privateKey Private key with leading 0x
* @param {ISendOptions} sendOptions
* @returns {Transaction} the [Transaction] object for chaining. Call [send] or [getSignedTranscation] on it
* @memberof Transaction
*/
public sign(privateKey: string, sendOptions: ISendOptions): Transaction {
if (this.isTxSent) {
throw new TransactionError("tx was already sent");
}
this.sendOptions = Object.assign({}, this.sendOptions, sendOptions);
if (!this.sendOptions.from) {
throw new TransactionError("from account is not set for sign");
}
this.txToSign = {
...this.sendOptions,
data: this.tx.encodeABI()
};
this.signedTxPromise = new Promise(async resolve => {
this.signedTx = await this.ethereumConnection.web3.eth.accounts.signTransaction(this.txToSign, privateKey);
resolve(this.signedTx);
});
return this;
}
public async getSignedTx(): Promise<ISignedTransaction> {
if (!this.signedTxPromise) {
throw new TransactionError("call .sign() first to get a signed transaction");
}
return this.signedTxPromise;
}
public send(sendOptions: ISendOptions): Transaction {
if (this.isTxSent) {
throw new TransactionError("tx was already sent");
}
this.isTxSent = true;
this.sendOptions = Object.assign({}, this.sendOptions, sendOptions);
if (this.signedTxPromise) {
this.getSignedTx().then(signedTx => {
if (
this.sendOptions.from &&
this.txToSign &&
this.txToSign.from &&
this.sendOptions.from.toLowerCase() !== this.txToSign.from.toLowerCase()
) {
throw new TransactionError(
"tx sign(sendOptions) and send( sendOptions) mismatch: from is differnt. Either don't provide from in sendOptions for send() or provide the same from address as for sign()"
);
}
this.sentTx = this.ethereumConnection.web3.eth.sendSignedTransaction(signedTx.rawTransaction);
this.addTxListeners(this.sentTx);
});
} else {
if (!this.sendOptions.from) {
throw new TransactionError("from account is not set for send");
}
try {
// webjs writes into passed params (beta36) (added .data to sendOptions and Metamask hang for long before confirmation apperaed)
// TODO: check if issues exists in latest web3
const _sendOptions: PayableTx | NonPayableTx = Object.assign({}, this.sendOptions);
this.sentTx = this.tx.send(_sendOptions);
} catch (error) {
this.sendError = new TransactionSendError(error);
throw this.sendError;
}
this.addTxListeners(this.sentTx);
}
return this;
}
public async getTxHash(): Promise<string> {
if (!this.isTxSent) {
throw new TransactionError("tx was not sent yet");
}
if (!this.txHashPromise) {
this.txHashPromise = new Promise((resolve, reject) => {
if (this.txHash) {
resolve(this.txHash);
}
if (this.sendError) {
reject(this.sendError); // tx already rejected and still no txHash
}
// tx not resolved yet so wait for our own transactionHash event
this.once("transactionHash", (hash: string) => {
this.txHash = hash;
resolve(hash);
});
this.once("error", (error: any) => {
if (this.txHash) {
// it's a tx revert. we still return the hash.
resolve(this.txHash);
} else {
reject(error);
}
});
});
}
return this.txHashPromise;
}
public async getTxReceipt(): Promise<ITransactionReceipt> {
if (!this.isTxSent) {
throw new TransactionError("tx was not sent yet");
}
if (!this.txReceiptPromise) {
this.txReceiptPromise = new Promise((resolve, reject) => {
if (this.txReceipt) {
resolve(this.txReceipt);
}
if (this.sendError) {
reject(this.sendError); // if tx already rejected and still no receipt
}
// tx not resolved yet so wait for our own transactionHash event
this.once("receipt", (receipt: ITransactionReceipt) => {
resolve(receipt);
});
this.once("error", error => {
if (this.txReceipt) {
resolve(this.txReceipt);
} else {
reject(error);
}
});
});
}
return this.txReceiptPromise;
}
public async getConfirmedReceipt(confirmationNumber: number = 1): Promise<ITransactionReceipt> {
if (!this.isTxSent) {
throw new TransactionError("tx was not sent yet");
}
if (this.confirmationCount && this.confirmationCount >= confirmationNumber) {
return this.txReceipt;
}
const txConfirmationPromise: Promise<ITransactionReceipt> = new Promise((resolve, reject) => {
if (this.sendError && !this.txReceipt) {
reject(this.sendError); // if tx already rejected and still no receipt
}
this.on("confirmation", (confNum: number, receipt: ITransactionReceipt) => {
if (confNum >= confirmationNumber) {
resolve(this.txReceipt);
}
});
// TODO: reject after x blocks if no confirmation received
});
return txConfirmationPromise;
}
public onceTxRevert(callback: (error: any, receipt: ITransactionReceipt) => any): Transaction {
this.once("txRevert", callback);
return this;
}
public onceTxHash(callback: (txHash: string) => any): Transaction {
this.once("transactionHash", callback);
return this;
}
public onceReceipt(callback: (receipt: ITransactionReceipt) => any): Transaction {
this.once("receipt", callback);
return this;
}
public onConfirmation(callback: (confirmationNumber: number, receipt: ITransactionReceipt) => any): Transaction {
this.on("confirmation", callback);
return this;
}
public onceConfirmedReceipt(
confirmationNumber: number,
callback: (receipt: ITransactionReceipt) => any
): Transaction {
this.once("transactionHash", async () => {
const receipt: ITransactionReceipt = await this.getConfirmedReceipt(confirmationNumber);
callback(receipt);
});
return this;
}
private addTxListeners(tx: IWeb3Tx): void {
tx.once("transactionHash", (hash: string) => {
this.txHash = hash;
this.emit("transactionHash", hash);
})
.once("receipt", (receipt: ITransactionReceipt) => {
this.setReceipt(receipt);
})
.on("error", async (error: any, receipt?: ITransactionReceipt) => {
this.sendError = new TransactionSendError(error);
if (this.txHash) {
if (!this.txReceipt) {
// workaround that web3js beta36 is not emmitting receipt event when tx fails on tx REVERT
this.setReceipt(await this.ethereumConnection.web3.eth.getTransactionReceipt(this.txHash));
}
// workaround that web3js beta36 is not emmitting confirmation events when tx fails on tx REVERT
this.sentTx.on(
"confirmation",
(confirmationNumber: number, receiptFromConf: ITransactionReceipt) => {
this.confirmationCount = confirmationNumber;
this.emit("confirmation", confirmationNumber);
}
);
this.emit("txRevert", this.sendError, this.txReceipt);
}
this.emit("error", this.sendError, this.txReceipt);
})
.on("confirmation", (confirmationNumber: number, receipt: ITransactionReceipt) => {
this.confirmationCount = confirmationNumber;
this.emit("confirmation", confirmationNumber);
});
}
private setReceipt(receipt: ITransactionReceipt): void {
if (!this.txReceipt && receipt) {
this.txReceipt = receipt;
this.emit("receipt", this.txReceipt);
}
}
}