-
Notifications
You must be signed in to change notification settings - Fork 21
/
transaction.ts
249 lines (223 loc) · 10.5 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
import { AddressOrPair, SubmittableExtrinsic } from "@polkadot/api/types";
import { ISubmittableResult } from "@polkadot/types/types";
import { EventRecord, DispatchError } from "@polkadot/types/interfaces/system";
import { ExtrinsicStatus } from "@polkadot/types/interfaces/author";
import { ApiPromise } from "@polkadot/api";
import { AugmentedEvent, ApiTypes } from "@polkadot/api/types";
import type { AnyTuple } from "@polkadot/types/types";
import { ACCOUNT_NOT_SET_ERROR_MESSAGE, IGNORED_ERROR_MESSAGES } from "../utils/constants";
import { MonetaryAmount, Currency } from "@interlay/monetary-js";
import { tokenSymbolToCurrency, newMonetaryAmount } from "../utils";
import { DryRunResult } from "../types";
export interface TransactionAPI {
api: ApiPromise;
setAccount(account: AddressOrPair): void;
removeAccount(): void;
getAccount(): AddressOrPair | undefined;
sendLogged<T extends AnyTuple>(
transaction: SubmittableExtrinsic<"promise">,
successEventType?: AugmentedEvent<ApiTypes, T>,
extrinsicStatus?: ExtrinsicStatus
): Promise<ISubmittableResult>;
/**
* Builds a submittable extrinsic to send other extrinsic in batch.
*
* @param extrinsics An array of extrinsics to be submitted as batch.
* @param atomic Whether the given extrinsics should be handled atomically or not.
* When true (default) all extrinsics will rollback if one fails (batchAll), otherwise allows partial successes (batch).
* @returns A batch/batchAll submittable extrinsic.
*/
buildBatchExtrinsic(
extrinsics: SubmittableExtrinsic<"promise", ISubmittableResult>[],
atomic?: boolean
): SubmittableExtrinsic<"promise", ISubmittableResult>;
/**
* Getter for fee estimate of the extrinsic.
*
* @param {SubmittableExtrinsic<"promise">} extrinsic Extrinsic to get fee estimation about.
* @returns {MonetaryAmount<Currency>} amount of native currency that will be paid as transaction fee.
* @note This fee estimation does not include tip.
*/
getFeeEstimate(extrinsic: SubmittableExtrinsic<"promise">): Promise<MonetaryAmount<Currency>>;
/**
* Tests extrinsic execution against runtime.
*
* @param {SubmittableExtrinsic<"promise">} extrinsic Extrinsic to dry run.
* @return {Promise<DryRunResult>} Object consisting of `success` boolean that is true if extrinsic
* was successfully executed, false otherwise. If execution fails, caught error is exposed.
*/
dryRun(extrinsic: SubmittableExtrinsic<"promise">): Promise<DryRunResult>;
}
export class DefaultTransactionAPI implements TransactionAPI {
constructor(public api: ApiPromise, private account?: AddressOrPair) {}
public setAccount(account: AddressOrPair): void {
this.account = account;
}
public removeAccount(): void {
this.account = undefined;
}
public getAccount(): AddressOrPair | undefined {
return this.account;
}
async sendLogged<T extends AnyTuple>(
transaction: SubmittableExtrinsic<"promise">,
successEventType?: AugmentedEvent<ApiTypes, T>,
extrinsicStatus?: ExtrinsicStatus
): Promise<ISubmittableResult> {
if (this.account === undefined) {
return Promise.reject(new Error(ACCOUNT_NOT_SET_ERROR_MESSAGE));
}
return DefaultTransactionAPI.sendLogged(this.api, this.account, transaction, successEventType, extrinsicStatus);
}
async getFeeEstimate(extrinsic: SubmittableExtrinsic<"promise">): Promise<MonetaryAmount<Currency>> {
const nativeCurrency = tokenSymbolToCurrency(this.api.consts.currency.getNativeCurrencyId.asToken);
const account = this.account;
if (account === undefined) {
return newMonetaryAmount(0, nativeCurrency);
}
const paymentInfo = await extrinsic.paymentInfo(account);
return newMonetaryAmount(paymentInfo.partialFee.toString(), nativeCurrency);
}
async dryRun(extrinsic: SubmittableExtrinsic<"promise">): Promise<DryRunResult> {
if (this.account === undefined) {
return Promise.reject(new Error(ACCOUNT_NOT_SET_ERROR_MESSAGE));
}
try {
await extrinsic.dryRun(this.account);
return { success: true };
} catch (error) {
return { success: false, error };
}
}
buildBatchExtrinsic(
extrinsics: SubmittableExtrinsic<"promise", ISubmittableResult>[],
atomic: boolean = true
): SubmittableExtrinsic<"promise", ISubmittableResult> {
return DefaultTransactionAPI.buildBatchExtrinsic(this.api, extrinsics, atomic);
}
/**
* Builds a submittable extrinsic to send other extrinsic in batch.
*
* @param api The ApiPromis instance to construct the batch extrinsic with.
* @param extrinsics An array of extrinsics to be submitted as batch.
* @param atomic Whether the given extrinsics should be handled atomically or not.
* When true (default) all extrinsics will rollback if one fails (batchAll), otherwise allows partial successes (batch).
* @returns A batch/batchAll submittable extrinsic.
*/
static buildBatchExtrinsic(
api: ApiPromise,
extrinsics: SubmittableExtrinsic<"promise", ISubmittableResult>[],
atomic: boolean = true
): SubmittableExtrinsic<"promise", ISubmittableResult> {
const batchExtrinsic = (atomic ? api.tx.utility.batchAll : api.tx.utility.batch)(extrinsics);
return batchExtrinsic;
}
static async sendLogged<T extends AnyTuple>(
api: ApiPromise,
account: AddressOrPair,
transaction: SubmittableExtrinsic<"promise">,
successEventType?: AugmentedEvent<ApiTypes, T>,
extrinsicStatus?: ExtrinsicStatus
): Promise<ISubmittableResult> {
const { unsubscribe, result } = await new Promise<{ unsubscribe: () => void; result: ISubmittableResult }>((resolve, reject) => {
let unsubscribe: () => void;
// When passing { nonce: -1 } to signAndSend the API will use system.accountNextIndex to determine the nonce
transaction
.signAndSend(account, { nonce: -1 }, (result: ISubmittableResult) => callback({ unsubscribe, result }))
.then((u: () => void) => (unsubscribe = u))
.catch((error) => reject(error));
let foundStatus = false;
// only need to check this if we do want to wait for the success event
let foundEvent = successEventType !== undefined;
function callback(callbackObject: { unsubscribe: () => void; result: ISubmittableResult }): void {
const status = callbackObject.result.status;
foundStatus =
foundStatus ||
(extrinsicStatus ? extrinsicStatus.type === status.type : status.isInBlock) ||
status.isFinalized;
foundEvent =
// if we found it before there is no need to check again
foundEvent ||
// if the event we are looking for is undefined, assume it has been found
successEventType === undefined ||
DefaultTransactionAPI.doesArrayContainEvent(callbackObject.result.events, successEventType);
if (foundStatus && foundEvent) {
resolve(callbackObject);
}
}
});
if (result.status.isInBlock) {
console.log(`Transaction included at blockHash ${result.status.asInBlock}`);
} else if (result.status.isFinalized) {
console.log(`Transaction finalized at blockHash ${result.status.asFinalized}`);
}
unsubscribe();
// Print all events for debugging
DefaultTransactionAPI.printEvents(api, result.events);
const dispatchError = result.dispatchError;
if (dispatchError) {
// Construct error message
let message = "The transaction failed.";
// Runtime error in one of the parachain modules
if (dispatchError.isModule) {
// for module errors, we have the section indexed, lookup
const decoded = api.registry.findMetaError(dispatchError.asModule);
const { docs, name, section } = decoded;
message = message.concat(` The error code is ${section}.${name}. ${docs.join(" ")}`);
// Bad origin
} else if (dispatchError.isBadOrigin) {
message = message.concat(` The error is caused by using an incorrect account.
The error code is BadOrigin ${dispatchError}.`);
}
// Other, CannotLookup, no extra info
else {
message = message.concat(` The error is ${dispatchError}.`);
}
console.log(message);
return Promise.reject(new Error(message));
}
return result;
}
static printEvents(api: ApiPromise, events: EventRecord[]): void {
let foundErrorEvent = false;
let errorMessage = "";
events
.map(({ event }) => event.data)
.forEach((eventData) => {
if (DefaultTransactionAPI.isDispatchError(eventData)) {
try {
const { docs, name, section } = api.registry.findMetaError(eventData.asModule);
if (docs?.length > 0) {
errorMessage = `${section}.${name}: ${docs.join(" ")}`;
} else {
errorMessage = `${section}.${name}`;
}
foundErrorEvent = true;
} catch (err) {
errorMessage = "Error. Could not find transaction failure details.";
}
}
});
if (!foundErrorEvent) {
events.forEach(({ phase, event: { data, method, section } }) => {
console.log(`\t' ${phase}: ${section}.${method}:: ${data}`);
});
} else if (!IGNORED_ERROR_MESSAGES.includes(errorMessage)) {
throw new Error(errorMessage);
}
}
static isDispatchError(eventData: unknown): eventData is DispatchError {
return (eventData as DispatchError).isModule !== undefined;
}
static doesArrayContainEvent<T extends AnyTuple>(
events: EventRecord[],
eventType: AugmentedEvent<ApiTypes, T>
): boolean {
for (const { event } of events) {
if (eventType.is(event)) {
return true;
}
}
return false;
}
}